单向链表的常见问题

18 篇文章 0 订阅
单向链表的常见问题
链表中的对象也是按线性顺序排列的,但与数组不同,数组的线性顺序是由数组的下标,再深层次说是由数组的内存结构限定的,而链表中的顺序则是由各对象中的指针决定的。

声明一个单向链表如下:


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一张,哈哈

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值