一、連結串列的定義
連結串列是一種遞迴的資料結構,是一種線性結構,但是並不會按線性的順序儲存資料,而是在每一個節點裡存到下一個節點的指標(Pointer),簡單來說連結串列並不像陣列那樣將陣列儲存在一個連續的記憶體地址空間裡,它們可以不是連續的因為他們每個節點儲存著下一個節點的引用(地址)
二、連結串列的型別
單連結串列
- 1、定義
單連結串列(又稱單向連結串列)是連結串列中的一種,其特點是連結串列的連結方向是單向的,對連結串列的訪問要從頭部(head)開始,然後依次通過next指標讀取下一個節點。
- 2、資料結構
單連結串列的資料結構可以分為兩部分:資料域和指標域,資料域儲存資料,指標域指向下一個儲存節點的地址。注意: 單向連結串列只可向一個方向進行遍歷
- 3、節點程式碼描述
//(Kotlin描述)
class LinkedNode(var value: Int) {
var next: LinkedNode? = null //指向下一個儲存節點的next指標
}
複製程式碼
//(Java描述)
public class LinkedNode {
int value;
LinkedNode next; //指向下一個儲存節點的next指標
public LinkedNode(int value) {
this.value = value;
}
}
複製程式碼
雙連結串列
- 1、定義
雙連結串列(又稱雙向連結串列),是連結串列中一種,與單連結串列不同的是它的每個節點都有兩個指標,分別指向直接後繼節點和直接前驅節點;所以,從雙連結串列中的任意一個結點開始,都可以很方便地訪問它的前驅結點和後繼結點。
- 2、資料結構
雙連結串列的資料結構可以分為三部分:prev指標域、資料域和next指標域,prev指標域指向上一個儲存節點的地址(也即指向直接前驅節點),資料域儲存資料,next指標域指向下一個儲存節點的地址(也即指向直接後繼節點)。注意: 單向連結串列可向兩個方向進行遍歷,分別為正序和逆序遍歷
- 3、節點程式碼描述
//(Kotlin描述)
class LinkedNode(var value: Int) {
var prev: LinkedNode? = null //指向上一個儲存節點的prev指標
var next: LinkedNode? = null //指向下一個儲存節點的next指標
}
複製程式碼
//(Java描述)
public class LinkedNode {
int value;
LinkedNode prev; //指向上一個儲存節點的prev指標
LinkedNode next; //指向下一個儲存節點的next指標
public LinkedNode(int value) {
this.value = value;
}
}
複製程式碼
單向迴圈連結串列
- 1、定義
單向迴圈連結串列,只是在單連結串列的基礎上,它的最後一個結點不再為null而是指向頭結點,形成一個環。並且在節點結構上和單連結串列是一樣的。因此,從單向迴圈連結串列中的任何一個結點出發都能找到任何其他結點。
- 2、資料結構
雙向迴圈連結串列
- 1、定義
雙向迴圈連結串列,只是在雙連結串列的基礎,它的頭節點的prev指標不再為null,而是直接指向它的尾節點;它的尾節點的next指標不再為null,而是直接指向它的頭節點。
- 2、資料結構
三、連結串列的特點
- 1、在記憶體中不是連續的記憶體地址空間,它只是一種邏輯上的線性連續結構。每個節點都含有指向下一個節點的next指標(可能指向下一個節點或null)
- 2、連結串列在節點的刪除和增加有著很高效率,基本是O(1)常數級的時間效率,而順序表實現刪除和增加操作則是線性級O(n)的時間效率。所以一般用於用於元素節點頻繁刪除和增加
- 3、而對於連結串列的查詢和獲得第K個連結串列中節點,往往需要採用遍歷的方式實現,所以一般需要O(n)的時間效率
- 4、連結串列長度是可變的,也就意味著在記憶體空間足夠範圍內,連結串列長度可以無限擴大。而順序表則一般是固定的,當超出長度的時候則會進行擴容。
四、連結串列的基本操作
連結串列的構造
我們知道一個節點型別的變數就可以表示一條連結串列,只要保證對應的每個節點的next指標能夠指向下一個節點即可或指向null(表示連結串列最後一個節點)
- 1、單連結串列的構造
//連結串列結構定義
class LinkedNode(var value: Int) {
var next: LinkedNode? = null
}
//連結串列的構造
fun main(args: Array<String>) {
val node1 = LinkedNode(value = 1)//建立節點1
val node2 = LinkedNode(value = 2)//建立節點2
val node3 = LinkedNode(value = 3)//建立節點3
node1.next = node2//通過node1的next指標指向node2,把node1和node2連線起來
node2.next = node3//通過node2的next指標指向node3,把node2和node3連線起來
}
複製程式碼
- 2、雙連結串列的構造
class LinkedNode(var value: Int) {
var prev: LinkedNode? = null
var next: LinkedNode? = null
}
fun main(args: Array<String>) {
val node1 = LinkedNode(value = 1)//建立節點1 此時的prev,next均為null
val node2 = LinkedNode(value = 2)//建立節點2 此時的prev,next均為null
val node3 = LinkedNode(value = 3)//建立節點3 此時的prev,next均為null
node1.next = node2 //node1的next指標指向直接後繼節點node2
node2.prev = node1 //node2的prev指標指向直接前驅節點node1
node2.next = node3 //node2的next指標指向直接後繼節點node3
node3.prev = node2 //node3的prev指標指向直接前驅節點node2
}
複製程式碼
連結串列表頭插入節點
在連結串列表頭插入一個節點是最簡單的一種操作,一般處理方式,先建立一個oldFirst指向第一個節點,然後重新建立一個新的節點,將新節點的next指向oldFirst指向的節點,first指向新插入的節點。
- 1、單連結串列表頭插入節點
fun insertToHead(head: LinkedNode): LinkedNode {
var first: LinkedNode = head
val oldFirst: LinkedNode = head
first = LinkedNode(value = 6)
first.next = oldFirst
return first
}
複製程式碼
- 2、雙連結串列表頭插入節點
fun insertToHead(head: LinkedNode): LinkedNode {
var first: LinkedNode = head
val oldFirst: LinkedNode = head
first = LinkedNode(value = 6)
oldFirst.prev = first
first.next = oldFirst
return first
}
複製程式碼
在表頭刪除節點
- 1、單連結串列表頭刪除節點
fun deleteToHead(head: LinkedNode): LinkedNode? {
var first: LinkedNode? = head
first = first?.next
return first
}
複製程式碼
- 2、雙連結串列表頭刪除節點
fun deleteToHead(head: LinkedNode): LinkedNode? {
var first: LinkedNode? = head
first = first?.next
first?.prev = null
return first
}
複製程式碼
在表尾插入節點
- 1、單連結串列尾部插入節點
fun insertToTail(head: LinkedNode): LinkedNode? {
var last = getTailNode(head) //通過遍歷得到尾部節點
val oldLast = last
last = LinkedNode(value = 4)
oldLast?.next = last
return head
}
複製程式碼
- 2、雙連結串列尾部插入節點
fun insertToTail(head: LinkedNode): LinkedNode? {
var last = getTailNode(head) //通過遍歷得到尾部節點
val oldLast = last
last = LinkedNode(value = 4)
oldLast?.next = last
last.prev = oldLast
return head
}
複製程式碼
在其他位置插入節點
- 1、單連結串列其他位置插入節點
fun insertToOther(head: LinkedNode): LinkedNode? {
val current = getInsertPrevNode(head) //拿到需要的插入位置的上一個節點
val newNode = LinkedNode(value = 6)
newNode.next = current?.next// 新插入的節點next指向插入位置的上一個節點的next
current?.next = newNode//然後斷開插入位置的上一個節點的next,並把指向新插入的節點
return head
}
複製程式碼
- 2、雙連結串列其他位置插入節點
fun insertToOther(head: LinkedNode): LinkedNode? {
val current = getInsertPrevNode(head) //拿到需要的插入位置的上一個節點
val newNode = LinkedNode(value = 6)
newNode.next = current?.next// 新插入的節點next指向插入位置的上一個節點的next
newNode.prev = current //新插入的節點prev指向插入位置的上一個節點
current?.next = newNode//然後斷開插入位置的上一個節點的next,並把它指向新插入的節點
current?.next?.prev = newNode //然後斷開插入位置的上一個節點的prev,並把它指向新插入的節點
return head
}
複製程式碼
在其他位置刪除節點
- 1、單連結串列其他位置刪除節點
fun deleteToOther(head: LinkedNode): LinkedNode? {
val current = getInsertPrevNode(head) //拿到需要的刪除節點的上一個節點
current?.next = current?.next?.next
return head
}
複製程式碼
- 2、雙連結串列其他位置刪除節點
fun deleteToOther(head: LinkedNode): LinkedNode? {
val current = getDeletePrevNode(head) //拿到需要的刪除節點的上一個節點
current?.next = current?.next?.next
current?.next?.prev = current
return head
}
複製程式碼
連結串列的遍歷
fun traverseLinkedList(head: LinkedNode?) {
var current = head
while (current != null){
println(current.value)
current = current.next
}
}
複製程式碼
獲取連結串列的大小
fun getLength(head: LinkedNode?): Int {
var len = 0
var current = head
while (current != null){
len++
current = current.next
}
return len
}
複製程式碼
五、連結串列實現棧和佇列資料結構
1、連結串列實現棧結構
由於棧是一個表,因此任何實現表的方法都能實現棧。顯然,Java中常用的ArrayList和LinkedList集合都是支援棧操作的。
- 實現思路
單連結串列也是能實現棧的,通過在表的頂端插入實現棧的push壓棧操作,通過刪除表的頂端元素實現pop入棧操作。top操作只需要返回頂部的元素的值即可。
- 實現程式碼
class LinkedStack {
private var first: Node? = null
private var len: Int = 0
fun push(value: Int) {//相當於連結串列從表頭插入新的元素
val oldFirst = first
first = Node(value)
first?.next = oldFirst
len++
}
fun pop(): Int {//相當於連結串列從表頭刪除新的元素
val value = first?.value
first = first?.next
return value ?: -1
}
fun top(): Int {
return first?.value ?: -1
}
fun isEmpty(): Boolean {
return first == null
}
fun size(): Int {
return len
}
inner class Node(var value: Int) {
var next: Node? = null
}
}
複製程式碼
2、連結串列實現佇列結構
class LinkedQueue {
private var first: Node? = null
private var last: Node? = null
private var len: Int = 0
fun enqueue(value: Int) {//相當於連結串列從尾部插入新的節點
val oldLast = last
last = Node(value)
last?.next = null
if (isEmpty()) {
first = last
} else {
oldLast?.next = last
}
len++
}
fun dequeue(): Int {//相當於連結串列從尾部刪除最後節點
val value = first?.value ?: -1
first = first?.next
if (isEmpty()) {
last = null
}
return value
}
fun isEmpty(): Boolean {
return first == null
}
fun size(): Int {
return len
}
inner class Node(var value: Int) {
var next: Node? = null
}
}
複製程式碼
六、連結串列反轉問題
- 1、定義
連結串列反轉(也稱連結串列的逆序)是連結串列中一種比較經典的操作,在一些資料結構的題目連結串列的反轉也是常考點,連結串列的反轉也會做為一部分融入題目,比如迴文連結串列問題等
-
2、實現過程
-
3、程式碼描述
fun reverseLinkedList(head: LinkedNode?): LinkedNode? {
var prev: LinkedNode? = null
var current: LinkedNode? = head
var next: LinkedNode? = head
while (current != null) {
next = current.next
current.next = prev
prev = current
current = next
}
return prev
}
複製程式碼
七、連結串列中經典快慢指標問題
快慢指標追趕問題在連結串列中是非常經典的,快慢指標問題一般用於解決連結串列中間節點問題和連結串列是否含有環以及連結串列中環的入口位置等問題。
如果使用快慢指標是判斷連結串列是否含有環的問題,我們更希望fast和slow指標的相對路程是正好是環的長度,(也就是slow指標剛進入環,而fast指標剛繞環一圈,此時兩指標正好相遇)這樣兩個指標就相遇了。這樣取每步的速度差能夠被環長度整除的數字。但是我們並不知道環的具體長度,所以只能取每步的速度差能夠被環長度整除的數字為1(1能被所有的數整除),所以我們取fast指標每次走2步,slow指標每次走1步,實際上只要保證兩者速度差為1就可以了,你甚至可以fast每次走3步,slow指標每次走2步都是可以的,這樣一來只要它們在環裡面就一定能相遇。
1、快慢指標與連結串列環問題
public boolean hasCycle(ListNode head) {
if(head == null || head.next == null) return false;
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;//慢指標每次走1步
fast = fast.next.next;//快指標每次走2步
if(slow == fast){//如果連結串列存在環,那麼slow和fast指標會相遇
return true;
}
}
return false;
}
複製程式碼
2、快慢指標找中間節點問題
由快慢指標追趕的原理可知,如果fast指標和slow指標同時從連結串列(連結串列不含環)的頭結點出發開始遍歷,如果fast指標的每次遍歷步數是slow指標的兩倍,那麼可得到如果fast遍歷到連結串列的尾部,那麼此時的slow指標應該處於連結串列的中間節點位置(具體題目可參考:LeetCode第876題)。
public ListNode middleNode(ListNode head) {
if(head == null) return null;
ListNode slow = head;
ListNode fast = head;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
複製程式碼
八、LeetCode連結串列相關題目
-
1、刪除連結串列的節點
-
2、反轉連結串列
-
3、連結串列的中間節點
-
4、合併兩個有序連結串列
-
5、刪除排序連結串列中的重複元素
-
6、移除連結串列中的元素
-
7、相交連結串列
-
8、環形連結串列
-
9、迴文連結串列
-
10、設計連結串列
歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~
Kotlin系列文章,歡迎檢視:
Kotlin邂逅設計模式系列:
資料結構與演算法系列:
翻譯系列:
- [譯] Kotlin中關於Companion Object的那些事
- [譯]記一次Kotlin官方文件翻譯的PR(內聯類)
- [譯]Kotlin中內聯類的自動裝箱和高效能探索(二)
- [譯]Kotlin中內聯類(inline class)完全解析(一)
- [譯]Kotlin的獨門祕籍Reified實化型別引數(上篇)
- [譯]Kotlin泛型中何時該用型別形參約束?
- [譯] 一個簡單方式教你記住Kotlin的形參和實參
- [譯]Kotlin中是應該定義函式還是定義屬性?
- [譯]如何在你的Kotlin程式碼中移除所有的!!(非空斷言)
- [譯]掌握Kotlin中的標準庫函式: run、with、let、also和apply
- [譯]有關Kotlin型別別名(typealias)你需要知道的一切
- [譯]Kotlin中是應該使用序列(Sequences)還是集合(Lists)?
- [譯]Kotlin中的龜(List)兔(Sequence)賽跑
原創系列:
- 教你如何完全解析Kotlin中的型別系統
- 如何讓你的回撥更具Kotlin風味
- Jetbrains開發者日見聞(三)之Kotlin1.3新特性(inline class篇)
- JetBrains開發者日見聞(二)之Kotlin1.3的新特性(Contract契約與協程篇)
- JetBrains開發者日見聞(一)之Kotlin/Native 嚐鮮篇
- 教你如何攻克Kotlin中泛型型變的難點(實踐篇)
- 教你如何攻克Kotlin中泛型型變的難點(下篇)
- 教你如何攻克Kotlin中泛型型變的難點(上篇)
- Kotlin的獨門祕籍Reified實化型別引數(下篇)
- 有關Kotlin屬性代理你需要知道的一切
- 淺談Kotlin中的Sequences原始碼解析
- 淺談Kotlin中集合和函式式API完全解析-上篇
- 淺談Kotlin語法篇之lambda編譯成位元組碼過程完全解析
- 淺談Kotlin語法篇之Lambda表示式完全解析
- 淺談Kotlin語法篇之擴充套件函式
- 淺談Kotlin語法篇之頂層函式、中綴呼叫、解構宣告
- 淺談Kotlin語法篇之如何讓函式更好地呼叫
- 淺談Kotlin語法篇之變數和常量
- 淺談Kotlin語法篇之基礎語法
Effective Kotlin翻譯系列
- [譯]Effective Kotlin系列之考慮使用原始型別的陣列優化效能(五)
- [譯]Effective Kotlin系列之使用Sequence來優化集合的操作(四)
- [譯]Effective Kotlin系列之探索高階函式中inline修飾符(三)
- [譯]Effective Kotlin系列之遇到多個構造器引數要考慮使用構建器(二)
- [譯]Effective Kotlin系列之考慮使用靜態工廠方法替代構造器(一)
實戰系列: