每週一數據結構之鏈表(Kotlin描述)

1、鏈表的定義

鏈表是一種遞歸的數據結構,是一種線性結構,可是並不會按線性的順序存儲數據,而是在每個節點裏存到下一個節點的指針(Pointer),簡單來講鏈表並不像數組那樣將數組存儲在一個連續的內存地址空間裏,它們能夠不是連續的由於他們每一個節點保存着下一個節點的引用(地址)java

2、鏈表的類型

單鏈表

  • 一、定義

單鏈表(又稱單向鏈表)是鏈表中的一種,其特色是鏈表的連接方向是單向的,對鏈表的訪問要從頭部(head)開始,而後依次經過next指針讀取下一個節點。node

  • 二、數據結構

單鏈表的數據結構能夠分爲兩部分:數據域和指針域,數據域存儲數據,指針域指向下一個存儲節點的地址。注意: 單向鏈表只可向一個方向進行遍歷算法

  • 三、節點代碼描述
//(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;
    }
}
複製代碼

雙鏈表

  • 一、定義

雙鏈表(又稱雙向鏈表),是鏈表中一種,與單鏈表不一樣的是它的每一個節點都有兩個指針,分別指向直接後繼節點直接前驅節點;因此,從雙鏈表中的任意一個結點開始,均可以很方便地訪問它的前驅結點和後繼結點。設計模式

  • 二、數據結構

雙鏈表的數據結構能夠分爲三部分:prev指針域、數據域和next指針域,prev指針域指向上一個存儲節點的地址(也即指向直接前驅節點),數據域存儲數據,next指針域指向下一個存儲節點的地址(也即指向直接後繼節點)。注意: 單向鏈表可向兩個方向進行遍歷,分別爲正序和逆序遍歷數組

  • 三、節點代碼描述
//(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;
    }
}
複製代碼

單向循環鏈表

  • 一、定義

單向循環鏈表,只是在單鏈表的基礎上,它的最後一個結點再也不爲null而是指向頭結點,造成一個環。而且在節點結構上和單鏈表是同樣的。所以,從單向循環鏈表中的任何一個結點出發都能找到任何其餘結點。數據結構

  • 二、數據結構

雙向循環鏈表

  • 一、定義

雙向循環鏈表,只是在雙鏈表的基礎,它的頭節點的prev指針再也不爲null,而是直接指向它的尾節點;它的尾節點的next指針再也不爲null,而是直接指向它的頭節點。app

  • 二、數據結構

3、鏈表的特色

  • 一、在內存中不是連續的內存地址空間,它只是一種邏輯上的線性連續結構。每一個節點都含有指向下一個節點的next指針(可能指向下一個節點或null)
  • 二、鏈表在節點的刪除和增長有着很高效率,基本是O(1)常數級的時間效率,而順序表實現刪除和增長操做則是線性級O(n)的時間效率。因此通常用於用於元素節點頻繁刪除和增長
  • 三、而對於鏈表的查找和得到第K個鏈表中節點,每每須要採用遍歷的方式實現,因此通常須要O(n)的時間效率
  • 四、鏈表長度是可變的,也就意味着在內存空間足夠範圍內,鏈表長度能夠無限擴大。而順序表則通常是固定的,當超出長度的時候則會進行擴容。

4、鏈表的基本操做

鏈表的構造

咱們知道一個節點類型的變量就能夠表示一條鏈表,只要保證對應的每一個節點的next指針可以指向下一個節點便可或指向null(表示鏈表最後一個節點)函數

  • 一、單鏈表的構造

//鏈表結構定義
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鏈接起來
}
複製代碼
  • 二、雙鏈表的構造

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指向新插入的節點。post

  • 一、單鏈表表頭插入節點

fun insertToHead(head: LinkedNode): LinkedNode {
    var first: LinkedNode = head
    val oldFirst: LinkedNode = head
    first = LinkedNode(value = 6)
    first.next = oldFirst
    return first
}
複製代碼
  • 二、雙鏈表表頭插入節點

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
}
複製代碼

在表頭刪除節點

  • 一、單鏈表表頭刪除節點

