單鏈表常考算法題彙總

目錄

1、從尾到頭打印單鏈表

2、單鏈表實現約瑟夫環---時間複雜度=O(1)

3、逆置/反轉單鏈表---時間複雜度=O(n)

4、單鏈表排序(冒泡排序/快速排序)

(1)選擇排序---時復=O(n^2),空復=O(1)

(2)冒泡排序---時復=O(n^2),空復=O(1)

(3)快速排序(親測有效,成功運行)---時復=O(nlogn),空復=O(1)

5、k個節點爲一組進行翻轉

(1)反轉單鏈表

(2)從頭部開始組起的部分反轉

(3)從尾部開始組起的部分反轉

6、(查找/刪除)鏈表中間節點(擴展:返回鏈表a/b節點)

(1)刪除中間節點

(2)刪除a/b節點

7、(查找/刪除)鏈表的倒數第k個節點---時間複雜度=O(N),空間複雜度=O(1)

(1)查找單鏈表的倒數第k個節點

(2)刪除單鏈表的倒數第k個節點

(3)刪除雙鏈表的倒數第k個節點

8、判斷一個單鏈表是否有環,有環則求環大小和環入口節點

(1)判斷是否有環:頭節點開始,1快1慢,快2步,慢1步,快追上慢則有環

(2)環大小:(1)基礎上,相遇節點開始,1個指針1步步走,直至再次回到該節點

(3)環入口節點:2種法1:(2)基礎上,頭節點開始,1指針先走n步,2指針和1指針同時一步步走,直至相遇

法2:(1)基礎上,快指針回到頭節點,快慢指針同時一步步走,直至相遇

9、兩鏈表相交

(1)判斷1個鏈表是否有環,有環則返回第一個進入環的節點,無返回null

(2)判斷2個無環鏈表是否相交,相交則返回第一個相交節點,不相交則返回null

(3)判斷2個有環鏈表是否相交,相交則返回第一個相交節點,不相交則返回null

(4)判斷兩個(有環/無環)單鏈表是否相交?相交返回第一個相交點,不相交返回null

10、求兩個已排序單鏈表中相同的數據

11、合併兩個有序鏈表,合併後依然有序---O(n)


1、從尾到頭打印單鏈表

Node * findMiddleNode(Node *head)
{
    if(null == head || null == head->next) return head;
    Node *p1 = *p2 = head;
    while(null != p2->next && null != p2->next->next )
    {
        p2 = p2->next->next;
        p1 = p1->next;
    }
    return p1;
}

2、單鏈表實現約瑟夫環---時間複雜度=O(1)

暴力數數的方式時間複雜度=O(n*m),從1開始報數,報到m就殺死,下一個數從1開始編號,繼續報數。
n個數最後只活了1個,n-1個節點被殺死,每一個被殺死的數都報數到m,所以計算了(n-1)*m次,約等於n*m

優化方法:
(1)取餘函數(剃刀函數,見圖a):y=x%i

(2)報數x與編號y的關係(見圖b):y=(x-1)%i+1
假設環中有i個節點,報數不斷增大,但環中節點個數有限。當環中有3個節點,報數到5就死(環大小<報數),當報到4時,編號回到環起點1。相當於取餘函數,右移1個,再上移一個,即:y=(x-1)%i+1

報數 1 2 ... i i+1 i+2
編號 1 2 ... i 1 2

(3)新編號x與舊編號y的關係(見圖c):y=(x-1+s)%i+1
最後1個活下來的節點的編號=1,那麼可以反推2個節點活下來時該節點編號,3個活下來....n個活下來...
假如,環節點數=8,報到3就死。相當於(2)左移s個,即:y=(x-1+s)%i+1
舊編號:1,2,3,4,5,6,7,8
新編號:6,7,-,1,2,3,4,5
相當於:

舊編號(i個) 1 2 ... s-1 s s+1 ... i
新編號(i-1個) ... ... i-2 i-1 - 1 2 ...

(4)舊編號=(新編號+(m-1))%i+1
A: 編號=(報數-1)%i+1
B: 舊編號=(新編號+s-1)%i+1
C: s=(m-1)%i+1
其中,s是被幹掉的編號,因爲報數到m就死,所以得出的C式。
將C式代入B式,舊編號=(新編號+(m-1)%i)%i+1,中間的%i可以化解掉(具體就不關心啦)
最終,舊編號=(新編號+(m-1))%i+1

