資料結構學習--連結串列

Ilion發表於2019-02-14

1、什麼是連結串列?

連結串列是物理儲存單元上非連續的、非順序的儲存結構,不同於棧和佇列。連結串列由一系列節點組成,每個結點包括兩個部分:一個是儲存資料元素的資料域,另一個是儲存下一個結點地址的指標域。

由於不必須按順序儲存,連結串列在插入的時候可以達到O(1)的複雜度,比另一種線性表順序錶快得多,但是查詢一個節點或者訪問特定編號的節點則需要O(n)的時間,而線性表和順序表相應的時間複雜度分別是O(logn)和O(1)。使用連結串列結構可以克服陣列連結串列需要預先知道資料大小的缺點,連結串列結構可以充分利用計算機記憶體空間,實現靈活的記憶體動態管理。但是連結串列失去了陣列隨機讀取的優點,同時連結串列由於增加了結點的指標域,空間開銷比較大。

下面為連結串列的結構示意圖

資料結構學習--連結串列

1.1、節點

節點包含了兩部分,一部分是儲存資料的元素區域,一部分是指向下一個節點的指標區域,上圖中綠色部分表示資料區域,藍色部分表示指標區域,它們共同構成一個節點。

定義一個節點:

let Node = function(data) {
    this.data = data // 資料
    this.next = null // 指標
}
// 建立新的節點
let node1 = new Node(1);
let node2 = new Node(2);
let node3 = new Node(3);
node1.next = node2;
node2.next = node3
複製程式碼

1.2、首尾節點

連結串列中的第一個節點是首節點,最後一個節點是尾節點。

1.3、有頭連結串列和無頭連結串列

  1. 無頭連結串列是指第一個節點既有資料域,又有指標域,第一個節點既是首節點又是頭節點。

  2. 有頭連結串列是指第一個節點只有指標域,而沒有資料域。通常有頭連結串列的資料域可以存放當前的連結串列的一些資訊。

在連結串列定義中展示的就是無頭連結串列,一個有頭連結串列的結構圖如下:

資料結構學習--連結串列

2、連結串列的實現

2.1、定義連結串列類

function LinkList() {
    let Node = function(data) {
        this.data = data
        this.next = null
    }
    let length = 0   // 連結串列長度 
    let head = null  // 頭節點
    let tail = null  // 尾節點
}
複製程式碼

2.2、連結串列的方法

  • append, 新增一個新的元素
  • insert,在指定位置插入一個元素
  • remove,刪除指定位置的節點
  • get,返回指定索引位置的元素
  • print,列印整個連結串列

2.2.1、append

  • 每次append,都要先建立一個node節點,如果列表為空,則讓head和tail指向這個新建立的節點
  • 如果列表不為空,則tail.next = node, 並讓tail指向node
this.append = (data) => {
    // 建立一個新的節點
    let new_node = new Node(data)
    
    // 判斷是否為空連結串列
    if(head === null) {
        head = new_node
        tail = head
    } else {
        tail.next = new_node // 尾節點指向新建立的節點
        tail = new_node      // 讓尾節點等於新建立的節點
    }
    length ++
}
複製程式碼

2.2.2、insert

append只能在連結串列的末尾新增元素,而insert可以在指定位置插入一個元素,新增資料的方式更加靈活,insert方法需要傳入引數index,指明要插入的索引位置。該方法的關鍵是找到索引為index-1的節點,只有找到這個節點,才能將新的節點插入到連結串列中

資料結構學習--連結串列

this.insert = (index. data) => {
        if(index<0 || index > length) return // index無效值
        if(index === length) { // 當index等於length呼叫append方法
            return this.append(data)
        } else {
            let new_node = new Node(data)
            if(index === 0) { // 
                new_node.next = head
                head = new_node
            } else {
                let insert_index = 1
                let curr_node = head
                while(insert_index < index) {
                    insert_index ++
                    curr_node = curr_node.next
                }
                let next_node = curr_node.next // 記錄當前節點下一個節點
                curr_node.next = new_node      // 當前節點下一個節點設為新節點
                new_node.next = next_node      // 看上面的圖會更能明白
            }
        }
        length ++
}
複製程式碼

