劍指LeetCode面試算法:鏈表

鏈表

基礎知識

一、*與&符號

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道經典鏈表面試常考題目

  1. 例:鏈表逆序
  2. 例2:鏈表求交點
  3. 例3:鏈表求環的入口
  4. 例4:鏈表劃分
  5. 例5:複雜鏈表的複製
  6. 例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;
    }
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章