前言
由於前面寫了一些數據結構的相關的文章,但是都是偏基本的數據結構知識,並沒有實際的算法題加以實踐,故整理十道題目,都是比較常見的鏈表類的算法題,也參考了優秀的博客。
預備的數據結構知識點:
數據結構緒論
循序漸進學習棧和隊列
循序漸進學習數據結構之線性表
循序漸進學習時間復雜度
1.鏈表的倒數第K個結點
問題描述:
輸入一個鏈表,輸出該鏈表中倒數第k個結點。為了符合大多數人的習慣,本題從1開始計數,即鏈表的尾結點是倒數第1個結點。例如一個鏈表有6個結點,從頭結點開始它們的值依次是1、2、3、4、5、6。這個鏈表的倒數第3個結點是值為4的結點,需要保證時間復雜度。
算法思路:
設置兩個指針p1,p2,從頭到尾開始出發,一個指針先出發k個節點,然后第二個指針再進行出發,當第一個指針到達鏈表的節點的時候,則第二個指針表示的位置就是鏈表的倒數第k個節點的位置。
代碼如下:
//倒數第k個結點
ListNode findKth(ListNode head,int k){
ListNode cur=head;
ListNode now=head;
int i=0;
while(cur!=null&i++<k){
cur=cur->next;
}
while(cur!=null){
now=now->next;
cur=cur->next;
}
}
總結:當我們用一個指針遍歷鏈表不能解決問題的時候,可以嘗試用兩個指針來遍歷鏈表。可以讓其中一個指針遍歷的速度快一些(比如一次在鏈表上走兩步),或者讓它先在鏈表上走若干步。
2.從尾到頭打印鏈表(遞歸和非遞歸)
問題描述:
輸入一個單鏈表鏈表,從尾到頭打印鏈表每個節點的值。輸入描述:輸入為鏈表的表頭;輸出描述:輸出為需要打印的“新鏈表”的表頭。
算法思路:
首先我們想到從尾到頭打印出來,由於單鏈表的查詢只能從頭到尾,所以可以想出棧的特性,先進后出。所以非遞歸可以把鏈表的點全部放入一個棧當中,然后依次取出棧頂的位置即可。
代碼如下:
//非遞歸
void PrintReversing(ListNode * head){
//利用一個棧
Stack stack;
ListNode *node=head->next;
//將鏈表的結點壓入
while(node!=null){
stack.push(node);
node=node->next;
}
ListNode *popNode;
while(stack.isEmpty()){
//獲得最上面的元素
popNode=stack.top();
//打印
printf("%d\t",popNode->value);
//彈出元素
stack.pop();
}
/遞歸
void printRevese(ListNode *head){
if(head!=null){
if(head->next!=null){
printRevese(head->next);
}
print("%d\t",head->value);
}
}
非遞歸的描述當中,經常會用棧或者隊列這些數據結構來改寫一些遞歸的算法。其實遞歸的算法的時間復雜度是遞歸樹的高度,所以遞歸的層數越高,時間復雜度也就會越高的。
3.如何判斷一個鏈表有環
問題描述:
有一個單向鏈表,鏈表當中有可能出現“環”,如何用程序判斷出這個鏈表是有環鏈表?
不允許修改鏈表結構。時間復雜度O(n),空間復雜度O(1)。
算法思路:
方法一、窮舉遍歷
首先從頭節點開始,依次遍歷單鏈表的每一個節點。每遍歷到一個新節點,就從頭節點重新遍歷新節點之前的所有節點,用新節點ID和此節點之前所有節點ID依次作比較。如果發現新節點之前的所有節點當中存在相同節點ID,則說明該節點被遍歷過兩次,鏈表有環;如果之前的所有節點當中不存在相同的節點,就繼續遍歷下一個新節點,繼續重復剛才的操作。
假設從鏈表頭節點到入環點的距離是D,鏈表的環長是S。那么算法的時間復雜度是0+1+2+3+….+(D+S-1) = (D+S-1)(D+S)/2 , 可以簡單地理解成 O(NN)。而此算法沒有創建額外存儲空間,空間復雜度可以簡單地理解成為O(1)。
這種方法是暴力破解的方式,時間復雜度太高。
方法二、快慢指針
首先創建兩個指針1和2,同時指向這個鏈表的頭節點。然后開始一個大循環,在循環體中,讓指針1每次向下移動一個節點,讓指針2每次向下移動兩個節點,然后比較兩個指針指向的節點是否相同。如果相同,則判斷出鏈表有環,如果不同,則繼續下一次循環。
說明 :在循環的環里面,跑的快的指針一定會反復遇到跑的慢的指針 ,比如:在一個環形跑道上,兩個運動員在同一地點起跑,一個運動員速度快,一個運動員速度慢。當兩人跑了一段時間,速度快的運動員必然會從速度慢的運動員身后再次追上並超過,原因很簡單,因為跑道是環形的。
代碼如下:
/**
* 判斷單鏈表是否存在環
* @param head
* @return
*/
public static <T> boolean isLoopList(ListNode<T> head){
ListNode<T> slowPointer, fastPointer;
//使用快慢指針,慢指針每次向前一步,快指針每次兩步
slowPointer = fastPointer = head;
while(fastPointer != null && fastPointer.next != null){
slowPointer = slowPointer.next;
fastPointer = fastPointer.next.next;
//兩指針相遇則有環
if(slowPointer == fastPointer){
return true;
}
}
return false;
}
4.鏈表中環的大小
問題描述
有一個單向鏈表,鏈表當中有可能出現“環”,那么如何知道鏈表中環的長度呢?
算法思路
由[3.如何判斷一個鏈表有環](# 3.如何判斷一個鏈表有環)可以知道,快慢指針可以找到鏈表是否有環存在,如果兩個指針第一次相遇后,第二次相遇是什么時候呢?第二次相遇是不是可以認為快的指針比慢的指針多跑了一個環的長度。所以找到第二次相遇的時候就找到了環的大小。
代碼如下
//求環中相遇結點
public Node cycleNode(Node head){
//鏈表為空則返回null
if(head == null)
return null;
Node first = head;
Node second = head;
while(first != null && first.next != null){
first = first.next.next;
second = second.next;
//兩指針相遇,則返回相遇的結點
if(first == second)
return first;
}
//鏈表無環,則返回null
return null;
}
public int getCycleLength(Node head){
Node node = cycleNode(head);
//node為空則代表鏈表無環
if(node == null)
return 0;
int length=1;
Node current = node.next;
//再次相遇則循環結束
while(current != node){
length++;
current = current.next;
}
return length;
}
5.鏈表中環的入口結點
問題描述
給一個鏈表,若其中包含環,請找出該鏈表的環的入口結點,否則,輸出null。
算法思路
如果鏈表存在環,那么計算出環的長度n,然后准備兩個指針pSlow,pFast,pFast先走n步,然后pSlow和pFase一塊走,當兩者相遇時,即為環的入口處;所以解決三個問題:如何判斷一個鏈表有環;如何判斷鏈表中環的大小;鏈表中環的入口結點。實際上最后的判斷就如同鏈表的倒數第k個節點。
代碼如下
public class Solution {
public ListNode EntryNodeOfLoop(ListNode pHead)
{
if(pHead.next == null || pHead.next.next == null)
return null;
ListNode slow = pHead.next;
ListNode fast = pHead.next.next;
while(fast != null){
if(fast == slow){
fast = pHead;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
slow = slow.next;
fast = fast.next.next;
}
return null;
}
}
以上5題的套路其實都非常類似,第5題可以說是前面幾道題的一個匯總題目吧,鏈表類的題利用快慢指針,兩個指針確實挺多的,如下面題目7
6.單鏈表在時間復雜度為O(1)刪除鏈表結點
問題描述
給定單鏈表的頭指針和一個結點指針,定一個函數在時間復雜度為O(1)刪除鏈表結點
算法思路
根據了解的條件,如果只有一個單鏈表的頭指針,鏈表的刪除操作其實正常的是O(n)的時間復雜度。因為首先想到的是從頭開始順序遍歷單鏈表,然后找到節點,再進行刪除。但是這樣的方式達到的時間復雜度並不是O(1);實際上純粹的刪除節點操作,鏈表的刪除操作是O(1)。前提是需要找到刪除指定節點的前一個結點就可以。
那么是不是必須找到刪除指定節點的前一個結點呢?如果我們刪除的節點是A,那么我們把A下一個節點B和A的data進行交換,然后我們刪除節點B,是不是也可以達到同樣的效果。
答案是肯定的。
既然不能在O(1)得到刪除節點的前一個元素,但我們可以輕松得到后一個元素,這樣,我們何不把后一個元素賦值給待刪除節點,這樣也就相當於是刪除了當前元素。可見,該方法可行,但如果待刪除節點為最后一個節點,則不能按照以上思路,沒有辦法,只能按照常規方法遍歷,時間復雜度為O(n),是不是不符合題目要求呢?可能很多人在這就會懷疑自己的思考,從而放棄這種思路,最后可能放棄這道題,這就是這道面試題有意思的地方,雖看簡單,但是考察了大家的分析判斷能力,是否擁有強大的心理,充分自信。其實我們分析一下,仍然是滿足題目要求的,如果刪除節點為前面的n-1個節點,則時間復雜度為O(1),只有刪除節點為最后一個時,時間復雜度才為O(n),所以平均的時間復雜度為:(O(1) * (n-1) + O(n))/n = O(1);仍然為O(1).
代碼如下
/* Delete a node in a list with O(1)
* input: pListHead - the head of list
* pToBeDeleted - the node to be deleted
*/
struct ListNode
{
int m_nKey;
ListNode* m_pNext;
};
void DeleteNode(ListNode *pListHead, ListNode *pToBeDeleted)
{
if (!pListHead || !pToBeDeleted)
return;
if (pToBeDeleted->m_pNext != NULL) {
ListNode *pNext = pToBeDeleted->m_pNext;
pToBeDeleted->m_pNext = pNext->m_pNext;
pToBeDeleted->m_nKey = pNext->m_nKey;
delete pNext;
pNext = NULL;
}
else { //待刪除節點為尾節點
ListNode *pTemp = pListHead;
while(pTemp->m_pNext != pToBeDeleted)
pTemp = pTemp->m_pNext;
pTemp->m_pNext = NULL;
delete pToBeDeleted;
pToBeDeleted = NULL;
}
}
題目的考慮的點,也很特別
7.兩個鏈表的第一個公共結點
問題描述
輸入兩個單鏈表,找出他們的第一個公共結點。
算法思路
我們了解到單鏈表的指針是指向下一個節點的,如果兩個單鏈表的第一個公共節點就說明他們后面的節點都是在一起的。類似下圖,由於兩個鏈表的長度可能是不一致的,所以首先比較兩個鏈表的長度m,n,然后用兩個指針分別指向兩個鏈表的頭節點,讓較長的鏈表的指針先走|m-n|個長度,如果他們下面的節點是一樣的,就說明出現了第一個公共節點。
代碼如下
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
if (pHead1 == null||pHead2 == null) {
return null;
}
int count1 = 0;
ListNode p1 = pHead1;
while (p1!=null){
p1 = p1.next;
count1++;
}
int count2 = 0;
ListNode p2 = pHead2;
while (p2!=null){
p2 = p2.next;
count2++;
}
int flag = count1 - count2;
if (flag > 0){
while (flag>0){
pHead1 = pHead1.next;
flag --;
}
while (pHead1!=pHead2){
pHead1 = pHead1.next;
pHead2 = pHead2.next;
}
return pHead1;
}
if (flag <= 0){
while (flag<0){
pHead2 = pHead2.next;
flag ++;
}
while (pHead1 != pHead2){
pHead2 = pHead2.next;
pHead1 = pHead1.next;
}
return pHead1;
}
return null;
}
}
8.合並兩個排序的鏈表
問題描述
輸入兩個單調遞增的鏈表,輸出兩個鏈表合成后的鏈表,當然我們需要合成后的鏈表滿足單調不減規則。
算法思路
這道題比較簡單,合並兩個有序的鏈表,就可以設置兩個指針進行操作即可,同時比較大小,但是也需要注意兩個鏈表的長度進行比較。
代碼如下
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Solution {
public ListNode Merge(ListNode list1,ListNode list2) {
ListNode head = new ListNode(-1);
ListNode cur = head;
while (list1 != null && list2 != null) {
if (list1.val <= list2.val) {
cur.next = list1;
list1 = list1.next;
} else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
if (list1 != null)
cur.next = list1;
if (list2 != null)
cur.next = list2;
return head.next;
}
}
9.復雜的鏈表復制
問題描述
題目:請實現函數ComplexListNode Clone(ComplexListNode head),復制一個復雜鏈表。在復雜鏈表中,每個結點除了有一個Next指針指向下一個結點外,還有一個Sibling指向鏈表中的任意結點或者NULL。
下圖是一個含有5個結點的復雜鏈表。圖中實線箭頭表示m_pNext指針,虛線箭頭表示m_pSibling指針。為簡單起見,指向NULL的指針沒有畫出。
算法思路
第一種:O(n2)的普通解法
第一步是復制原始鏈表上的每一個結點,並用Next節點鏈接起來;
第二步是設置每個結點的Sibling節點指針。
第二種 :借助輔助空間的O(n)解法
第一步仍然是復制原始鏈表上的每個結點N創建N',然后把這些創建出來的結點用Next鏈接起來。同時我們把<N,N'>的配對信息放到一個哈希表中。
第二步還是設置復制鏈表上每個結點的m_pSibling。由於有了哈希表,我們可以用O(1)的時間根據S找到S'。
第三種:不借助輔助空間的O(n)解法
第一步仍然是根據原始鏈表的每個結點N創建對應的N'。(把N'鏈接在N的后面)
第二步設置復制出來的結點的Sibling。(把N'的Sibling指向N的Sibling)
第三步把這個長鏈表拆分成兩個鏈表:把奇數位置的結點用Next鏈接起來就是原始鏈表,偶數數值的則是復制鏈表。
代碼如下
public class Solution {
public RandomListNode Clone(RandomListNode pHead) {
if(pHead == null) {
return null;
}
RandomListNode currentNode = pHead;
//1、復制每個結點,如復制結點A得到A1,將結點A1插到結點A后面;
while(currentNode != null){
RandomListNode cloneNode = new RandomListNode(currentNode.label);
RandomListNode nextNode = currentNode.next;
currentNode.next = cloneNode;
cloneNode.next = nextNode;
currentNode = nextNode;
}
currentNode = pHead;
//2、重新遍歷鏈表,復制老結點的隨機指針給新結點,如A1.random = A.random.next;
while(currentNode != null) {
currentNode.next.random = currentNode.random==null?null:currentNode.random.next;
currentNode = currentNode.next.next;
}
//3、拆分鏈表,將鏈表拆分為原鏈表和復制后的鏈表
currentNode = pHead;
RandomListNode pCloneHead = pHead.next;
while(currentNode != null) {
RandomListNode cloneNode = currentNode.next;
currentNode.next = cloneNode.next;
cloneNode.next = cloneNode.next==null?null:cloneNode.next.next;
currentNode = currentNode.next;
}
return pCloneHead;
}
}
10.反轉鏈表
問題描述
題目:定義一個函數,輸入一個鏈表的頭結點,反轉該鏈表並輸出反轉后鏈表的頭結點。如圖:
算法思路
為了正確地反轉一個鏈表,需要調整鏈表中指針的方向。為了將復雜的過程說清楚,這里借助於下面的這張圖片。
上面的圖中所示的鏈表中,h、i和j是3個相鄰的結點。假設經過若干操作,我們已經把h結點之前的指針調整完畢,這個結點的m_pNext都指向前面的一個結點。接下來我們把i的m_pNext指向h,此時結構如上圖所示。
從上圖注意到,由於結點i的m_pNext都指向了它的前一個結點,導致我們無法在鏈表中遍歷到結點j。為了避免鏈表在i處斷裂,我們需要在調整結點i的m_pNext之前,把結點j保存下來。
即在調整結點i的m_pNext指針時,除了需要知道結點i本身之外,還需要i的前一個結點h,因為我們需要把結點i的m_pNext指向結點h。同時,還需要實現保存i的一個結點j,以防止鏈表斷開。故我們需要定義3個指針,分別指向當前遍歷到的結點、它的前一個結點及后一個結點。故反轉結束后,新鏈表的頭的結點就是原來鏈表的尾部結點。尾部結點為m_pNext為null的結點。
代碼如下
public class ReverseList_16 {
public ListNode ReverseList(ListNode head) {
if (head == null || head.nextNode == null) {
return head;
}
ListNode next = head.nextNode;
head.nextNode = null;
ListNode newHead = ReverseList(next);
next.nextNode = head;
return newHead;
}
public ListNode ReverseList1(ListNode head) {
ListNode newList = new ListNode(-1);
while (head != null) {
ListNode next = head.nextNode;
head.nextNode = newList.nextNode;
newList.nextNode = head;
head = next;
}
return newList.nextNode;
}
}
歡迎關注公眾號:coder辰砂,一個認認真真寫文章的公眾號
參考:
https://blog.csdn.net/u010983881/article/details/78896293
https://blog.csdn.net/inspiredbh/article/details/54915091
https://www.jianshu.com/p/092d14d13216
https://www.cnblogs.com/bakari/p/4013812.html