public static Node josephusKill2(Node head, int m) {
		if (head == null || head.next == head || m < 1) {
			return head;
		}
		Node cur = head.next;
		int tmp = 1;//tmp爲環的長度
		while (cur != head) {
			tmp++;
			cur = cur.next;
		}
		tmp = getLive(tmp, m);
		while (--tmp != 0) {//從1走到活着的tmp,走tmp-1步驟
			head = head.next;
		}
		head.next = head;//自己指向自己的環形單鏈表
		return head;
	}
    //獲取舊編號,i=舊環的大小,m是從1報數到m就死,getLive(i-1,m)是新編號
	public static int getLive(int i, int m) {
		if (i == 1) {
			return 1;
		}
		return (getLive(i - 1, m) + m - 1) % i + 1;
	}

 

3、逆置/反轉單鏈表---時間複雜度=O(n)

詳見4(1)

4、單鏈表排序(冒泡排序/快速排序)

(1)選擇排序---時復=O(n^2),空復=O(1)


(2)冒泡排序---時復=O(n^2),空復=O(1)

選擇排序

public static Node selectionSort(Node head) {
    Node tail = null; // sorted part tail
    Node cur = head; // unsorted part head
    Node smallPre = null; // previous node of the smallest node
    Node small = null; // smallest node
    while (cur != null) {
        small = cur;
        smallPre = getSmallestPreNode(cur);
        if (smallPre != null) {
            small = smallPre.next;
            smallPre.next = small.next;
        }
        cur = cur == small ? cur.next : cur;
        if (tail == null) {
            head = small;
        } else {
            tail.next = small;
        }
        tail = small;
    }
    return head;
}

冒泡排序

void BubbleSord(pList plist) {
    pNode pCur = NULL;
    pNode pPre = NULL;
    pNode pTail = NULL;//pTail的指向是這個算法的關鍵
    if (plist == NULL || plist->next == NULL) {//排除空和一個結點的情況
        return;
    }
    while (plist != pTail) {//趟數
        int IsChange = 0;//排除排序時已經是有序的,則不需要再排序
        pPre = plist;
        pCur = pPre->next;
        while (pCur != pTail) {//次數
            if (pPre->data > pCur->data) {//升序
                int tmp = pPre->data;
                pCur->data = pPre->data;
                pPre->data = tmp;
                IsChange = 1;
            }
            pPre = pPre->next;
            pCur = pCur->next;
        }
        if (!IsChange) {//如果有序了,就不排了
            return;
        }
        pTail = pPre;
    }
}

(3)快速排序(親測有效,成功運行)---時復=O(nlogn),空復=O(1)

思路:需要兩個指針p和q,這兩個指針均往next方向移動,移動的過程中保持p之前的key都小於選定的key,p和q之間的key都大於選定的key,那麼當q走到末尾的時候便完成了一次支點的尋找。

注意:這裏的GetPartition方法裏判斷條件:if(j->data < key)時,先i++,再swap(i->data, j->data)。因爲i前(包含i)的數據都比key小;i後j前的數據都比key大。如果先swap再i++的話,很有可能把i的數據(<=key)換到j位置了。

完整代碼如下:

#include <iostream>
using namespace std;

struct ListNode {
	int data;
	ListNode * next;
	ListNode(int _data = -1) : data(_data), next(NULL) {}
};//最後的分號不能丟

void swap(int & p, int & q) {//記得用&
	int tmp = p;
	p = q;
	q = tmp;
}

ListNode * createList(int a[], int n) {//用數組值創建單鏈表
    ListNode * pHead,* p;
    pHead = p = NULL;
    for (int i = 0; i < n; i++) {
        ListNode * tmp = new ListNode(a[i]);
        if (i == 0) pHead = tmp;
        else p->next = tmp;
        p = tmp;
    }
    return pHead;
}

