单向链表的常见问题

来源:互联网 发布:靠谱的网络征友 编辑:程序博客网 时间:2024/06/06 14:11
单向链表的常见问题链表中的对象也是按线性顺序排列的,但与数组不同,数组的线性顺序是由数组的下标,再深层次说是由数组的内存结构限定的,而链表中的顺序则是由各对象中的指针决定的。声明一个单向链表如下:class SLink_ListNode{public:    SLink_ListNode(int data,SLink_ListNode* pNext = NULL);    ~SLink_ListNode();    void ShowData();    int                m_data;    SLink_ListNode*    m_next;};class SingleLink_List{public:    SingleLink_List();    ~SingleLink_List();    SLink_ListNode* Append(int data);//添加一个结点并设置数据    int Delete(SLink_ListNode* pNode);//删除指定结点    SLink_ListNode* Search(int data);//查询指定数据的结点    int Delete(int data);//删除指定数据的结点    void ShowList();//显示列表数据    void Reverse();//非递归方法的列表反转    void RecursiveReverse(SLink_ListNode* pHead);//递归方法的列表反转    bool HasRing();//检测一个单向链表是否存在一个环    SLink_ListNode*     FirstRingNode();    SLink_ListNode*    m_pHead;    int                m_Length;}; 出于个人长久记忆的原因,逐一对每一个函数的实现做一下说明,毕竟最近面试遭人鄙视了,TMD!//链表结点的内部成员虽有指针类型,但此处无需进行内存操作,因为我们就是只需要一个指针变量,用于指向未来想指向的玩意。SLink_ListNode::SLink_ListNode(int data,SLink_ListNode* pNext){    m_data = data;    m_next = pNext;}SLink_ListNode::~SLink_ListNode(){    m_data = 0;    m_next = NULL;}此处在构造链表结点时,就将想存储的数据进行存储,个人感觉“做女人,这样挺好”。下面是LIST的构造与析构:SingleLink_List::SingleLink_List(){    //对于起始的一个空LIST而言,指针设置为空,长度设置为0就好了    m_pHead = NULL;    m_Length = 0;}SingleLink_List::~SingleLink_List(){    SLink_ListNode* pNext = m_pHead;    //依次向后推移头结点指针,推移一次,删除一个    while(pNext)    {        m_pHead = pNext->m_next;        delete pNext;        pNext = m_pHead;    }    m_Length = 0;}下面是链表的扩充方法:SLink_ListNode* SingleLink_List::Append(int data){    //向LIST中增加结点,首先要做的就是先为新结点开辟一块内存    SLink_ListNode* pNode = new SLink_ListNode(data);    if(NULL == pNode){        return NULL;    }    //结点生成之后,从LIST的前面逆向插入到链表中就可以了,这里有点栈的味道了,先插入的后被访问到    pNode->m_next = m_pHead;    m_pHead = pNode;    m_Length ++;    return m_pHead;} 下面是链表的结点删除方法://删除方法分了三种情况,首先看是不是个空链表,其次看是不是删除的头结点,最后才是删除的非头结点//因为头结点是没有前驱结点的,所以鄙人专门将删除头结点的情况专门处理int SingleLink_List::Delete(SLink_ListNode* pNode){    if(NULL == pNode){    //空        return -1;    }    if(pNode == m_pHead){//删除头结点,简单改变下头指针的指向就可以删除了        m_pHead = m_pHead->m_next;        delete pNode;        m_Length --;        return 0;    }    //删除非头结点,涉及到前,中,后三个结点的链接问题    SLink_ListNode *pNext = m_pHead->m_next;    SLink_ListNode *pPrev = m_pHead;    while(pNext)    {        if(pNode == pNext){            pPrev->m_next = pNode->m_next;            delete pNode;            m_Length --;            return 0;        }else{            pPrev = pNext;            pNext = pNext->m_next;        }    }    return -1;//链表中不存在要删除的结点,没办法} 查询方法://链表中数据的查询,只是一个简单的链表遍历SLink_ListNode* SingleLink_List::Search(int data){    SLink_ListNode* pNode = m_pHead;    while(NULL != pNode && pNode->m_data != data)    {        pNode = pNode->m_next;    }    return pNode;}int SingleLink_List::Delete(int data){    SLink_ListNode* pDelete = Search(data);    return Delete(pDelete);}单向链表就是个独眼龙,只能看到一个方向,这有好处也有坏处,好处是人家单纯,人家只看一个方向,坏处是他有点死板,只会正着搞,反着就不会。也正是这个特点,使单向链表的反转成为一个经典的笔试题目:先上代码:void SingleLink_List::Reverse(){    if(NULL == m_pHead)    {        return;    }    //从链表的第一个结点开始摸    SLink_ListNode* pCurrent = m_pHead;    //事先先摸清楚链表的第二个结点    SLink_ListNode* pNext = pCurrent->m_next;    //这里再找一个中介,因为你一旦把“线”给剪断了,如果不靠别的玩意临时抓住它的兄弟们,你就永远失去它们了    SLink_ListNode* pReverse = NULL;    //链表反转常忘记的问题,那就是开始的头结点,你丫的本来“前无古人”,现在你要“后无来者”了。    pCurrent->m_next = NULL;    while(NULL != pNext)//这里,循环的条件就变成“有货,咱就给让它转向”    {       pReverse = pNext->m_next;//抓住它的兄弟们!!务必        pNext->m_next = pCurrent;//给它本人转向        pCurrent = pNext;//更新转向灯,指定下一个转向点        pNext = pReverse;//让它的兄弟们一个一个转向!    }    m_pHead = pCurrent;//最后一个了,让它变成“老大”就是了}反转的基本思想就是:先让以前的头变尾(即将其NEXT指针设置为NULL),然后依次将每一个元素作为转向灯(即让其后的元素的NEXT指针指向它),从第二个元素开始让其转向,期间要注意一定要在切断“绳子”之前,先想办法抓住它的兄弟们(即找一个临时指针指向当前要转头的结点的后续列表),转完一个之后,要立即更新转向灯,依次进行,如是而已。  采用递归实现的话,方式微微有些变化,因为它是采用的从后向前转向的方式,不解释,直接上代码:void SingleLink_List::RecursiveReverse(SLink_ListNode* pHead){    if(NULL == pHead){        return;    }    if(NULL == pHead->m_next){        //当前结点已是最后一个,则让链表的头指针指向它就可以了        m_pHead = pHead;    }else{        //如果不是最后一个,则先解决它的内部问题,这也就是递归的思想了,经此步,一定会走进if条件中,        //完成链表新头结点的定位        RecursiveReverse(pHead->m_next);        //代码第一次走到这里,一定是经过一次回溯之后到达了原链表的倒数第二个结点        //此时的pHead就是倒数第二个结点,而它的NEXT是最后一个结点,再NEXT就是最后一个结点要指向的结点了        //好了,调个头就是了,让最后一个结点的的NEXT指向倒数第二个结点,就完成了调头        //一旦再次回溯,即RecursiveReverse再RETURN一次,pHead就是倒数第三个结点了,如是而已        pHead->m_next->m_next = pHead;        //这里有一个小小的细节,那就是在回溯的过程中,将每一个当前的结点的NEXT指定为NULL,这行代码虽然做了次重复的工作        //即为当前的结点指定后继,但这是必须的,这是为了保证原链表的头结点最终指向NULL,以结束链表        pHead->m_next = NULL;    }} 如此说来,链表的转向,无非就这两种方法,因为单向链表的特性,决定了其操作(哪位要是有第三种方法,请一定要赐教!)接下来,讨论另一个经典的问题,问,如何检测一个单向链表有没有环?如果按照传统最笨的方法,第一感觉就是一一作比较,不过仔细想,单向链表一旦有环,意味着什么?是不是意味着你永远也无法通过与NULL值的比较找到链表的结尾?此处暂时不多说,先看下流行且经典的检测单向链表是否存在环的方法: bool SingleLink_List::HasRing(){    if(NULL == m_pHead)    {        return false;    }    SLink_ListNode*    pSlow = m_pHead;    SLink_ListNode* pFast = m_pHead;    while(NULL != pSlow && NULL != pFast && NULL != pFast->m_next)    {        pSlow = pSlow->m_next;//一次前进一个结点        pFast = pFast->m_next->m_next;//一次前进两个结点        if(pSlow == pFast){//重合了!意味着什么?            return true;        }    }    return false;}此函数的原理想必大家都知道,即从链表的头开始,指定两个指针,一个一次移动一个位置,另一个一次移动两个位置,只要二者在某一时刻相等了,就意味着这个链表是存在环的。为什么?为什么出现相等的情况就意味着当前链表存在环?此问题我也是纠结半天,看了网上了不少数学证明,基本不理解,不过在不经意间突然想到:两个指针,A,B,从链表LIST的头结点开始依次向后走,且A移动的比B移动的慢。此时,作一个假设:假设当前链表不存在环,那么B的位置永远在A的前面,A是绝对在一万年的时间内也不可能与B相等或超过B,直到链表的结束;但是,如果出现了二者相等的情况(当然A超过B这种情况肯定是正确,但是程序对这种情况是无法做出有效检测的,只有相等才是可以准确检测出来的),意味着什么?在什么条件下A才有可能与B相等?想想学习里的直线与曲线,明白了吧~~,只有链表出现“贪吃蛇”失败的情况,才有这种可能。由此可知,相等必定有环,有环必定会出现相等的情况。继续下一话题:如果确定了单向链表存在环,那如何确定环的起始位置呢?看了别人的总结,心中暗自一凉,不用数学计算,是不会产生这种方法的: 假设二个指针移动了n次之后相遇了,且相遇的位置距离环的起始位置的距离是M,则,这两个指针移动的距离是:慢的:n = L + M + xC    (x是慢指针在相遇前在环内的移动次数)快的:2*n = L + M + yC  (y是快指针在相遇前在环内的移动次数)由此可以计算得出:2L + 2M + 2xC = L + M + yC化简后得到:L = (y-2x)C - M此公式意味着什么?首先说x与y,因为x是慢指针在环内的移动次数,y是快指针在环内的移动次数,试想,慢的能跑过快的吗?所以x肯定比y小。其次,二者肯定相遇,慢的移动一个结点,快的移动两个结点,相当于后者与前者的距离在以1为单位进行缩小,总会有赶上而不是跨越的那一点。最后,假设慢指针在首次进行环的第一个结点的时间,快指针在距离环起始结点距离为M的位置,M的取值范围是【0,C-1】,即M肯定不会大于C,如此一来,慢指针在绕了马上整整一圈的时间,快指针已经马上绕了快两圈,二者的差距已经被缩小了快整整一圈,而二者的差距最大就是C-1,且由于“其次”中论证的二者必须是相遇,所以慢指针在未绕环一圈时,快指针肯定会追上它,所以这里的x=0,y=1。再所以,L=C-M。而L的长度正是环的起始点的坐标,如是而已,代码如下:SLink_ListNode* SingleLink_List::FirstRingNode(){    if(NULL == m_pHead)//是否空链表    {        return NULL;    }    SLink_ListNode* pSlow = m_pHead;    SLink_ListNode* pFast = m_pHead;    while(NULL != pSlow && NULL != pFast && NULL != pFast->m_next)    {        pSlow = pSlow->m_next;        pFast = pFast->m_next->m_next;                if(pSlow == pFast){            break;        }    }    if(NULL == pFast){//是否没有环        return NULL;    }    pFast = m_pHead;    while(pSlow != pFast)    {//从头一次移动一个位置,由于二者此时距离环头结点的距离相等,     //所以检测出相等时,二者此时同时指向了链表环的头结点        pSlow = pSlow->m_next;        pFast = pFast->m_next;    }    return pSlow;}接下来再说下两个单向链接是否有交叉点。仔细想一想,两个单向链表如果存在交叉点,那两个链表会组成一个什么形状?对,只能是Y形。接下来,也正是利用这个Y形,对这种特殊关系的链表做了一些操作,先看如何检测两个单向链表是否存在交叉点:bool ListsHaveCross(SingleLink_List s1,SingleLink_List s2){    if(s1.HasRing() || s2.HasRing())    {        //m*n的一一比较,比较时不要对NODE进行NULL检测,而是直接使用LIST的长度进行循环比较        return false;    }    else    {        SLink_ListNode* pNode1 = s1.m_pHead;        SLink_ListNode* pNode2 = s2.m_pHead;        while(NULL != pNode1){//获取LIST1的最后一个结点            pNode1 = pNode1->m_next;        }        while(NULL != pNode2){//获取LIST2的最后一个结点            pNode2 = pNode2->m_next;        }                return pNode1 == pNode2;    }}上函数中if并没有填写,因为如果有一单向链表存在环,那else中的逻辑便可能不再适用,因为如果事先不知道一个链表的长度,我们还需要一个辅助比较的过程去确定链表的长度,所以我建议在写链表的时间,最好有一个记录其长度的字段,有了长度,一切都简单很多。ELSE中代码的原理,就是利用了两个单向链表一旦交叉,其注定是一个Y形的原理,我们只需要去检测两个链表的最后一个元素是否相同,就可以做出两个链表是否交叉的判断。那再接下来,如何确定两个交叉链表的第一个交叉结点呢?同样是利用其Y形的特性,代码如下: SLink_ListNode* ListFirstCrossNode(SingleLink_List s1,SingleLink_List s2){    if(false == ListsHaveCross(s1,s2))    {        return NULL;    }    else    {        SLink_ListNode* pNode1 = s1.m_pHead;        SLink_ListNode* pNode2 = s2.m_pHead;        int iMoved = s1.m_Length - s2.m_Length;        //将长的链表从头先移动“长度差”个结点        if(iMoved > 0)        {            for(int i = 0;i < iMoved;++i)            {                pNode1 = pNode1->m_next;            }        }        else if(iMoved < 0)        {            for(int i = 0;i < abs(iMoved);++i)            {                pNode2 = pNode2->m_next;            }        }        //在对应位置一一比较        while(pNode1 != pNode2){            pNode1 = pNode1->m_next;            pNode2 = pNode2->m_next;        }        return pNode1;    }}该函数的原理也是相当简单,我们只需要将Y的两个翅膀先做成一样的长度,然后同时向后走并做比较,第一次出现相同的结点,就是首次的交叉结点。如是而已~~好了,意向链表大致就这些考点了,以上代码全是个人书写,肯定会存在问题,还请发现问题的朋友们多多赐教!!本人喜欢玩WOW,附个人PP一张,哈哈

原创粉丝点击