鏈表
基礎知識
一、*與&符號
a 本質上代表一個存儲單元。CPU通過該存儲單元的地址訪問該存儲單元中的數據。a中可以存放數值(10)和地址(336647)
例如b=10,&b=336647
&:取地址
a=&b:表示a = b存儲單元的地址(336647)
a:代表獲得a中存儲的地址(336647)對應的存儲單元(b)中的數據。也就是訪問a就等於訪問b
int b = 10;
int *a;//定義一個整形指針
a = &b;//給指針賦值,使指針指向b的地址
printf("%d", a);//輸出的是b的地址
printf("\n");//換行符
printf("%d", *a);//*的作用是解引用,取出指針指向地址的內容,獲得b10
return 0;
二、遍歷鏈表而不改變指針位置的辦法
struct ListNode
{
int data; //數據域
ListNode *next; // 指針域
ListNode(int x):data(x), next(NULL){};
};
ListNode leftHead(0);
//這種方式可以不改變leftHead指向
ListNode *leftptr = &leftHead;
leftptr = leftptr->next;
//如果是傳參的形式
ListNode* xx(ListNode *head){
ListNode *p = head;
p = p->next;
// 這種情況時會改變head的指向的,所以之後要變回來
p = head;
}
三、8道經典鏈表面試常考題目
- 例:鏈表逆序
- 例2:鏈表求交點
- 例3:鏈表求環的入口
- 例4:鏈表劃分
- 例5:複雜鏈表的複製
- 例6:2個排序鏈表歸併
例1-a:鏈表逆序(easy)
鏈表反轉的特點,原先的頭的pre=NULL, next!=NULL;反轉後pre!=NULL,next=NULL
原先的尾的pre=!NULL, next=null;反轉後pre=NULL,next!=NULL
pNode每次讓pNode->next斷開原先的指向(下一個),轉向前一個pPre, 之後pPre到pNode的位置, pNode再通過pNext向後走
1.初始:pNext-Null, pNode-1, pPre=Null
通過*pNext = pNode->next, 讓pNext移到第2個位置
pNext-2, pNode-1, pNode->next-Null, pPre=Null
通過pNode->next = pPrev, 使得第1個位置指向pPre
pNext-2, pNode-1, pNode->next-Null, pPre=Null
通過pPrev = pNode, 讓pPrev移到第1個位置
pNext-2, pNode-1, pNode->next-1, pPre-1
通過pNode = pNext, 讓pNode移到第2個位置
pNext-2, pNode-2, pNode->next-1, pPre-1
2.第二次遍歷:pNext-2, pNode-2, pPre-1
通過*pNext = pNode->next, 讓pNext移到第3個位置
pNext-3, pNode-2, pNode->next-1, pPre-1
通過pNode->next = pPrev, 使得第2個位置指向pPre
pNext-3, pNode-2, pNode->next-1, pPre=1
通過pPrev = pNode, 讓pPrev移到第2個位置
pNext-3, pNode-2, pNode->next-2, pPre-2
通過pNode = pNext, 讓pNode移到第2個位置
pNext-3, pNode-3, pNode->next-2, pPre-2
struct ListNode
{
int data; //數據域
ListNode *next; // 指針域
ListNode(int x):data(x), next(NULL){};
};
class Solution
{
public:
ListNode* ReverseList(ListNode *pHead)
{
ListNode *pReversedHead = NULL;
ListNode *pNode = pHead;
ListNode *pPrev = NULL;
while (pNode != NULL)
{
ListNode *pNext = pNode->next; // pNode指向頭結點,後續遍歷列表每個節點
if(pNext == NULL){ //pNode到了最後一個結點,這時pNext指向NULL
pReversedHead = pNode;
}
pNode->next = pPrev; // 斷開原先的指向(下一個),轉向前一個pPre
pPrev = pNode; // pPre到pNode的位置
pNode = pNext; // pNode再通過pNext向後走
}
return pReversedHead;
}
};
例2:鏈表求交點
思路1:使用set存放遍歷的鏈表1,在遍歷列表2時判斷set中是否已存在該結點
時間複雜度O(nlogn),空間複雜度O(n)
ListNode *getIntersectionNode(ListNode *pHead1, ListNode *pHead2){
std::set<ListNode*> node_set;
while (pHead1)
{
node_set.insert(pHead1);
pHead1 = pHead1->next;
}
while (pHead2)
{
if(node_set.find(pHead2) != node_set.end()){ // 未找到則返回node_set.end()
return pHead2;
}
pHead2 = pHead2->next;
}
return NULL;
}
思路2:遍歷兩鏈表長度,長的移動至和段的同一起點,兩指針遍歷一定有相交點。
時間複雜度O(n),空間複雜度O(1)
int getLen(ListNode *pHead){
int len=0;
while (pHead){
len++;
pHead = pHead->next;
}
return len;
}
ListNode *getIntersectionNode2(ListNode *pHead1, ListNode *pHead2){
int len1 = 0, len2 = 0;
len1 = getLen(pHead1);
len2 = getLen(pHead2);
if(len1 > len2){
for(int i = 0; i < len1-len2; i++)
pHead1 = pHead1->next;
while (pHead1 && pHead2){
if(pHead1 == pHead2)
return pHead1;
pHead1 = pHead1->next;
pHead2 = pHead2->next;
}
}
else{
for(int i = 0; i < len1-len2; i++)
pHead2 = pHead2->next;
while (pHead1 && pHead2){
if(pHead1 == pHead2)
return pHead1;
pHead1 = pHead1->next;
pHead2 = pHead2->next;
}
}
return NULL;
}
例3:鏈表求環的入口
思路1:遍歷環,將遍歷到的結點加入set中,每次檢查set中是否有該結點, 有則說明該結點爲入環結點。
ListNode* detectCycle(ListNode *pHead)
{
std::set<ListNode *> node_set;
while (pHead)
{
if(node_set.find(pHead)!=node_set.end()){
return pHead;
}
node_set.insert(pHead);
pHead = pHead->next;
}
return NULL;
}
思路2:快慢指針賽跑思想。快指針走2步,慢指針走1步,相遇說明有環;根據兩個指針路程關係,可得起點。
方程解的結果爲a=c,及在結點3處相遇。
// 思路2:快慢指針相遇
ListNode* detectCycle2(ListNode *pHead){
ListNode *fast = pHead;
ListNode *slow = pHead;
ListNode *meet = NULL;
while (fast) {
slow = slow->next;
fast = fast->next;
if(!fast) // 遇到鏈表尾爲NULL,則無環return
return NULL;
fast = fast->next; // 多走1步
if(fast == slow){
meet = fast;
break;
}
}
if(meet == NULL) // 沒有相遇則無環,meet爲初值null
return NULL;
while (pHead && meet)
{
if(pHead == meet)
return pHead;
pHead = pHead->next;
meet = meet->next;
}
return NULL;
}
例4:鏈表劃分
已知x值和鏈表頭,將鏈表中小於x的放置在x前,大於的放後面,保持相對位置。
思路:使用兩個臨時指針空結點,遍歷鏈表,將小於x的加入lefehead,大於x的加入righthead。
注意的是劃分完之後,lefthead要鏈接到righthead的next結點上,righthead->next要置空。
注意.next和->next的區別
ListNode* partition(ListNode *head, int x){
ListNode leftHead(0);
ListNode rightHead(0);
ListNode *leftptr = &leftHead;
ListNode *rightptr = &rightHead;
while (head)
{
if(head->data < x){
leftptr->next = head;
leftptr = head;
}
else{
rightptr->next = head;
rightptr = head;
}
head = head->next;
}
leftptr->next = rightHead.next;
rightptr->next = NULL;
return leftHead.next;
}
例5:複雜鏈表的複製
在複雜鏈表中,每個節點除了有一個 next指針指向下一個節點,還有一個 random指針指向鏈表中的任意節點或者null。
複製複雜鏈表,即需要將原鏈表所有的鏈接關係都複製。
思路1:使用map和數組。map用於將原鏈表中元素映射到位置,如1,2,3,4,vector用於存入複製的新結點。再次遍歷原鏈表,如果當前元素的random指向某一結點,則通過map獲得指向結點的位置。通過vector獲得數組中該位置對應的結點,將新結點random指向該結點。
ComplexListNode* copyRandomList(ComplexListNode *head){
std::map<ComplexListNode *, int> node_map;
std::vector<ComplexListNode *> node_vec; // 理解爲新鏈表
ComplexListNode *ptr = head;
int i = 0;
while (ptr)
{
node_vec.push_back(new ComplexListNode(ptr->data)); // 複製結點, 存入數組
node_map[ptr] = i; // 爲原鏈表中結點做map, 映射到元素順序
ptr = ptr->next;
i++;
}
node_vec.push_back(0); // 最後一個結點指向這個0元素
ptr = head;
i = 0;
while (ptr)
{
node_vec[i]->next = node_vec[i+1]; // 將數組中元素鏈接
if(ptr->random){ // 如果原鏈表元素有random指向
// 獲得該元素->random指向的結點的位置
int id = node_map[ptr->random];
// 將新的複製結點->random,指向新鏈表中該位置對應的結點
node_vec[i]->random = node_vec[id]; //數組的索引是1,2,3,4正好對應位置,所以可以通過位置索引得到該元素
}
ptr = ptr->next;
i++;
}
return node_vec[0]; // 返回新鏈表頭部
}
思路2:
第一步根據原始鏈表的每個節點N創建對應的N’。把N’鏈接在N的後面。圖中的鏈表經過這一步之後的結構如圖所示。
第二步設置複製出來的節點的random。假設原始鏈表上的N的random指向節點S,那麼其對應複製出來的N’是N的 next指向的節點,同樣S’也是S的 random指向的節點。設置 random之後的鏈表如圖所示。
第三步把這個長鏈表拆分成兩個鏈表:把奇數位置的節點用 next鏈接起來就是原始鏈表,把偶數位置的節點用 next鏈接起來就是複製出來的鏈表。圖中的鏈表拆分之後的兩個鏈表如圖所示。
struct ComplexListNode
{
int data; //數據域
ComplexListNode *next, *random; // 指針域
};
void CloneNodes(ComplexListNode *pHead){
// 第一步:複製結點
ComplexListNode *pNode = pHead;
while (pNode!=NULL)
{
ComplexListNode *pCloned = new ComplexListNode();
pCloned->data = pNode->data;
// 添加新節點,鏈接到原節點之後
pCloned->next = pNode->next;
pNode->next = pCloned;
pNode = pCloned->next;
pCloned->random = NULL;
}
}
void ConnectRandomNodes(ComplexListNode *pHead){
// 第二步:複製random指向
ComplexListNode *pNode = pHead;
while (pNode != NULL)
{
// pNode是第一個結點,pNode->next是複製出來的結點,每次都創建pCloned去指向該結點
ComplexListNode *pCloned = pNode->next;
if(pNode != NULL){
// pNode->random->next是原先結點random指向的結點的複製結點
pCloned->random = pNode->random->next;
}
pNode = pCloned->next;
}
}
ComplexListNode* ReconnectNodes(ComplexListNode *pHead){
// 第三步:將克隆結點從原鏈表中刪除
ComplexListNode *pNode = pHead;
ComplexListNode *pClonedHead = NULL;
ComplexListNode *pClonedNode = NULL;
if(pNode != NULL){
pClonedHead = pClonedNode = pNode->next;
// 將pClonedNode從原鏈表中剔除,但pClonedHead指向pClonedNode
pNode->next = pClonedNode->next;
pNode = pNode->next;
}
while (pNode != NULL)
{
// 移動pClonedNode,指向下一個pClonedNode
pClonedNode->next = pNode->next;
pClonedNode = pClonedNode->next;
// 將pClonedNode從原鏈表中剔除
pNode->next = pClonedNode->next;
pNode = pNode->next;
}
return pClonedHead;
}
例6:2個排序鏈表歸併
鏈表的歸併不難理解,比較兩個鏈表指針指向的數值,進行比對大小,再使用一個新指針鏈接到較小的那個值,原指針和新指針向後移動;當其中一個鏈表遍歷完成時,跳出循環,未完成的鏈表將剩餘的元素鏈接到新鏈表中。
class Solution
{
public:
// 遞歸方法存在問題,會缺少最後一個結點
ListNode* MergeList(ListNode *pHead1, ListNode *pHead2)
{
if(pHead1 == NULL){
return pHead1;
}
else if (pHead2 == NULL){
return pHead2;
}
ListNode *pMergedHead = NULL;
if(pHead1->data < pHead2->data){
pMergedHead = pHead1;
pMergedHead->next = MergeList(pHead1->next, pHead2);
}
else{
pMergedHead = pHead2;
pMergedHead->next = MergeList(pHead1, pHead2->next);
}
return pMergedHead;
}
ListNode* MergeList2(ListNode *pHead1, ListNode *pHead2)
{
ListNode tempHead(0);
ListNode *pre = &tempHead;
while (pHead1 && pHead2)
{
if(pHead1->data < pHead2->data){
pre->next = pHead1;
pHead1 = pHead1->next;
}else
{
pre->next = pHead2;
pHead2 = pHead2->next;
}
pre=pre->next;
}
if(pHead1){ //如果pHead1有剩餘
pre->next = pHead1;
}
if(pHead2){
pre->next = pHead2;
}
return tempHead.next;
}
};