ListNode * GetPartion(ListNode * pBegin, ListNode * pEnd)
{
	int key = pBegin->data;
	ListNode * i = pBegin;
	ListNode * j = i->next;
	while (j != pEnd)
	{
		if (j->data < key) {
		    i = i->next;
		    swap(i->data, j->data);
		}
		j = j->next;
	}
	swap(i->data, pBegin->data);
	return i;
}

void QuickSort(ListNode * pBeign, ListNode * pEnd)
{
	if (pBeign != pEnd) {
		ListNode * partion = GetPartion(pBeign, pEnd);
		QuickSort(pBeign, partion);
		QuickSort(partion->next, pEnd);
	}
}

int main() {
	int datalist[8] = { 4,2,5,3,7,9,0,1 };
	ListNode * pHead = createList(datalist, 8);
	QuickSort(pHead, NULL);
	ListNode * tmp = pHead;
	while (tmp != NULL) {
		cout << tmp->data << ' ';
		tmp = tmp->next;
	}
	return 0;
}

5、k個節點爲一組進行翻轉

(1)反轉單鏈表

                               A

                                 B

                                        C

假設方法 reverse() 的功能是將單鏈表進行逆轉。採用遞歸的方法時,我們可以不斷着對子鏈表進行遞歸。我們對子鏈表 2->3->4 進行遞歸,即Node newList = reverse(head.next)。遞歸之後的結果如圖B,逆轉之後子鏈表 2->3->變爲了 4->3->2。注意,剛纔假設 reverse() 的功能就是對鏈表進行逆轉。不過此時結點 1 仍然是指向結點 2 的。這個時候,我們再把結點1 和 2逆轉一下,然後 1 的下一個結點指向 null 就可以了。如圖C。遞歸的結束條件就是:當子鏈表只有一個結點,或者爲 null 時,遞歸結束。

//逆序單鏈表
private static ListNode reverse(ListNode head) {
    if(head == null || head.next == null)
        return head;
    ListNode result = reverse(head.next);
    head.next.next = head;
    head.next = null;
    return result;
}

(2)從頭部開始組起的部分反轉

A

B

C

D

E

F

對於上面的這個單鏈表(見圖A),其中 K = 3。我們把前K個節點與後面的節點分割出來(見圖B和C),temp指向的剩餘的鏈表,可以說是原問題的一個子問題。我們可以調用reverseKNode()方法將temp指向的鏈表每K個節點之間進行逆序。再調用reverse()方法把head指向的那3個節點進行逆序(見圖D和E),接着,我們只需要把這兩部分給連接起來就可以了(見圖F)。

//k個爲一組逆序
public ListNode reverseKGroup(ListNode head, int k) {
    ListNode temp = head;
    for (int i = 1; i < k && temp != null; i++) {
        temp = temp.next;
    }
    //判斷節點的數量是否能夠湊成一組
    if(temp == null)
        return head;

    ListNode t2 = temp.next;
    temp.next = null;
    //把當前的組進行逆序
    ListNode newHead = reverse(head);
    //把之後的節點進行分組逆序
    ListNode newTemp = reverseKGroup(t2, k);
    // 把兩部分連接起來
    head.next = newTemp;
    
    return newHead;
}

(3)從尾部開始組起的部分反轉

A

B

C

D

對於K = 3鏈表(見圖A),我們把它從尾部開始組起,每 K 個節點爲一組進行逆序。先進行逆序(見圖B),逆序之後就可以把問題轉化爲從頭部開始組起,每 K 個節點爲一組進行逆序(見圖C)。接再把結果逆序一次(見圖D)。

public ListNode solve(ListNode head, int k) {
    head = reverse(head);//調用逆序函數
    // 調用每k個爲一組的逆序函數(從頭部開始組起)
    head = reverseKGroup(head, k);
    head = reverse(head);//再次逆序
    return head;
}

6、(查找/刪除)鏈表中間節點(擴展:返回鏈表a/b節點)

                    圖A

                              圖B

(1)刪除中間節點

len=0/1---不刪除
len=2-----刪第1個
len=3/4---刪第2個
len=5/6---刪第3個
用2個指針,pre和cur,初始分別爲第1個和第3個。pre每次走1個,cur每次走2個,走不動了,刪除pre+1。詳見圖A

(2)刪除a/b節點

