• 链表题目总结(第一篇)


    首先感谢下面几个博客的技术支持:

      http://www.cnblogs.com/newth/archive/2012/05/03/2479903.html 

    另外,这里再贴上一些我的好朋友的关于链表的总结贴子

      http://www.cnblogs.com/carlsama/p/4123709.html

      http://www.cnblogs.com/carlsama/p/4126470.html

      http://www.cnblogs.com/carlsama/p/4126503.html

      http://www.cnblogs.com/carlsama/p/4127201.html

    这里分析的链表默认情况下都是单链表,本篇主要分析一条单链表上的各种问题与解答

    (一)单链表数据结构  

    1 struct ListNode{
    2     int val;
    3     ListNode *next;
    4     ListNode(int val = 0) : val(val), next(NULL){}
    5 };

    (二)单链表相关题目

      (1)倒序打印

      以倒序的方式访问节点,比如打印或者其他操作.

    1 // 方法 i:   递归的打印, 代码非常的简洁
    2 void ReversePrint(ListNode *pHead){
    3     if(!pHead)  return;
    4     ReversePrint(pHead->next);
    5     std::cout<<pHead->val<<std::endl;
    6 }
     1 // 方法 ii:  使用栈,仿照递归的遍历
     2 void ReversePrint(ListNode *pHead){
     3     if(!pHead)  return;
     4     stack<ListNode*>    data;
     5     ListNode *pos = pHead;
     6     while(pos){
     7         data.push(pos);
     8         pos = pos->next;
     9     }   
    10     ListNode *cur;
    11     while(!data.empty()){
    12         cur = data.top();
    13         std::cout<<cur->val<<std::endl;
    14         data.pop();
    15     }   
    16 }

      (2)获取链表倒数第K个节点

     1 // 设置两个指针,两个指针之间距离保持为k,前面的指针从头节点开始, 然后一起往后面走
     2 ListNode * RGetKthNode(ListNode *pHead, unsigned int k){ 
     3     if(k < 1)   return NULL;
     4     ListNode *pre = pHead;
     5     ListNode *last = pHead;
     6     while(k-- > 0 && last){
     7         last = last->next;
     8     }   
     9     if(!last) return NULL;
    10     while(last){
    11         last = last->next;
    12         pre = pre->next;
    13     }   
    14     return pre; 
    15 }

      (3)查找链表的中间节点

     1 // 慢指针以每步一个节点的速度前进,快指针以每步两个节点的速度前进,这样快指针到达末端时候,慢指针就指向中间节点
     2 ListNode* getMidNode(ListNode *pHead){
     3     if(!pHead || !pHead->next)  return pHead;
     4     ListNode *slow = pHead, *fast = pHead;
     5     while(fast && fast->next){
     6         slow = slow->next;
     7         fast = fast->next;
     8         fast = fast->next;
     9     }   
    10     return slow;
    11 }

      (4)链表反转

    //方法i    new一个头节点,然后遍历链表,不断的插入到头节点的next位置
    ListNode *ReverseList(ListNode *pHead){
        if(!pHead || !pHead->next)  return pHead;
        ListNode * newhead = new ListNode();
        ListNode *pos = pHead, *tmp;
        newhead->next = pHead;
        while(pos->next){
            tmp = pos->next;
            pos->next = tmp->next;
            tmp->next = newhead->next;
            newhead->next = tmp;
        }
        return newhead->next;
    }
     1 //方法ii 就地倒转 + 迭代, 反转之前pre->cur, 反转之后 cur->pre
     2 ListNode *ReverseList(ListNode *pHead){
     3     if(!pHead || !pHead->next)  return pHead;
     4     ListNode *pre = pHead, *cur = pHead->next, *next = NULL;
     5     pHead->next = NULL;
     6     while(cur){
     7         next = cur->next;
     8         cur->next = pre;
     9         pre = cur;
    10         cur = next;
    11     }   
    12     return pre;
    13 }
    1 //方法iii 就地倒转 + 递归,     非常具有技巧性
    2 ListNode *ReverseList(ListNode *pHead){
    3     if(!pHead || !pHead->next)  return pHead;
    4     ListNode *left = ReverseList(pHead->next); // 返回left为left段倒转后的首节点
    5     pHead->next->next = pHead; // pHead->next 一开始是left段的首节点,倒转后就是末节点
    6     pHead->next = NULL;
    7     return left;
    8 }

      (5)链表节点的删除

      给定链表的头节点指针和要删除的节点指针,一般来说通常想到的方法就是从头节点开始遍历,找到删除节点的pre节点,借助pre节点删除该节点,这个时间复杂度是O(n),这种方法实在不允许直接数值交换的情况下才只能这样,如果允许节点之间的数值交换或者拷贝,那么有平均时间为O(1)的方法:

     1 // delete Node, with O(1), 平均时间复杂度
     2 void deleteNode(ListNode *pHead, ListNode *pDelete){
     3     if(!pHead || !pDelete)  return;
     4     if(pDelete->next){
     5         // 不是最后一个节点,只需要复制下一个节点的值过来然后删除下一个节点, O(1)
     6         ListNode *tmp = pDelete->next;
     7         pDelete->val = pDelete->next->val;
     8         pDelete->next = pDelete->next->next;
     9         delete tmp;  // 很容易忘记
    10     }else{
    11         // 是最后一个节点,只能从前遍历到倒数第二个节点,O(n)
    12         if(pHead == pDelete){ // 只有一个节点是个特殊情况
    13             delete pHead;
    14             pHead = NULL;
    15         }
    16         ListNode *pos = pHead;
    17         while(pos->next != pDelete){
    18             pos = pos->next;
    19         }
    20         pos->next = NULL;
    21         delete pDelete;   // 这一句很容易忘记
    22     }   
    23 }

      (6)在链表指定节点前插入某个节点

      对于单链表,我们知道插入指定节点后面是很容易的,但是要插入到指定节点前面似乎需要从头遍历链表,如果允许节点之间值拷贝的话那么就可以先插入到后面

    然后两个节点交换一下值便变相实现插入到前面

    1 // 题目要求在指定节点pPos前插入,我们可以先插入到后面,然后交换他们的值即可
    2 void insertNode(ListNode *pPos, ListNode *pInsert){
    3     if(!pPos || !pInsert)   return;
    4     pInsert->next = pPos->next;
    5     pPos->next = pInsert;
    6     int tmp = pPos->val;
    7     pPos->val = pInsert->val;
    8     pInsert->val = tmp;
    9 }

      (7)链表是否有环

      

    // 快慢指针, 慢指针每步一个节点,快指针每步两个节点,如果有环肯定会相遇
    bool isCircleList(ListNode *pHead){
        if(!pHead || !pHead->next) return false;
        ListNode *slow = pHead, *fast = pHead;
        while(fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
            if(slow == fast)    return true;
        }   
        return false;
    }

      这里解释一下:       假设链表里面有环,那么快指针会先进入环内,然后在里面不断的循环,当慢指针进入环内那一刻,可以根据相对运动的观念,可以看成慢指针静止,快指针以每步一节点的速度走,那么很显然必然会相遇

      (8)判断有环链表的环入口点

       先使用一快一慢指针,快指针一步两个节点,慢指针一步一个节点,一是用来探测链表是否有环,二是如果有环那么就已经把快指针送到了环内部。

    这样之后,慢指针重置为指向链表头节点,快指针指向不变,但是移动速度变为每步一个节点,即和慢指针速度一样,然后快慢指针同步移动,那么相遇的时候

    就是环的入口点。

    //
    ListNode *GetFirstCircleNode(ListNode* pHead){
        if(!pHead || !pHead->next)  return pHead;
        ListNode *slow = pHead, *fast = pHead;
        // fast指针先进入环内  第一部分代码
        while(fast && fast->next){
            slow = slow->next;
            fast = fast->next->next;
            if(slow == fast)
                break;
        }   
        if(!fast || !fast->next)    return NULL;
        // slow指针再次从头开始, fast指针减速
        slow = pHead; // 第二部分代码
        while(slow != fast){
            slow = slow->next;
            fast = fast->next;
        }   
        return slow; // 相遇点就是环的入口
    }

       这里面代码似乎很简单,但是要证明其正确性需要费一点周折:

        

      如上图所示: 假设链表的头节点在s处, O为链表入口点, m为第一部分代码中快慢指针的相遇点,

    以O点为坐标原点,以向右为正方向,以相隔链表节点数目为坐标的值,那么m点坐标为m(0, p), p >= 0  

    p 为m点到o点中间的节点个数,同理s点坐标为(0, -d), d >= 0; 另外设环的大小为r, 即有r个节点

      那么在第一部分代码中, 两个节点在m点相遇时候,慢指针移动距离 d + p,  快指针为2d + 2p;

    那么 2d + 2p - d - p =  d + p ,因为快指针先进入圈内,然后再和慢指针相遇时候必然比慢指针多走n圈,

    n >= 1, 所以  

            d + p = n * r      (*   结论1)

      然后在第二部分代码中,快慢指针一起同步的走, 我们可以看到当慢指针走动距离为d的时候,快指针这时候

    和慢指针同速,所以走动距离也是d:

      那么, 此时慢指针的坐标是 -d + d = 0, 即O(0,0)点,而快指针的坐标是 d + p, 即(0,d+p),而根据结论1

    我们可以看到 d+ p = n*r , n >= 1, 所以点(0,d+p)就是O(0,0),此时两个指针相遇,而之前慢指针一直都未进入到环,快

    指针则一直在环内,所以这一次相遇是它们的第一次相遇点,同样也是环的入口点 

      (9)链表的排序

      这里链表的排序主要都是基于归并排序,不过可以分为迭代的归并排序和递归的归并排序

     1 // 递归的方式实现归并排序
     2 ListNode *ListSort(ListNode *pHead){
     3     if(!pHead || !pHead->next)  return pHead;
     4     ListNode *mid = getMidNode(pHead);
     5     ListNode *right = pHead, *left = mid->next;
     6     mid->next = NULL;
     7     right = ListSort(right);
     8     left = ListSort(left);
     9     pHead = MergeSortedList(right, left);
    10     return pHead;
    11 }
     1 //仿照SGI STL里面的List容器的sort函数,实现迭代版的归并排序
     2 ListNode *ListSort(ListNode *pHead){
     3     if(!pHead || !pHead->next)  return pHead;
     4     vector<ListNode*> counter(64, NULL);
     5     ListNode *carry;
     6     ListNode *pos = pHead;
     7     int fill = 0;
     8     while(pos){
     9         carry = new ListNode(pos->val);
    10         pos = pos->next;
    11         int i = 0;
    12         for(i = 0; i < fill && counter[i]; i++){
    13             carry = MergeSortedList(carry, counter[i]); // 合并两个已排序的链表,参见链表总结第二篇
    14             counter[i] = NULL;
    15         }
    16         counter[i] = carry;
    17         if(i == fill) fill++;
    18     }
    19     for(int i = 1; i < fill; i++){
    20         counter[i] = MergeSortedList(counter[i-1], counter[i]);
    21     }
    22     return counter[fill-1];
    23 }

       下面以链表数据4,2,1,5,6,9,7,8,10为例分析这个迭代版的代码过程

      在while循环里面每次都是从原链表里取出一个节点,然后往counter数组里面归并,fill值表明目前counter数组中

    存有数据的最大的那个数组标号+1,比如说,我们首先取出4,此时fill = 0, 直接就把4放在counter[0]链表上,fill变为1,

    然后取出2,就拿2与counter[0]进行merge,得到结果放到count[1],fill变为2,此时counter数组的情况是counter[0]为空,

    counter[1]存放2,4,再加入1时候直接放到counter[0], 再加入5时候,1与5merge,得到1,5再继续向上和couter[1]中

    的2,4,merge得到1,2,4,5,如果此时counter[2]不为空,那么继续merge,这里为空则1,2,4,5存到counter[2],

    于是就这样一步步的向上merge,每次merge链表长度都是加倍

      最终取完所有数据之后,counter数组里面的数据需要最终整合一下,代码中最后一个for循环就是不断的把couter中的内容

    向上merge,最终形成最后的结果 

      这里其实也就是形成了一颗归并树,时间复杂度依然是O(nlgn)

      

  • 相关阅读:
    花儿飘落何处
    别了,攀枝花
    致我心爱的梦中女孩
    解锁华为云AI如何助力无人车飞驰“新姿势”,大赛冠军有话说
    技术实操丨HBase 2.X版本的元数据修复及一种数据迁移方式
    技术实践丨手把手教你使用MQTT方式对接华为IoT平台 华为云开发者社区
    必须收藏:20个开发技巧教你开发高性能计算代码
    原来AI也可以如此简单!教你从0到1开发开源知识问答机器人
    诸多老牌数据仓库厂商当前,Snowflake如何创近12年最大IPO金额
    详解GaussDB(DWS) explain分布式执行计划
  • 原文地址:https://www.cnblogs.com/sosohu/p/4127213.html
Copyright © 2020-2023  润新知