链表的经典技巧及算法

来源:互联网 发布:驾驶远程教育及时软件 编辑:程序博客网 时间:2024/05/19 03:17

1 寻找链表的中间节点:最简单的方法是,先遍历一遍链表,计算出链表的长度,然后计算出中间节点的位置,然后再遍历一遍,边遍历边计算,直到找到中间节点,这个方法略显啰嗦,最坏的情况需要遍历2次链表,代码如下:

ListNode* findMiddle(ListNode* head){    if(!head) return NULL;      int length = 0;    while(head) { head = head->next; length ++ ; }    int middle = (lenght+1)/2;    --middle;    while(middle--) head = head->next;    return head;}

另一个更灵巧的方法是,用两个指针,慢指针每次走一步,快指针每次走两步,当快指针走到链表的末端(NULL)时,慢指针正好指向了中间节点,代码如下:

ListNode* findMiddle(ListNode* head){    if(!head) return NULL;      ListNode* slow = head;    ListNode* fast = head->next;    while(fast && fast->next){        slow = slow->next;        fast = fast->next->next;    }    return slow;}

2 检测链表是否有环:经典的做法也是用快慢指针,如果没有环,快指针一定先到达链表的末端(NULL),如果有环,快、慢指针一定会相遇在环中,代码如下:

bool hasCircle(ListNode* head, ListNode **node){    if(!head) {*node = NULL; return false; }    ListNode* slow = head;    ListNode* fast = head->next;    while(fast && fast->next && fast!=slow){        slow = slow->next;        fast = fast->next->next;    }    if(fast==slow) {*node = fast; return true; }    else {*node = NULL; return false; }}

3 检测环的入口:经典的做法是先检测是否有环,如果有环,则计算出环的长度,然后使用前后指针(不是快慢指针),所谓的前后指针就是一个指针先出发,走了若干步以后,第二个指针也出发,然后两个指针一起走,当前后指针相遇时,它们正好指向了环的入口,代码如下:

ListNode* detectEntry(ListNode* head){    //判断是否有环,无环则返回NULL    ListNode* node = NULL;    if(!hasCircle(head, &node)) return NULL;    //计算环的长度    int circleLen = 1; ListNode *temp = node->next;    while(temp!=node) { temp = temp->next; circleLen ++; }    //使用前后指针,此时fast每次也只移动一步    ListNode *first = head , *second = head;    while(circleLen--) first = first->next; // first指针先出发    //然后两个指针同时向前走,每次走一步    while(first!=second){ first = first->next; second = second->next ; }    return first;}

如果允许使用额外的内存,可以有更简单的做法,即一边遍历,一边将节点放在map中,当某个节点第二次出现在map中时,它就是入口节点,代码如下:

ListNode* detectEntry(ListNode* head){    if(!head) return NULL;    map<ListNode*,bool> hm;    while(!head){        if(hm.count(head)) return head;        else hm[head] = true;        head = head->next;    }    return NULL;}

4 链表翻转:假设原链表为1->2->3,翻转以后的链表应该是1<-2<-3,即节点3变成了头节点,代码如下:

ListNode* reverseList(ListNode *head){    if(!head || !head->next) return head;    ListNode* p = head->next;    head->next = NULL;    while(p){        ListNode* t = p->next;        p->next = head;        head = p;        p = t;    }    return head;}

5 删除链表中的节点,注意这里只给出要删除的那个节点,不给出链表头(假设被删除节点不是尾节点),代码如下:

void removeNode(ListNode *target){    if(!target) return ;    ListNode *t = target->next;    target->val = t->val;    target->next = t->next;  //用target的下一个节点覆盖了target,然后删除下一个节点即可    delete t;}

如果被删除节点是尾节点,上面的代码就无法将target上一个节点的next置为NULL,所以只有给了头节点后,才能遍历到target的上一个节点,并把其next置为NULL。

6 回文链表的检测:所谓回文链表,即链表元素关于中间对称,,如1->2->3->2->1,比较简单的方法是用一个栈,先顺序遍历链表,并把每个节点放入栈中,遍历完成后,栈的出栈顺序正好是原链表的逆,代码如下:

bool isPalindrome(ListNode* head){    if(!head) return true; //空链表姑且认为是回文的    stack<ListNode*> s;    ListNode* p = head;    while(p) { s.push(p); p = p->next ; }    int length = s.size();    length /= 2;    while(length--) {        p = s.top();        s.pop();        if(p->val!=head->val) return false;        head = head->next;    }    return true;    }

上面代码的空间复杂度为O(N),其实还有空间复杂度为O(1)的算法,也很灵巧,运用了之前提到的一些技巧,代码如下:

bool isPalindrome(ListNode* head) {    if (!head || !head->next) return true; //空链表或只有一个元素的链表都认为是回文的    //寻找链表的中间节点    ListNode* slow = head;    ListNode* fast = head->next;    while (fast && fast->next)    {        slow = slow->next;        fast = fast->next->next;    }    //反转后半段链表    ListNode* prev = slow; //prev目前是前半段链表的尾节点,后半段链表的头结点    ListNode* p = slow->next;    slow->next = nullptr;     while (p)    {        ListNode* tmp = p->next;        p->next = prev;        prev = p;        p = tmp;    }  // 反转完成后,prev是后半段链表的头节点,slow是前、后两段链表的公共尾节点    //判断是否为回文链表,从两边向中间靠拢    ListNode* l = head;    ListNode* r = prev;    while (l && r)    {        if (l->val != r->val)            return false;        l = l->next;        r = r->next;    }    return true;}

这个方法的缺点是修改了原链表,但是综合运用了链表的很多技巧,值得收藏。:-D

7 合并有序链表:基本思路跟合并有序数组一样,但是不需要O(N)的空间复杂度了,只需要O(1)的空间复杂度,代码如下:

ListNode* mergeSortedList(ListNode* h1, ListNode* h2){    if(!h1) return h2; //如果链表1为空,直接返回链表2    if(!h2) return h1;    ListNode *head, *p;    if(h1->val<=h2->val) { head = h1; h1 = h1->next ; }    else { head = h2; h2 = h2->next; }    p = head;    while(h1 && h2){        if(h1->val<=h2->val) { p->next = h1; h1 = h1->next; }        else{ p->next = h2; h2 = h2->next; }        p = p->next;    }    if(h1) p->next = h1;    else p->next = h2;    return head;}

其实如果不要求空间复杂度为O(1),可以用递归的思想,代码更简略,如下:

ListNode* mergeSortedList(ListNode* head1, ListNode* head2){        if(head1==NULL) return head2;        if(head2==NULL) return head1;        if(head1->val<=head2->val){            head1->next = merge(head1->next,head2);            return head1;        }        else{            head2->next = merge(head1,head2->next);            return head2;        }}

8 链表排序:如果没有空间复杂度、时间复杂度的要求,那可选的方法太多了,像插入排序、选择排序、冒泡排序,但是如果要求时间复杂度为O(NlogN),而且空间复杂度为O(1)呢?归并排序!!!正好可以用上刚刚写的合并有序链表的代码,代码如下:

ListNode* sortList(ListNode* head){    if(head==NULL || head->next==NULL) return head;    //寻找中间节点        ListNode* p = head;    ListNode* q = p->next;    while(q && q->next){        p = p->next;        q = q->next->next;    }    ListNode* head2 = p->next;    p->next = NULL;    //排序子链表        head = sortList(head);    head2 = sortList(head2);    return mergeSortedList(head,head2);}

9 链表的循环右移:举例如下1->2->3->4->5->NULL,循环右移2位后,变成了4->5->1->2->3->NULL,可以这么考虑,如果链表的长度为N,循环右移K位,那么等效于循环右移 K%N位,K%N是一个小于N的数,然后我们只需要找到循环右移后的头节点即可,上面的例子就是4,然后直接把1->2->3链接到4->5->的后面,代码如下:

ListNode* rotateRight(ListNode* head, int k) {        if(!head || k==0) return head;        int len = 0;        ListNode* p = head;        while(p){ len ++; p = p->next; } //计算链表长度        k = k%len;        if(k==0) return head;        p = head;        int m = len-k-1;        while(m--) p = p->next; // 寻找循环右移K位后的头节点的前一个节点           ListNode* res = p->next; //res就是新的头节点        p->next = NULL;        p = res;        while(p->next!=NULL) p = p->next; //寻找原链表的尾节点        p->next = head;        return res;}

10 以组为单位翻转链表,组的长度用K表示,比如原链表为1->2->3->4->5,当K=2时,翻转的结果为2->1->4->3->5,当K=3时,翻转的结果为3->2->1->4->5,即先翻转K个,再翻转K个,当剩下的节点数小于K时,就不用翻转了。用递归的方法很容易实现,代码如下:

ListNode* reverseKGroup(ListNode* head, int k) {        if(!head || k<=1)  return head;        ListNode* p = head;        int len = 0;        while(p) { len++; p = p->next; } //计算链表长度        if(len<k) return head;        p = head;        ListNode* q = p->next;        int i = k-1;        while(i--) {            ListNode* t = q->next;            q->next = p;            p = q;            q = t;        } //翻转前K个元素        head->next = reverseKGroup(q,k);        return p;}

这个算法的空间复杂度为O(N/K),即正比于递归深度,如果要求空间复杂度为O(1)呢?其实也比较简单,只要循环处理每一段长度为K的链表,处理的时候注意保存上一段链表的尾节点,代码如下:

ListNode* reverseKGroup(ListNode* head, int k) {        if(!head || k<=1)  return head;        ListNode *p = head;        int len = 0;        while(p) { len++; p = p->next; }        int n = len/k;        if(n==0) return head;        ListNode* lastTail = new ListNode(-1); //lastTail是上一段链表的尾节点        lastTail->next = head;        ListNode* newHead = NULL; //如果n=1,只执行一次翻转即可,故用newHead保存翻转后的头结点        ListNode *q = NULL;        if(n--){ //执行第一次翻转            p = head;            q = p->next;            int i = k-1;            while(i--) {                ListNode *t = q->next;                q->next = p;                p = q;                q = t;            }            lastTail->next = p; //p是翻转后的头结点,用lastTail->next指向它            newHead = p; //newHead是程序最终要返回的头结点            lastTail = head; //原来头节点变成了尾节点            lastTail->next = NULL; //把尾节点的next置为NULL,此步必须有,否则无法打印链表            head = q; //q是下一段链表的头节点        }        while(n--){ //循环处理下一段链表            p = head;            q = p->next;            int i = k-1;            while(i--) {                ListNode *t = q->next;                q->next = p;                p = q;                q = t;            }            lastTail->next = p;            lastTail = head;            lastTail->next = NULL;            head = q;        }        if(q) lastTail->next = q; //把最终不需要翻转的那段链表(长度小于K)接上        return newHead;}

11 翻转链表的相邻节点,比如原链表为1->2->3->4,翻转后为2->1->4->3,这个其实就是上一道题的特例,即K=2,也要求空间复杂度为O(1),不过还是递归简洁啊,这里只给出递归的代码:

ListNode* swapPairs(ListNode* head) {        if(head==NULL || head->next==NULL) return head;        ListNode* p = head;        ListNode* q = p->next;        p->next = swapPairs(q->next);        q->next = p;        return q;        }

12 删除链表的倒数第N个节点,要求只遍历一次,还记得检测环的入口吗?是的,用前后指针,前后指针需要相隔(N+1)步,这样当前指针为NULL的时候,后指针正好指向倒数第(N+1)个节点,然后直接删除倒数第N个节点即可,代码如下:

ListNode* removeNthFromEnd(ListNode* head, int n) {        if(!head || n==0) return head;        ListNode *p = head;        ListNode *q = p;        while(n>=0 && q) {q = q->next; n--; }        while(q){ p = p->next; q = q->next; }        if(n<0) { //要删除的不是头节点            ListNode *t = p->next;             p->next = t->next;            delete t;        }        else{ //要删除的是头结点            ListNode *t = p->next;            delete p;            head = t;        }        return head;     }

13 删除有序链表中的重复元素,如原链表为1->2->3->3->4->4->5,删除后的链表为1->2->5,这道题的关键是如果某节点有重复,务必将其全部删掉,所以要对有重复的节点做个标记,代码如下:

ListNode* deleteDuplicates(ListNode* head) {        if(!head || !head->next) return head;        ListNode* dumy = new ListNode(0); //哑变量的引入使得头结点不再具有特殊性,从而简化处理流程        dumy->next = head;        bool flag = true; //指示当前节点是否是第一次遇到        ListNode* p = dumy; // p 为当前节点的上一个节点        ListNode* q = head; // q 为当前节点        ListNode* t = NULL; // 临时变量        while(q) {            if(q->next && q->next->val==q->val){ //如果当前节点与它的下一个节点相同,则删掉当前节点                t = q->next;                delete q;                p->next = t;                q = t;                flag = false;            }            else{                if(flag){ //如果当前节点在以前没有遇到过,且其不等于它的下一个节点。                    p = q;                    q = q->next;                    flag = true;                }                else{ //如果当前节点在以前遇到过,则删除当前节点                    t = q->next;                    delete q;                    p->next = t;                    q = t;                    flag = true;                }            }        }          return dumy->next;}

技巧:哑变量的引入使得头结点不再具有特殊性,从而简化处理流程。

14 像快排那样将链表分成前后两个部分,比如原链表为1->4->3->2->5->2,给出数字3,那么链表中比3小的放在前面,比3大(或等于3)的放在链表的后面,处理后的链表应该是这样的1->2->2->4->3->5,注意,4和5都大于3,那么处理后的链表中4仍然应该在5的前面,代码如下:

ListNode* partition(ListNode* head, int x) {        if(!head || !head->next) return head;        ListNode* dumyHead = new ListNode(0);        dumyHead->next = head;        ListNode* dumyTail = new ListNode(0);        dumyTail->next = NULL;        ListNode* p = dumyHead; //当前节点的上一个节点        ListNode* q = p->next; //当前节点        ListNode* t = NULL; //临时变量        ListNode* m = dumyTail; // dumyTail可以看做是后半部分链表的头(即dumyTail后面的节点都大于等于x)        while(q){            if(q->val<x) { p = p->next; q = q->next; }            else {                t = q->next;                m->next = q;  //把大于等于x的节点接到dumyTail的后面                q->next = NULL;                m = q;                p->next = t;                q = t;            }        }        p->next = dumyTail->next;        return dumyHead->next;}

15 合并K个有序链表,这个咋一听很简单,先合并第1个、第2个,然后将合并后的结果与第3个合并,然后将合并的结果与第4个合并……,假设每个链表的长度为N,那么时间复杂度为O(NK2),N为总的节点数,因为要合并K次。其实有更优的时间复杂度,我们可以先两两合并,即第1个与第2个合并,第3个与第4个合并,即执行K/2次合并,这作为第一轮合并,时间复杂度为O(KN),接下来就只需要合并K/2个有序链表了,即进行第二轮合并,这样总共需要进行logK轮合并,每一轮的时间复杂度为O(KN),所以总的时间复杂度为O(NKlogK),代码如下(合并两个有序链表的代码已经在前面给出):

ListNode* mergeKLists(vector<ListNode*>& lists) {        if(lists.empty())  return NULL;        if(lists.size() == 1)  return lists[0];        vector<ListNode*> vec(lists);        return helper(lists, vec);        }ListNode* helper(vector<ListNode*>& lists, vector<ListNode*>& vec){        lists = vec;        vec.clear();        int size = lists.size();        int i = 0;        for(i=0; i<size-1; i+=2)            vec.push_back(mergeTwoLists(lists[i],lists[i+1]));        if(i==size-1) vec.push_back(lists[i]);        if(vec.size() == 1)  return vec[0];        else  return helper(lists, vec);}

16 寻找两个链表的第一个公共节点,即两个链表从某个节点开始合并了,我们要找出这个节点,经典的方法是先计算两个链表的长度:L1,L2,假设L1>L2,那么公共节点一定不在链表1的前(L1-L2)个节点中,这样我们就可以让链表1的头节点指向第(L1-L2+1)个节点,然后同时推进两个链表的头节点,边推进边比较,直到遇到同一个节点,代码如下:

ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {        if(!headA || !headB) return NULL;        ListNode *p = headA;        int lenA = 0;        while(p){ lenA++; p = p->next; }        p =headB;        int lenB = 0;        while(p){ lenB++; p = p->next; }        if(lenA>lenB){            int count = lenA-lenB;            while(count--) headA = headA->next;        }        if(lenA<lenB){            int count = lenB-lenA;            while(count--) headB = headB->next;        }        while(headA){            if(headA==headB) return headA;            headA = headA->next;            headB = headB->next;        }        return NULL; }

17 链表的插入排序,这个就很直白了,代码如下:

ListNode* insertionSortList(ListNode* head) {        if(!head || !head->next) return head; //空链表或只有一个节点的链表不需要排序        ListNode* dumy = new ListNode(0);        dumy->next = head;        ListNode* p = head; //已经排序好的链表的尾节点        ListNode* q = p->next; //当前要处理的        while(q){            if(p->val<=q->val) { //如果当前节点大于已经排好序的链表的尾节点,那么不需要做插入                p = p->next;                q = q->next;                continue;            }            //插入,这时候dumy派上用场了            ListNode* m = dumy;             ListNode* n = m->next;            while(n->val<=q->val) { m = m->next; n = n->next; }            m->next = q;            p->next = q->next;            q->next = n;            q = p->next;        }          return dumy->next;}

18 把有序链表转换成一个尽量平衡的二叉树,其实所谓的尽量平衡,就是把链表的中间节点作为根节点,根节点左边的链表是树的左子树,根节点右边的链表是树的右子树,然后问题就转换成了原问题的子问题,即用前半段链表建立一个尽量平衡的左子树,用后半段链表建立一个尽量平衡的右子树,代码如下:

TreeNode* sortedListToBST(ListNode* head) {        if(!head) return NULL;        if(!head->next) return new TreeNode(head->val);        ListNode* slow = head;        ListNode* fast = head->next->next; //注意,这里fast比slow先走了两步,这是为了找到中间节点的上一个节点        while(fast && fast->next){            slow = slow->next;            fast = fast->next->next;        }// 这里稍微与之前的代码有些不同,我们最终会找到中间节点的上一个节点        TreeNode *root = new TreeNode(slow->next->val);        ListNode *head2 = slow->next->next;        slow->next = NULL;        root->left = sortedListToBST(head);        root->right = sortedListToBST(head2);        return root;}

19 在链表上模拟加法运算,比如(2 -> 4 -> 3) + (5 -> 6 -> 5)=7 -> 0 -> 9,链表的头节点为个位,然后是十位……其实模拟的关键就是处理进位,代码如下(略长,但比较直白):

ListNode* addTwoNumbers(ListNode* a1, ListNode* a2) {        if(!a1) return a2;        if(!a2) return a1;        int jinwei = 0;        ListNode *head = new ListNode(a1->val + a2->val);        if(head->val>=10){            jinwei = 1;            head->val = head->val % 10;        }        a1 = a1->next;        a2 = a2->next;        ListNode *p = head;        while(a1 && a2){            int sum = a1->val+a2->val+jinwei;            if(sum>=10){                jinwei = 1;                p->next = new ListNode(sum%10);                p = p->next;            }            else{                jinwei = 0;                p->next = new ListNode(sum);                p = p->next;            }            a1 = a1->next;            a2 = a2->next;        }        if(a1==NULL && a2==NULL){  // a1与a2等长,只处理最后的进位即可            if(jinwei==1) p->next = new ListNode(1);            return head;        }        if(a1!=NULL){ //a1更长            while(a1!=NULL || jinwei==1){                if(a1==NULL){                    p->next = new ListNode(1);                    return head;                }                int sum = a1->val + jinwei;                if(sum>=10){                    jinwei = 1;                    p->next = new ListNode(sum%10);                    p = p->next;                }                else{                    jinwei = 0;                    p->next = new ListNode(sum);                    p = p->next;                }                a1 = a1->next;            }               }        if(a2!=NULL){ // a2更长            while(a2!=NULL || jinwei==1){                if(a2==NULL){                    p->next = new ListNode(1);                    return head;                }                int sum = a2->val + jinwei;                if(sum>=10){                    jinwei = 1;                    p->next = new ListNode(sum%10);                    p = p->next;                }                else{                    jinwei = 0;                    p->next = new ListNode(sum);                    p = p->next;                }                a2 = a2->next;            }               }        return head;}
1 0
原创粉丝点击