大學的時候不好好學習,老師在講臺上講課,自己在以為老師看不到的座位看小說,現在用到了老師講的知識,只能自己看書查資料進行再回爐學習,真心對不住老師,對不住父母,對不住自己的青春啊!
有些路,就是需要你自己走,有些知識就是需要你掌握,不論早幾年還是現在還是幾年后。。。是你的終歸還是你的。。。
什么是線性表?
線性表:零個或者多個數據元素的有限序列。
我們可以看出別管上邊說的前驅還是后繼,都是在表達這個序列是有序的,即:前面有且僅有一個元素,后邊有且僅有一個元素(除了首尾兩端)。
在較復雜的線性表中,一個數據元素可以由若干個數據項組成的。
我們首先來看這個線性表都包含什么:看下圖我們可以看出,線性表里面比較常見的兩個結構是:順序存儲結構和鏈式存儲結構,而鏈式存儲結構下邊又分為:單鏈表、靜態鏈表、循環鏈表、雙向鏈表。
1、順序存儲結構:
我們來看線性表的順序存儲結構的代碼
我們可以發現描述順序存儲結構需要的三個屬性:
(1)存儲空間的起始位置:數組data,它的存儲位置就是存儲空間的存儲位置。
(2)線性表的最大存儲容量:數組長度MaxSize。
(3)線性表的當前長度:length。
順序存儲結構的插入和刪除
(1)插入
插入的思路:1、如果插入的位置不合理,拋出異常。
2、如果線性表長度大于等于數組長度,則拋出異常或動態增加容量。
3、從最后一個元素開始向前遍歷到第i個位置,分別將它們都向后移動一個位置。
4、將要插入的元素填入位置i處。
5、表長加1。
(2)刪除
刪除的思路:1、如果刪除位置不合理,拋出異常。
2、取出刪除元素。
3、從刪除元素位置開始遍歷到最后一個元素位置,分別將它們都向前移動一個位置。
4、表長減1。
順序存儲結構的優缺點
優點:無須為表示表中元素之間的邏輯關系而增加額外的存儲空間;可以快速的存取表中任一位置的元素。
缺點:插入和刪除操作需要移動大量元素;當線性表長度變化較大時,難以確定存儲空間的容量;造成存儲空間的“碎片”。
問題的暴露:
這樣我們就引申出來了線性表的鏈式存儲結構。
為了解決上邊暴露出來的問題,我們讓相鄰的元素之間留有足夠的余地,并且所有的元素都不要考慮相鄰位置了,只讓每個元素知道它下一個元素的位置即可。
我們來看鏈式存儲結構的概念
鏈表中第一個結點的存儲位置叫做頭指針。
在單鏈表的第一個結點附設一個結點叫做頭結點。
頭指針和頭結點的異同:
(1)頭指針是指鏈表指向第一個結點的指針,若鏈表有頭結點,則是指向頭結點的指針;
? ? ? ? 頭指針具有標識作用,所以常用頭指針冠以鏈表的名字;
? ? ? ? 無論鏈表是否為空,頭指針均不為空。頭指針是鏈表的必要元素。
? ? ? ? Node** a;
(2)頭結點是為了操作的統一和方便而設立的,放在第一元素的結點之前,其數據域一般無意義,也可以存放鏈表的長度;
? ? ? ? 有了頭結點,對在第一元素結點前插入結點和刪除第一結點,其操作與其它結點的操作就統一了;
? ? ? ? 頭結點不一定是鏈表的必須元素。
? ? ? ? Node *a;
單鏈表
我們在C語言中可用結構指針來描述:
我們可以看出:結點是由存放數據元素的數據域和存放后繼結點地址的指針域組成。
單鏈表的插入和刪除
(1)插入
單鏈表第i個數據插入結點的算法思路:
1、聲明一個指針p指向鏈表頭結點,初始化j從1開始;
2、當j<i時,就遍歷鏈表,讓p的指針向后移動,不斷指向下一結點,j累加1;
3、若到鏈表末尾p為空,則說明第i個結點不存在;
4、否則查找成功,在系統中生成一個空結點s;
5、將數據元素e賦值給s->data;
6、單鏈表的插入標準語句:s->next=p->next; ?p->next=s;
7、返回成功。
紅框內是關鍵代碼:
(2)刪除
單鏈表第i個數據刪除結點的算法思路:
1、聲明一個指針p指向鏈表頭結點,初始化j從1開始;
2、當j<i時,就遍歷鏈表,讓p的指針向后移動,不斷指向下一結點,j累加1;
3、若到鏈表末尾p為空,則說明第i個結點不存在;
4、否則查找成功,將欲刪除的結點p->next賦值給q;
5、單鏈表的刪除標準語句p->next=q->next;
6、將q結點中的數據賦值給e,作為返回;
7、釋放q結點;
8、返回成功。
紅框內是關鍵代碼:
對于插入和刪除數據越頻繁的操作,單鏈表的效率優勢就越是明顯。
單鏈表結構與順序存儲結構有缺點
(1)存儲分配方式:
順序存儲結構用一段連續的存儲單元依次存儲線性表的數據元素;
單鏈表采用鏈式存儲結構,用一組任意的存儲單元存放線性表的元素。
(2)時間性能
查找:順序存儲結構O(1); ??
? ? ? ? ?單鏈表O(n)。
插入與刪除:順序存儲結構需要平均移動表長一半的元素,時間為O(n);
? ? ? ? ? ? ? ? ? 單鏈表在找出某位置的指針后,插入和刪除時間僅為O(1)。
(3)空間性能:順序存儲結構需要欲分配存儲空間,分大了浪費,分小了容易發生上溢;
? ? ? ? ? ? ? ? ? ? ? ?單鏈表不需要分配存儲空間,只要有就可以分配,元素個數也不受限制。
經驗性總結:
1、如果線性表需要頻繁的查找,很少進行插入和刪除的操作時,宜采用順序存儲結構。如果需要頻繁的插入和刪除時,宜采用單鏈表結構。
2、當線性表中的元素個數變化比較大或者根本不知道有多大的時候,最好用單鏈表結構,這樣可以不用考慮存儲空間的大小問題。如果事先知道線性表的大致長度,用順序存儲結構效率要高很多。
靜態鏈表
對于有指針的語言里面,可以利用指針能力,使得非常容易的操作內存中的地址和數據,但是對于沒有指針的語言,比如Basic、Fortran等早期的變成高級語言,怎么辦呢?
有人想出用數組代替指針!
我們讓數組的元素都是由兩個數據域組成,data和cur。即數組的每一個下標都對應一個data和一個cur。數據域data,用來存放數據。cur相當于單鏈表中的next指針,存放該元素的后繼在數組中的下標。我們把cur叫做游標。
另外我們對數組的第一個和最后一個元素作為特殊元素處理,不存數據。我們通常把違背使用的數組元素成為備用鏈表。而數組的第一個元素,即下標為0的元素的cur就存放備用鏈表的第一個結點的下表;而數組的最后一個元素的cur則存放第一個有數值的元素的下標,相當于單鏈表中的頭結點的作用,當整個鏈表為空時,則為O2。
靜態鏈表的插入:
靜態鏈表的刪除:
靜態鏈表的優缺點:
優點:在插入和刪除操作的時候,只需要修改游標,不需要移動元素,從而改進了在順序存儲結構中的插入和刪除需要移動大量元素的缺點。
缺點:沒有解決連續存儲分配帶來的表長難以確定的問題;失去了順序存儲結構隨機存取的特性。
總的來說,靜態鏈表其實是為了給沒有指針的高級語言設計的一種實現單鏈表能力的方法。
循環鏈表
將單鏈表中終端結點的指針端由空指針改為指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單循環鏈表。簡稱循環鏈表。
雙向鏈表
在單鏈表的每個結點中,再設置一個指向其前驅結點的指針域。所以,在雙向鏈表中有兩個指針域,一個指向后繼,一個指向前驅。
雙向鏈表的插入:
雙向鏈表的刪除:
鏈表的進階學習
面試官喜歡考察與鏈表相關的知識的原因有如下幾點:
1、鏈表的操作代碼量比較小,可以在面試的短時間內完成手寫代碼;
2、鏈表是一種動態的數據結構,其操作需要指針進行操作,可以檢驗面試者的編程功底;
3、鏈表的數據結構很靈活,可以用鏈表設計具有挑戰性的面試題。
我們說鏈表是一種動態的數據結構,是因為創建鏈表的時候,無須知道鏈表的長度,當插入一個結點的時候,我們只需要為新的結點分配內存,然后調整指針的指向被鏈接到鏈表中即可。內存分配不是在創建鏈表的時候一次性完成的,而是每添加一個結點分配一次內存,由于沒有閑置的內存,鏈表的空間效率比數組的要高。
下邊我們就幾道常見的鏈表面試題進行進階階段的學習,通過具體的題目,我們能夠深刻的理解鏈表,并且可以學習到一些常用的思想。
1、從尾到頭打印鏈表
題目:輸入一個鏈表的頭結點,從尾到頭翻過來打印出每個結點的值。
注意:這個題目并不是鏈表的反轉,這里只是將鏈表從尾到頭打印出來,對鏈表并沒有作任何操作,鏈表還是原來的鏈表。
鏈表結點定義如下:
struct ListNode{
int ? ? ? ? ? ? m_nKey;
ListNode* ?m_pNext;
}
答案:網上有人說三種方法可以解決該問題,附上鏈接:三種方法實現從尾到頭打印鏈表
這里我們只介紹兩種:借用棧倒序輸出鏈表和遞歸實現。
(1)借用棧倒序輸出鏈表
解決這個問題我們首先想到的是遍歷鏈表,遍歷的順序是從頭到尾,而輸出的順序是從尾到頭, 即第一個遍歷的結點最后一個輸出,最后一個遍歷的結點第一個輸出,這是典型的“后進先出”,所以我們可以借助棧實現這種順序。
這里需要注意的有以下兩點:
1、我們對于鏈表的認識:鏈表我們只需要知道一個頭結點,那么我們就可以知道整個鏈表,因為頭結點里面包含著指向下一個結點的指針,所以我們上邊定義一個棧的操作后,重新定義一個頭結點=題目中的鏈表的頭結點,那么就相當于我們拿到了題目中的鏈表。
2、在壓棧和彈棧的過程中,我們先進行取棧頂的結點的操作,在打印完成后,我們還需要進行彈棧操作,不然打印的始終是棧頂的同一個結點(也就是鏈表的最后一個結點)!
(2)遞歸實現
這里需要注意的是當鏈表非常長的時候,會導致函數調用的層級很深,占用很多的資源,有可能導致函數調用棧溢出,顯然用棧基于循環實現的代碼(即第一個方法)的健壯性要好一些。
這里我們還理解了“遞歸在本質上是一個棧結構”(不知道后邊的解釋是不是清楚,大家權且看一下吧)
遞歸本質上是一個棧結構:就上邊的這個遞歸舉例,我們可以看到“調用自己形成循環”的這一步該函數調用了自己,相當于欠套了一層我們的函數方法,直至調用到鏈表的最后一個結點,比方我們的鏈表是有5個結點,那么到調用到第5個的時候,實際上我們的代碼已經套用了5層,調用了5遍函數方法,(這其實就是“壓棧”的過程)寫出來的話,會有很大一堆,然后判斷最后一個結點沒有指向的結點了,那么開始返回,從第5層開始一層層的往外層方法返回,雖然我們的函數返回的是一個void,但是也是會返回的,直至返回到我們的第1層函數(這其實就是“彈棧”的過程)。所以我們得出結論:遞歸在本質上是一個棧結構。
2.1、反轉鏈表
既然上邊我們提到了鏈表的反轉,那么我們就來看一下鏈表的反轉到底是怎么實現的。
題目:定義一個函數,輸入一個鏈表的頭結點,反轉該鏈表并輸出反轉后鏈表的頭結點。
鏈表結點定義如下:
struct ListNode{
int ? ? ? ? ? ? m_nKey;
ListNode* ?m_pNext;
}
通過下邊的圖,我們進行解釋:
由于結點i的m_pNext指向了它的前一個結點,導致我們無法在鏈表中遍歷到結點j,為了避免鏈表在結點i處斷開,我們需要在調整結點i的m_pNext之前,把結點j保存下來,所以我們需要的結點有3個:結點i、前一個結點h,后一個結點j,相應我們需要定義3個指針。
注意:以下情況在寫反轉的過程中需要注意。
1、輸入的鏈表頭指針是NULL的情況;
2、輸入的鏈表只有一個結點的情況;
3、輸入的鏈表有多個結點。
其實這里寫的代碼并不多,實際理解過程中,建議大家手寫一個簡單的1234鏈表按照循環過程一步一步的畫一畫比較容易理解。
2.2字符串反轉
OC中字符串的反轉方式可以用兩種方式來處理:
第一種:從頭到尾取出字符串的每一個字符,然后將其從尾到頭添加到可變的字符串中,最后輸出即可。
第二種:將OC內部的字符串轉換為C語言中的字符串,然后動態分配一個數組,然后將字符串內容拷貝到數組中,進行首尾交換操作。共進行數組長度/2次操作。
第一種方法的代碼:(像壓棧和彈棧的思想)
第二種方法的代碼:
3、合并兩個排序的鏈表
題目:輸入兩個遞增的排序鏈表,合并這兩個鏈表使新鏈表中的結點仍然是按照遞增排序的。
鏈表結點定義如下:
struct ListNode{
int ? ? ? ? ? ? m_nKey;
ListNode* ?m_pNext;
}
我們按照題目的要求,可以畫出如下圖的簡單示意圖,鏈表1和鏈表2合并后形成鏈表3.
合并的過程需要有一個比較的過程,誰是頭結點,誰是第二個結點...過程如下圖:
這個過程我們可以分析得到鏈表1和鏈表2的頭結點比較,然后小的最為新的鏈表的頭結點,然后剩余的遞增的鏈表我們命名為鏈表1'和鏈表2',兩個新的鏈表再比較頭結點,比較出來后,又會出現兩個新的鏈表1''和2'',依舊進行比較...這樣我們會發現這其實是一個典型的遞歸過程,我們可以用遞歸函數完成合并過程。
當然在這個過程中,我們還要考慮到如果開始的鏈表1或者鏈表2是空鏈表的情況,但凡其中一個為空,那么合并的結果就是不為空的那個鏈表。如果兩個都為空,合并出來還是空的,屬于上邊返回一個鏈表的情況。
4、鏈表中倒數第k個結點
題目:輸入一個鏈表,輸出該鏈表中倒數第k個結點。
思路:我們走到鏈表的尾端,往回走k個結點就可以得到倒數第k個結點。但是單鏈表只有指向后邊的指針,沒有向前指的指針,所以這個思路暫且不通。
那么我們想到,如果鏈表一共有n個結點的話,倒數第k個結點也就是正數第n-k+1個結點,打比方一共有7個結點,那么倒數點3個,就是正數第(7-3+1)=第5個結點。所以我們只要知道鏈表的結點個數n,然后再找到第n-k+1個結點即可。那樣的話就是遍歷兩遍,首先遍歷得到n,再遍歷走到n-k+1個結點,得到倒數第k個結點,那我們有沒有辦法優化一下,只遍歷一遍呢?
這里我們就引入“快慢指針”的概念。
籠統的解釋一下這個“快慢指針”,簡單的說就是設定兩個指針a、b,兩個指針分別指向頭結點,打比方讓a每次移動兩步,讓b每次移動一步,那么a就是“快指針”,b就是“慢指針”,比方鏈表不是循環鏈表,那么a走到尾端,b才走到中間的位置,這里不考慮奇偶,我們設定一個鏈表的結點數是3個,那么我們的頭結點就是1,(這里需要說明的是我們還是要理解清楚“頭結點”和“頭指針”的區別,頭指針是指向頭結點的指針,只是一個指針,而頭結點是一個結點,包含數據域和指針域,這里我們設定的鏈表,頭結點就是1,所以a、b指針開始的時候都是指向結點1的)那么我們的a從1走到3的結點的位置,a就到了鏈表的尾端,b呢,從1走到了2的的結點的位置,那么我們就可以利用這個“快慢指針”找到所謂的“中位數”是哪個結點,僅僅遍歷一次。
再舉例,一個環形的鏈表,即循環鏈表,同樣的我們設定兩個指針a、b,打比方讓a每次移動兩步,讓b每次移動一步,那么a和b最終有相遇的那么一刻,在某一個結點的時候兩者的數據域是一樣的,而且兩者的指針是指向同一個的下一個結點。這樣我們就可以反過來證明如果有一個鏈表,不告訴你是不是循環鏈表,讓你證明是不是循環鏈表,那我們我們就可以用這個“快慢指針”的方法來證明。
這里附上快慢指針在鏈表的應用,大家可以參考一下。謝謝作者lostman。
言歸正傳,我們回到我們的這個“鏈表中倒數第k個結點”的問題上,根據上邊說的快慢指針的思想我們可以設定兩個指針a、b,同樣的都指向頭結點,讓a先走上兩步,再讓b開始走,兩者每次都是移動一個結點,那么當a到了尾結點的時候,b恰好指到的是倒數第3個結點的位置,那么怎么讓b恰好指到倒數第k個結點的位置呢?我們叫a先走k-1步,再兩者同步走,那么當a到達尾結點的時候,b指向的是正數n-(k-1)的結點,也就是正數n-k+1的結點的位置,也就是倒數第k個結點的位置。
注意:根據前面的反轉鏈表等一些題目,在這里我們也需要考慮鏈表是不是為空,還有就是k=0的情況,還有就是總結點數n<k的情況。想一想前面這3個問題,如果不考慮到的話,很有可能造成我們的程序崩潰。
5、兩個鏈表的第一個公共結點
題目:輸入兩個鏈表,找出它們的第一個公共結點。
示意圖:
蠻力法:遍歷第一個鏈表上每一個結點,每遍歷一個結點,遍歷一遍第二個鏈表,進行進行比較是否相同,如果相同,那么就找到了第一個公共結點;如果不同,繼續遍歷。時間復雜度O(mn)。
棧方法:我們可以想到,有“第一個公共結點”,那么就有大于等于1個公共結點,那么我們是不是可以先到尾結點那里,比較兩個鏈表,如果相同,就彈走,直到兩個不同的結點,那么彈走的那個結點就是第一個公共結點,“后進先出”,這個方法需要設置兩個棧,時間復雜度O(m+n),用空間消耗換取了時間效率。
徹底提高效率的方法(快慢指針法):先得到兩個鏈表的長度,一個長,一個短,那么先讓長的走(長-短)步,然后兩者在一起走,那么走到兩個結點相等的時候,就恰好是“第一個公共結點”。時間復雜度O(m+n),但是沒有用到棧。
首先我們先寫一個獲取鏈表長度的方法,以便使用。
我們開始找第一個公共結點。
同樣過程中我們需要考慮鏈表是不是為空等特殊情況。
我們將上邊的示意圖逆時針旋轉90°,是不是發現,這是樹!不過是葉結點指向了根結點的樹。兩個鏈表的第一個公共結點正好是二叉樹中兩個葉結點的最低公共祖先。
參考資料:《大話數據結構》作者:程杰。清華大學出版社。
? ? ? ? ? ? ? ? 《劍指offer》作者:何海濤。電子工業出版社。
最后,哪里不對的地方可以給我留言,我會及時改進的,謝謝大家。