数据结构之链表
链表
本文主要的目的是记录一些常用的链表问题解决办法。
-
单链表翻转
从头到尾遍历原链表,每遍历一个结点,将其摘下放在新链表的最前端。注意链表为空和只有一个结点的情况。时间复杂度为O(n).
ListNode* reverseList(ListNode* pHead) { /*check the param*/ if(nullptr == pHead || nullptr == phead->next) return pHead; ListNode* pReverseHead = nullptr; ListNode* pCur = pHead; while(pCur) { ListNode* pTemp = pCur; pCur = pCur->next; pTemp->next = pReverseHead; pReverseHead = pTemp; } return pReverseHead; }
-
查找单链表中倒数第K个结点(K>0)
最普通的方法是:先统计单链表中结点的个数,然后再找到第(n-k)个结点。注意链表为空,k为0,k为1,k大于链表中节点个数时的情况。时间复杂度为O(n)。代码略。
链表中双指针的一快一慢的使用频率特别高。
方法二:主要思路就是使用两个指针,先让前面的指针走到正向第k个结点,这样前后两个指针的距离差是k-1,之后前后两个指针一起向前走,前面的指针走到最后一个结点时,后面指针所指结点就是倒数第k个结点。ListNode* getKthNode(ListNode* pHead, int k) { /*check the param*/ if(nullptr == pHead || k<=0) return nullptr; ListNode* pAlphaHead = pHead; /*先走*/ ListNode* pBetaHead = pHead; /*后走*/ /*Alpha先走到第K个结点*/ while(k>1 && pAlphaHead != nullptr) { pAlphaHead = pAlphaHead->next; k--; } if(k>1 || pAlphaHead != nullptr) return nullptr; /*pAlphaHead走到底,pBetaHead开始走*/ while(pAlphaHead->next != nullptr) { pBetaHead = pBetaHead->next; pAlphaHead = pAlphaHead->next; } return pBetaHead; }
-
查找单链表的中间结点
同上一题所做的分析,两个指针同时向前走,前面的指针每次走两步,后面的指针每次走一步,前面的指针走到最后一个结点时,后面的指针所指结点就是中间结点,即第(n/2+1)个结点。注意链表为空,链表结点个数为1和2的情况。时间复杂度O(n)
ListNode* getMiddleList(ListNode* pHead) { /*check the param*/ if(nullptr == pHead || nullptr == phead->next) return pHead; ListNode* pAlphaHead = pHead; /*先走*/ ListNode* pBetaHead = pHead; /*后走*/ /*Alpha先走到底,不过一次走两步,Beta一次走一步,这样,Alpha走到结尾的时候,Beta刚好在中间位置*/ while(pAlphaHead->next != nullptr) { pAlphaHead = pAlphaHead->next->next; pBetaHead = pAlphaHead->next; } return pBetaHead; }
-
倒序输出链表
对于这种颠倒顺序的问题,我们应该就会想到栈,后进先出。所以,这一题要么自己使用栈,要么让系统使用栈,也就是递归。注意链表为空的情况。时间复杂度为O(n)
// 从尾到头打印链表,使用栈 void RPrintList(ListNode * pHead) { std::stack<ListNode *> s; ListNode * pNode = pHead; while(pNode != NULL) { s.push(pNode); pNode = pNode->m_pNext; } while(!s.empty()) { pNode = s.top(); printf("%d\t", pNode->m_nKey); s.pop(); } }
使用递归
// 从尾到头打印链表,使用递归 void RPrintList(ListNode * pHead) { if(pHead == NULL) { return; } else { RPrintList(pHead->m_pNext); printf("%d\t", pHead->m_nKey); } }
-
合并有序链表
这个类似归并排序。尤其注意两个链表都为空,和其中一个为空时的情况。只需要O(1)的空间。时间复杂度为O(max(len1, len2))
// 合并两个有序链表 ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2) { if(pHead1 == NULL) return pHead2; if(pHead2 == NULL) return pHead1; ListNode * pHeadMerged = NULL; if(pHead1->m_nKey < pHead2->m_nKey) { pHeadMerged = pHead1; pHeadMerged->m_pNext = NULL; pHead1 = pHead1->m_pNext; } else { pHeadMerged = pHead2; pHeadMerged->m_pNext = NULL; pHead2 = pHead2->m_pNext; } ListNode * pTemp = pHeadMerged; while(pHead1 != NULL && pHead2 != NULL) { if(pHead1->m_nKey < pHead2->m_nKey) { pTemp->m_pNext = pHead1; pHead1 = pHead1->m_pNext; pTemp = pTemp->m_pNext; pTemp->m_pNext = NULL; } else { pTemp->m_pNext = pHead2; pHead2 = pHead2->m_pNext; pTemp = pTemp->m_pNext; pTemp->m_pNext = NULL; } } if(pHead1 != NULL) pTemp->m_pNext = pHead1; else if(pHead2 != NULL) pTemp->m_pNext = pHead2; return pHeadMerged; }
递归解法
ListNode * MergeSortedList(ListNode * pHead1, ListNode * pHead2) { if(pHead1 == NULL) return pHead2; if(pHead2 == NULL) return pHead1; ListNode * pHeadMerged = NULL; if(pHead1->m_nKey < pHead2->m_nKey) { pHeadMerged = pHead1; pHeadMerged->m_pNext = MergeSortedList(pHead1->m_pNext, pHead2); } else { pHeadMerged = pHead2; pHeadMerged->m_pNext = MergeSortedList(pHead1, pHead2->m_pNext); } return pHeadMerged; }
-
判断链表中是否有环
这里也是用到两个指针。如果一个链表中有环,也就是说用一个指针去遍历,是永远走不到头的。因此,我们可以用两个指针去遍历,一个指针一次走两步,一个指针一次走一步,如果有环,两个指针肯定会在环中相遇。时间复杂度为O(n)。
bool HasCircle(ListNode * pHead) { ListNode * pFast = pHead; // 快指针每次前进两步 ListNode * pSlow = pHead; // 慢指针每次前进一步 while(pFast != NULL && pFast->m_pNext != NULL) { pFast = pFast->m_pNext->m_pNext; pSlow = pSlow->m_pNext; if(pSlow == pFast) // 相遇,存在环 return true; } return false; }
-
判断两个单链表是否相交
如果两个链表相交于某一节点,那么在这个相交节点之后的所有节点都是两个链表所共有的。也就是说,如果两个链表相交,那么最后一个节点肯定是共有的。先遍历第一个链表,记住最后一个节点,然后遍历第二个链表,到最后一个节点时和第一个链表的最后一个节点做比较,如果相同,则相交,否则不相交。时间复杂度为O(len1+len2),因为只需要一个额外指针保存最后一个节点地址,空间复杂度为O(1)。
bool IsIntersected(ListNode * pHead1, ListNode * pHead2) { if(pHead1 == NULL || pHead2 == NULL) return false; ListNode * pTail1 = pHead1; while(pTail1->m_pNext != NULL) pTail1 = pTail1->m_pNext; ListNode * pTail2 = pHead2; while(pTail2->m_pNext != NULL) pTail2 = pTail2->m_pNext; return pTail1 == pTail2; }
-
求两个单链表相交的第一个结点
对第一个链表遍历,计算长度len1,同时保存最后一个节点的地址。
对第二个链表遍历,计算长度len2,同时检查最后一个节点是否和第一个链表的最后一个节点相同,若不相同,不相交,结束。
两个链表均从头节点开始,假设len1大于len2,那么将第一个链表先遍历len1-len2个节点,此时两个链表当前节点到第一个相交节点的距离就相等了,然后一起向后遍历,知道两个节点的地址相同。
时间复杂度,O(len1+len2)。ListNode* GetFirstCommonNode(ListNode * pHead1, ListNode * pHead2) { if(pHead1 == NULL || pHead2 == NULL) return NULL; int len1 = 1; ListNode * pTail1 = pHead1; while(pTail1->m_pNext != NULL) { pTail1 = pTail1->m_pNext; len1++; } int len2 = 1; ListNode * pTail2 = pHead2; while(pTail2->m_pNext != NULL) { pTail2 = pTail2->m_pNext; len2++; } if(pTail1 != pTail2) // 不相交直接返回NULL return NULL; ListNode * pNode1 = pHead1; ListNode * pNode2 = pHead2; // 先对齐两个链表的当前结点,使之到尾节点的距离相等 if(len1 > len2) { int k = len1 - len2; while(k--) pNode1 = pNode1->m_pNext; } else { int k = len2 - len1; while(k--) pNode2 = pNode2->m_pNext; } while(pNode1 != pNode2) { pNode1 = pNode1->m_pNext; pNode2 = pNode2->m_pNext; } return pNode1; }
-
已知一个单链表中存在环,求进入环中的第一个结点
首先判断是否存在环,若不存在结束。
在环中的一个节点处断开,这样就形成了两个相交的单链表,求进入环中的第一个节点也就转换成了求两个单链表相交的第一个节点。ListNode* GetFirstNodeInCircle(ListNode * pHead) { if(pHead == NULL || pHead->m_pNext == NULL) return NULL; ListNode * pFast = pHead; ListNode * pSlow = pHead; while(pFast != NULL && pFast->m_pNext != NULL) { pSlow = pSlow->m_pNext; pFast = pFast->m_pNext->m_pNext; if(pSlow == pFast) break; } if(pFast == NULL || pFast->m_pNext == NULL) return NULL; // 将环中的此节点作为假设的尾节点,将它变成两个单链表相交问题 ListNode * pAssumedTail = pSlow; ListNode * pHead1 = pHead; ListNode * pHead2 = pAssumedTail->m_pNext; ListNode * pNode1; int len1 = 1; ListNode * pNode1 = pHead1; while(pNode1 != pAssumedTail) { pNode1 = pNode1->m_pNext; len1++; } int len2 = 1; ListNode * pNode2 = pHead2; while(pNode2 != pAssumedTail) { pNode2 = pNode2->m_pNext; len2++; } pNode1 = pHead1; pNode2 = pHead2; // 先对齐两个链表的当前结点,使之到尾节点的距离相等 if(len1 > len2) { int k = len1 - len2; while(k--) pNode1 = pNode1->m_pNext; } else { int k = len2 - len1; while(k--) pNode2 = pNode2->m_pNext; } while(pNode1 != pNode2) { pNode1 = pNode1->m_pNext; pNode2 = pNode2->m_pNext; } return pNode1; }
-
删除某一个结点,要求时间复杂度O(1)
对于删除节点,我们普通的思路就是让该节点的前一个节点指向该节点的下一个节点,这种情况需要遍历找到该节点的前一个节点,时间复杂度为O(n)。
对于链表,链表中的每个节点结构都是一样的,所以我们可以把该节点的下一个节点的数据复制到该节点,然后删除下一个节点即可。
要注意最后一个节点的情况,这个时候只能用常见的方法来操作,先找到前一个节点,但总体的平均时间复杂度还是O(1)void Delete(ListNode * pHead, ListNode * pToBeDeleted) { if(pToBeDeleted == NULL) return; if(pToBeDeleted->m_pNext != NULL) { pToBeDeleted->m_nKey = pToBeDeleted->m_pNext->m_nKey; // 将下一个节点的数据复制到本节点,然后删除下一个节点 ListNode * temp = pToBeDeleted->m_pNext; pToBeDeleted->m_pNext = pToBeDeleted->m_pNext->m_pNext; delete temp; } else { if(pHead == pToBeDeleted) // 链表中只有一个节点的情况 { pHead = NULL; delete pToBeDeleted; } else // 要删除的是最后一个节点 { ListNode * pNode = pHead; while(pNode->m_pNext != pToBeDeleted) // 找到倒数第二个节点 pNode = pNode->m_pNext; pNode->m_pNext = NULL; delete pToBeDeleted; } } }
-
去除链表中的重复数字
方法一:循环遍历,依次比较,如果相同就删除
Time Complexity: O(n^2)void removeDuplicates(struct Node *start) { struct Node *ptr1, *ptr2, *dup; ptr1 = start; /* Pick elements one by one */ while (ptr1 != NULL && ptr1->next != NULL) { ptr2 = ptr1; /* Compare the picked element with rest of the elements */ while (ptr2->next != NULL) { /* If duplicate then delete it */ if (ptr1->data == ptr2->next->data) { /* sequence of steps is important here */ dup = ptr2->next; ptr2->next = ptr2->next->next; delete(dup); } else /* This is tricky */ ptr2 = ptr2->next; } ptr1 = ptr1->next; } }
方法二:用hash表来维护结构体。遍历链表,加入unorder_set,如果set中已经有了该值,就删除。
We traverse the link list from head to end. For every newly encountered element, we check whether it is in the hash table:
if yes, we remove it;
otherwise we put it in the hash table.
Time Complexity: O(n) on average (assuming that hash table access time is O(1) on average)./* Function to remove duplicates from a unsorted linked list */ void removeDuplicates(struct Node *start) { // Hash to store seen values unordered_set<int> seen; /* Pick elements one by one */ struct Node *curr = start; struct Node *prev = NULL; while (curr != NULL) { // If current value is seen before if (seen.find(curr->data) != seen.end()) { prev->next = curr->next; delete (curr); } else { seen.insert(curr->data); prev = curr; } curr = prev->next; } }
方法三:先排序,再逐个删除。但是改变了链表原有的顺序。
Time Complexity: O(nLogn) -
链表排序
采用分治法,分开排序,然后合并的方法。
合并有序链表在上述问题中有过阐述,时间复杂度为O(max(len1, len2))
分治使用快慢指针的方法ListNode* sortList(ListNode* head) { if (head == NULL || head->next ==NULL) { return head; } ListNode* slow = head; ListNode* fast = head; /*快慢指针的用法,fast到结尾,slow到中间*/ while (fast->next &&fast->next->next) { fast = fast->next->next; slow = slow->next; } ListNode* rightNode = slow->next; slow->next = NULL; return mergeList(sortList(head), sortList(rightNode)); } ListNode * mergeList(ListNode* l1, ListNode* l2) { ListNode* retListNode = new ListNode(0); ListNode* curNode = retListNode; while (l1 && l2) { if (l1->val > l2->val) { curNode->next = l2; l2 = l2->next; } else { curNode->next = l1; l1 = l1->next; } curNode = curNode->next; } if (l1) { curNode->next = l1; } else { curNode->next = l2; } return retListNode->next; }
-
删除某个值
ListNode* removeElements(ListNode* head, int val) { ListNode *pseudo_head = new ListNode(0); pseudo_head->next = head; ListNode *cur = pseudo_head; while(cur){ if(cur->next && cur->next->val == val) cur->next = cur->next->next; else cur = cur->next; } return pseudo_head->next; }