2.2.3、remove

刪除指定位置的節點,需要傳入引數index,和insert方法一樣,先考慮索引的範圍是否合法,然後考慮索引在邊界時的操作,關鍵點是找到索引為index-1的這個節點,這個節點的next指向了要刪除的節點。

資料結構學習--連結串列

this.remove = (index) => {
    if(index<0 || index>length) return
    if(index === 0) {
        let del_node = head
        head = head.next
        del_node.next = null
    } else {
        let del_index = 0
        let pre_node = null // 要刪除節點的前一個節點
        let curr_node = head // 要刪除的節點
        while(del_index<index) { // 依此迴圈找到
            del_index++
            pre_node = curr_node
            curr_node = curr_node.next
        }
        let del_node = curr_node
        pre_node.next = curr_node.next // 要刪除節點的前一個節點的下一個節點等於要刪除節點的下一個節點
        del_node.next = null // 要刪除節點的下一個節點為空
        if(curr_node.next === null) { // 如果刪除的是尾節點
            tail_node = pre_node
        }
    }
    length --
}
複製程式碼

2.2.4、最終程式碼

其他方法比較容易理解

function LinkList() {
  let Node = function (data) {
    this.data = data
    this.next = null
  }
  let length = 0
  let head = null
  let tail = null

  // 在尾部新增節點
  this.append = (data) => {
    // 建立新節點
    let new_node = new Node(data)
    if (head == null) {
      head = new_node
      tail = new_node
    } else {
      tail.next = new_node
      tail = new_node
    }
    length +=1
    return true
  }
  // 列印節點
  this.print = () => {
    let curr_node = head
    while (curr_node) {
      console.log(curr_node.data)
      curr_node = curr_node.next
    }
  }
  // 指定位置新增節點
  this.insert = (index, data) => {
    if (index > length || index < 0) {
      return
    } else if (index == length) {
      return this.append(data)
    } else {
      let new_node = new Node(data)
      if (index == 0) {
        new_node.next = head
        head = new_node
      } else {
        let insert_index = 1
        let curr_node = head
        while (insert_index < index) {
          insert_index ++
          curr_node = curr_node.next
        }
        let next_node = curr_node.next
        curr_node.next = new_node
        new_node.next = next_node
      }
    }
    length ++
    return true
  }
  // 刪除指定位置節點
  this.remove = (index) =>{
    if (index<0||index>=length) {
      return false
    } else {
      let del_node = null
      if (index == 0) {
        del_node = head
        head = head.next
        del_node.next = null
      } else {
        let del_index = 0
        let pre_node = null
        let curr_node = head
        while (del_index<index) {
          del_index++
          pre_node = curr_node
          curr_node = curr_node.next
        }
        del_node = curr_node
        pre_node.next = curr_node.next
        if (curr_node.next == null) {
          tail = pre_node
        }
        del_node.next = null
      }
    }
    length --
    // return del_node.data
  }
  // 返回指定位置節點
  this.get = (index) => {
    if (index>=length || index<0) {
      return false
    }
    let node_index = 0
    let curr_node = head
    while (node_index<index) {
      node_index++
      curr_node = curr_node.next
    }
    return curr_node.data
  }
複製程式碼

3、連結串列應用

3.1、連結串列反轉

迭代反轉

思路

假設連結串列中間的某個點為curr_node,它的前一個節點是pre_node,後一個節點是next_node,現在把思路聚焦到這個curr_node節點上,只考慮在這一個點上進行翻轉:curr_node.next = pre_node;只需要這簡單的一個步驟就可以完成對curr_node節點的翻轉,對於頭節點來說,它沒有上一個節點,讓 pre_node=null,表示它的上一個節點是一個空節點。在遍歷的過程中,每完成一個節點的翻轉,都讓curr_node = next_node,找到下一個需要翻轉的節點。同時,pre_nodenext_node也跟隨curr_node一起向後滑動。

function reveser(head) {
  if (!head) {
    return false
  }
  let pre_node = null
  let curr_node = head
  while (curr_node) { // 迴圈結束條件為當前節點為空
    let next_node = curr_node.next // 記錄當前節點下一個節點
    curr_node.next = pre_node // 當前節點的下一個節點變為前一個節點
    pre_node = curr_node // 向下遍歷
    curr_node = next_node
  }
  return pre_node
}
複製程式碼

遞迴反轉

遞迴的核心之處在於先執行的後執行完,以及遞迴的出口

function reveser_digui(head) {
  if (!head) {
    return false
  }
  if (head.next == null) { // 出口
    return head
  }
  let new_head = reveser_digui(head.next) // 遞迴呼叫
  head.next.next = head // 下一個節點指向上一個節點
  head.next = null // 
  return new_head
}
複製程式碼

3.2、合併兩個有序連結串列

已知有兩個有序連結串列(連結串列元素從小到大),請實現函式merge_link,將兩個連結串列合併成一個有序連結串列,並返回新連結串列,原有的兩個連結串列不要修改。

思路

合併兩個有序連結串列,是歸併排序在連結串列上的一種實踐。對兩個連結串列,各自設定一個遊標節點指向頭節點,對遊標節點上的數值進行比較,數值小的那個拿出來放入到合併連結串列中,同時遊標節點向後滑動,繼續比較遊標節點數值大小。

為了實現滑動,需要使用一個while迴圈,當其中一個遊標節點為null時,迴圈終止,這時,可能另一個遊標節點還沒有到達尾節點,那麼把這段還沒有遍歷結束的連結串列新增到合併列表上。

程式碼

function merge_link(head1,head2) {
    if(head1 === null && head2 === null) return
    if(head1 === null) {
        return head2
    } else if (head2 === null){
        return head1
    }
    let merge_head = null // 合併後的頭節點
    let merge_tail = null // 合併後的尾節點
    let curr1 = head1 // 遊標
    let curr2 = head2
    while(curr1&&curr2) {
        let min_node = null // 最小的節點
        if(curr1.data<curr2.data) { // 找到最小的節點
            min_node = curr1.data
            curr1 = curr1.next  // 向後滑動
        } else {
            min_node = curr2.data
            curr2 = curr2.next
        }
        // 想合併的連結串列新增節點
        if(merge_head === null) { // 連結串列為空
            merge_head = new Node(min_node)
            merge_tail = merge_head
        } else { // 不為空
            let new_node = new Node(min_node)
            merge_tail.next = new_node
            merge_tail = new_node
        }
        // 判斷是否有剩餘的部分
        let res_link = null
        if(curr_1){
            rest_link = curr_1;
        }
        if(curr_2){
            rest_link = curr_2;
        }
        while (res_link) { // 依此將剩餘的加到合併連結串列
            let new_node = new Node(res_link.data)
            merge_tail.next = new_node
            merge_tail = new_node
            res_link = res_link.next
        }
    }
    return merge_head
}
複製程式碼

連結串列還有很多其他會被問道的問題比如:

  1. 查詢單連結串列中的倒數第K個節點(k > 0):定義兩個遊標都指向head,先讓其中一個走k步,然後兩個一起走,當先走的走到盡頭時此時後走的所在的位置就是倒數第k個。
  2. 查詢單連結串列的中間結點:定義兩個節點k1、k2,k1一次走兩步,k2一次走一步,當k2走到盡頭時此時k1所在的位置中間節點。
  3. 實現雙向連結串列:多了一個前驅指標

4、最後

在學習了連結串列之後,發現連結串列比佇列和棧更加困難,日後要多加複習和練習來鞏固學到的內容。

相關文章