r=(a*n)/b,向上取整
a<1 或 a>b,直接返回。
r=1,刪除頭節點=返回head->next
r>1,如r=5,走r-2步。詳見圖B

(1)刪除中間節點

public static Node removeMidNode(Node head) {
        if (head == null || head.next == null) {
            return head;
        }
        if (head.next.next == null) {
            return head.next;
        }
        Node pre = head;
        Node cur = head.next.next;
        while (cur.next != null && cur.next.next != null) {
            pre = pre.next;
            cur = cur.next.next;
        }
        pre.next = pre.next.next;
        return head;
    }

(2)刪除a/b節點

    public static Node removeByRatio(Node head, int a, int b) {
        if (a < 1 || a > b) {
            return head;
        }
        int n = 0;
        Node cur = head;
        while (cur != null) {
            n++;
            cur = cur.next;
        }
        n = (int) Math.ceil(((double) (a * n)) / (double) b);
        if (n == 1) {
            head = head.next;
        }
        if (n > 1) {
            cur = head;
            while (--n != 1) {
                cur = cur.next;
            }
            cur.next = cur.next.next;
        }
        return head;
    }

7、(查找/刪除)鏈表的倒數第k個節點---時間複雜度=O(N),空間複雜度=O(1)

(1)查找單鏈表的倒數第k個節點

思路:如果鏈表爲空,或者k<鏈表長度,直接返回null。
設置兩個指針A和B,相差k-1步,A先走k-1步驟,然後A和B一起走,當A走到最後,B即爲所求。

(2)刪除單鏈表的倒數第k個節點

思路:鏈表爲空或k<1,直接返回。其他:從頭走到尾,每走一步k-1(頭部開始k-1),如果走到尾k=0,說明倒數第k個節點即爲頭節點,如果k<0,說明倒數第k個節點在鏈表中間,需要找到該節點的前一個節點:再次從頭開始,每走一步k+1(頭部開始k+1),直到k=0走到的節點即爲倒數第k個節點的前一個節點

A -> B -> C -> D,k=2 A B C D
step1: 1 0 -1 -2
step2: -1 0    

(3)刪除雙鏈表的倒數第k個節點

與(2)相同,只不過需要注意指針

(1)查找單鏈表的倒數第k個節點

ListNode * findKthFromTail(ListNode * head, int k) {
    if (head == NULL || k < 1) {
        return NULL;
    }
    ListNode * pQuick = head, 
    ListNode * pSlow = NULL;
    for (int i = 0; i< k-1; i++) {
        if (pQuick->next == NULL) {//k>鏈表長度
            return NULL;
        }
        pQuick = pQuick->next;
    }
    pSlow = head;
    while (pQuick->next != NULL) {
        pQuick = pQuick->next;
        pSlow = pSlow->next;
    }
    return pSlow;
}

(2)刪除單鏈表的倒數第k個節點

ListNode * removeLastKthNode(ListNode * head, int lastKth) {
    if (head == null || lastKth < 1) {
        return head;
    }
    ListNode * cur = head;
    while (cur != null) {
        lastKth--;
        cur = cur.next;
    }
    if (lastKth == 0) {
        head = head.next;
    }
    if (lastKth < 0) {
        cur = head;
        while (++lastKth != 0) {
            cur = cur.next;
        }
        cur.next = cur.next.next;
    }
    return head;
}

(3)刪除雙鏈表的倒數第k個節點

ListNode * removeLastKthNode(ListNode * head, int lastKth) {
    if (head == null || lastKth < 1) {
        return head;
    }
    ListNode * cur = head;
    while (cur != null) {
        lastKth--;
        cur = cur.next;
    }
    if (lastKth == 0) {
        head = head.next;
        head.last = null;
    }
    if (lastKth < 0) {
        cur = head;
        while (++lastKth != 0) {
            cur = cur.next;
        }
        ListNode * newNext = cur.next.next;
        cur.next = newNext;
        if (newNext != null) {
            newNext.last = cur;
        }
    }
    return head;
}

 

8、判斷一個單鏈表是否有環,有環則求環大小和環入口節點

(1)判斷是否有環:頭節點開始,1快1慢,快2步,慢1步,快追上慢則有環