fun deleteToHead(head: LinkedNode): LinkedNode? {
    var first: LinkedNode? = head
    first = first?.next
    return first
}
複製代碼
  • 二、雙鏈表表頭刪除節點

fun deleteToHead(head: LinkedNode): LinkedNode? {
    var first: LinkedNode? = head
    first = first?.next
    first?.prev = null
    return first
}
複製代碼

在表尾插入節點

  • 一、單鏈表尾部插入節點

fun insertToTail(head: LinkedNode): LinkedNode? {
    var last = getTailNode(head) //經過遍歷獲得尾部節點
    val oldLast = last
    last = LinkedNode(value = 4)
    oldLast?.next = last
    return head
}
複製代碼
  • 二、雙鏈表尾部插入節點

fun insertToTail(head: LinkedNode): LinkedNode? {
    var last = getTailNode(head) //經過遍歷獲得尾部節點
    val oldLast = last
    last = LinkedNode(value = 4)
    oldLast?.next = last
    last.prev = oldLast
    return head
}
複製代碼

在其餘位置插入節點

  • 一、單鏈表其餘位置插入節點
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
}
複製代碼
  • 二、雙鏈表其餘位置插入節點

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
}
複製代碼

在其餘位置刪除節點

  • 一、單鏈表其餘位置刪除節點

fun deleteToOther(head: LinkedNode): LinkedNode? {
    val current = getInsertPrevNode(head) //拿到須要的刪除節點的上一個節點
    current?.next = current?.next?.next
    return head
}
複製代碼
  • 二、雙鏈表其餘位置刪除節點

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
}
複製代碼

5、鏈表實現棧和隊列數據結構

一、鏈表實現棧結構

因爲棧是一個表,所以任何實現表的方法都能實現棧。顯然,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
    }
}
複製代碼

二、鏈表實現隊列結構

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
    }
}
複製代碼

6、鏈表反轉問題

  • 一、定義

鏈表反轉(也稱鏈表的逆序)是鏈表中一種比較經典的操做,在一些數據結構的題目鏈表的反轉也是常考點,鏈表的反轉也會作爲一部分融入題目,好比迴文鏈表問題等

  • 二、實現過程

  • 三、代碼描述

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
}
複製代碼

7、鏈表中經典快慢指針問題

快慢指針追趕問題在鏈表中是很是經典的,快慢指針問題通常用於解決鏈表中間節點問題和鏈表是否含有環以及鏈表中環的入口位置等問題。

若是使用快慢指針是判斷鏈表是否含有環的問題,咱們更但願fast和slow指針的相對路程是正好是環的長度,(也就是slow指針剛進入環,而fast指針剛繞環一圈,此時兩指針正好相遇)這樣兩個指針就相遇了。這樣取每步的速度差可以被環長度整除的數字。可是咱們並不知道環的具體長度,因此只能取每步的速度差可以被環長度整除的數字爲1(1能被全部的數整除),因此咱們取fast指針每次走2步,slow指針每次走1步,實際上只要保證二者速度差爲1就能夠了,你甚至能夠fast每次走3步,slow指針每次走2步都是能夠的,這樣一來只要它們在環裏面就必定能相遇。

一、快慢指針與鏈表環問題

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;
    }
複製代碼

二、快慢指針找中間節點問題

由快慢指針追趕的原理可知,若是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;
    }
複製代碼

8、LeetCode鏈表相關題目

  • 一、刪除鏈表的節點

  • 二、反轉鏈表

  • 三、鏈表的中間節點

  • 四、合併兩個有序鏈表

  • 五、刪除排序鏈表中的重複元素

  • 六、移除鏈表中的元素

  • 七、相交鏈表

  • 八、環形鏈表

  • 九、迴文鏈表

  • 十、設計鏈表

歡迎關注Kotlin開發者聯盟,這裏有最新Kotlin技術文章,每週會不按期翻譯一篇Kotlin國外技術文章。若是你也喜歡Kotlin,歡迎加入咱們~~~

Kotlin系列文章,歡迎查看:

Kotlin邂逅設計模式系列:

數據結構與算法系列:

翻譯系列:

原創系列:

Effective Kotlin翻譯系列

實戰系列:

相關文章
相關標籤/搜索