ListNode* MeetingNode(ListNode* pHead)
{
    if(pHead == nullptr)
        return nullptr;

    ListNode* pSlow = pHead->m_pNext;
    if(pSlow == nullptr)
        return nullptr;

    ListNode* pFast = pSlow->m_pNext;
    while(pFast != nullptr && pSlow != nullptr)
    {
        if(pFast == pSlow)
            return pFast;

        pSlow = pSlow->m_pNext;

        pFast = pFast->m_pNext;
        if(pFast != nullptr)
            pFast = pFast->m_pNext;
    }

    return nullptr;
}


(2)環大小:(1)基礎上,相遇節點開始,1個指針1步步走,直至再次回到該節點

int LenOfLoop(ListNode* pHead)
{
	ListNode* meetingNode = MeetingNode(pHead);
    if(meetingNode == nullptr) return nullptr;
    int nodesInLoop = 1;//環結點數
    ListNode* pNode1 = meetingNode;
    while(pNode1->m_pNext != meetingNode)
    {
        pNode1 = pNode1->m_pNext;
        ++nodesInLoop;
    }
    return nodesInLoop;
}

(3)環入口節點:2種
法1:(2)基礎上,頭節點開始,1指針先走n步,2指針和1指針同時一步步走,直至相遇

ListNode* getLoopNode(ListNode* pHead)
{
	int nodesInLoop = LenOfLoop(pHead);
    // 先移動pNode1,次數爲環中結點的數目
    pNode1 = pHead;
    for(int i = 0; i < nodesInLoop; ++i)
        pNode1 = pNode1->m_pNext;

    // 再同時移動pNode1和pNode2
    ListNode* pNode2 = pHead;
    while(pNode1 != pNode2)
    {
        pNode1 = pNode1->m_pNext;
        pNode2 = pNode2->m_pNext;
    }

    return pNode1;
}

法2:(1)基礎上,快指針回到頭節點,快慢指針同時一步步走,直至相遇

ListNode* getLoopNode(ListNode* pHead)
{
	ListNode* meetingNode = MeetingNode(pHead);
	ListNode* p1,p2;
	p1 = head;
	p2 = meetingNode;
	while(p1 != p2) {
		p1 = p1->next;
		p2 = p2->next;
	}
    return p1;
}


9、兩鏈表相交

(1)判斷1個鏈表是否有環,有環則返回第一個進入環的節點,無返回null

詳見10(1)(3)

public static Node getLoopNode(Node head) {
	if (head == null || head.next == null || head.next.next == null) {
		return null;
	}
	Node n1 = head.next; // n1 -> slow
	Node n2 = head.next.next; // n2 -> fast
	while (n1 != n2) {
		if (n2.next == null || n2.next.next == null) {
			return null;
		}
		n2 = n2.next.next;
		n1 = n1.next;
	}
	n2 = head; // n2 -> walk again from head
	while (n1 != n2) {
		n1 = n1.next;
		n2 = n2.next;
	}
	return n1;
}

(2)判斷2個無環鏈表是否相交,相交則返回第一個相交節點,不相交則返回null

分別計算兩個鏈表的長度len1,len2,還有鏈表尾節點end1,end2。若end1!=end2,說明不相交直接返回null。
若end1==end2,說明相交。求第一個交點:長鏈表先走abs(len1-len2)步,然後兩個同時一步步走,直至相遇。

public static Node noLoop(Node head1, Node head2) {
	if (head1 == null || head2 == null) {
		return null;
	}
	Node cur1 = head1;
	Node cur2 = head2;
	int n = 0;
	while (cur1.next != null) {
		n++;
		cur1 = cur1.next;
	}
	while (cur2.next != null) {
		n--;
		cur2 = cur2.next;
	}
	if (cur1 != cur2) {
		return null;
	}
	cur1 = n > 0 ? head1 : head2;
	cur2 = cur1 == head1 ? head2 : head1;
	n = Math.abs(n);
	while (n != 0) {
		n--;
		cur1 = cur1.next;
	}
	while (cur1 != cur2) {
		cur1 = cur1.next;
		cur2 = cur2.next;
	}
	return cur1;
}

(3)判斷2個有環鏈表是否相交,相交則返回第一個相交節點,不相交則返回null

                   圖A

                        圖B

                           圖C

分別求兩個鏈表的第一個入環節點loop1和loop2
若loop1==loop2(圖A),直接返回第一次相交節點=loop1
若loop1!=loop2,判斷是那種拓撲(圖B還是圖C)
鏈表1從loop1出發,走一圈還會再回到loop1,中間若沒遇到loop2,則說明是不相交(圖B)。若遇到loop2說明是相交(圖C),
只不過loop1是離鏈表1近,loop2離鏈表2近,返回loop1或loop2都可以。

public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
	Node cur1 = null;
	Node cur2 = null;
	if (loop1 == loop2) {
		cur1 = head1;
		cur2 = head2;
		int n = 0;
		while (cur1 != loop1) {
			n++;
			cur1 = cur1.next;
		}
		while (cur2 != loop2) {
			n--;
			cur2 = cur2.next;
		}
		cur1 = n > 0 ? head1 : head2;
		cur2 = cur1 == head1 ? head2 : head1;
		n = Math.abs(n);
		while (n != 0) {
			n--;
			cur1 = cur1.next;
		}
		while (cur1 != cur2) {
			cur1 = cur1.next;
			cur2 = cur2.next;
		}
		return cur1;
	} else {
		cur1 = loop1.next;
		while (cur1 != loop1) {
			if (cur1 == loop2) {
				return loop1;
			}
			cur1 = cur1.next;
		}
		return null;
	}
}

(4)判斷兩個(有環/無環)單鏈表是否相交?相交返回第一個相交點,不相交返回null

public static Node getIntersectNode(Node head1, Node head2) {
	if (head1 == null || head2 == null) {
		return null;
	}
	Node loop1 = getLoopNode(head1);
	Node loop2 = getLoopNode(head2);
	if (loop1 == null && loop2 == null) {
		return noLoop(head1, head2);
	}
	if (loop1 != null && loop2 != null) {
		return bothLoop(head1, loop1, head2, loop2);
	}
	return null;
}

 

10、求兩個已排序單鏈表中相同的數據

思路:分別創建兩個結點變量cur1,cur2,如果cur1->data大於cur2->data,則cur2 = cur2->next;如果cur1->data小於cur2->data,則cur1 = cur1->next;如果cur1->data= cur2->data,則cur1和cue2 同時向後走

void commonNode(ListNode * List1, ListNode * List2)
{
	ListNode *cur1,*cur2;
	cur1 = List1, cur2  = List2;
	while (cur1 != NULL && cur2 != NULL)
	{
		if (cur1->data < cur2->data) {
			cur1 = cur1->Next;
		}
		else if (cur1->data > cur2->data) {
			cur2 = cur2->Next;
		} else {
			printf("  %d  ", cur1->data);
			cur1 = cur1->Next;
			cur2 = cur2->Next;
		}
	}
}

11、合併兩個有序鏈表,合併後依然有序---O(n)

遞歸方式

ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
    if(pHead1 == nullptr)
        return pHead2;
    else if(pHead2 == nullptr)
        return pHead1;

    ListNode* pMergedHead = nullptr;

    if(pHead1->m_nValue < pHead2->m_nValue)
    {
        pMergedHead = pHead1;
        pMergedHead->m_pNext = Merge(pHead1->m_pNext, pHead2);
    }
    else
    {
        pMergedHead = pHead2;
        pMergedHead->m_pNext = Merge(pHead1, pHead2->m_pNext);
    }

    return pMergedHead;
}

非遞歸方式

ListNode * Merge(ListNode * pHead1, ListNode * pHead2) {
    ListNode * p1,p2,p,pMergeHead;
    p1 = pHead1; p2 = pHead2; pMergeHead = p = NULL;
    if (pHead1->data < pHead2->data) {
        pMergeHead = pHead1; p1 = p1->next;
    } else {
        pMergeHead = pHead2; p2 = p2->next;
    }
    p = pMergeHead;
    while (p1 && p2) {
        if (pHead1->data < pHead2->data) {
            p->next = p1; p1 = p1->next;
        } else {
            p->next = p2; p2 = p2->next;
        }
        p = p->next;
    }
    if (p1) p->next = p1;
    if (p2) p->next = p2;
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章