文章目錄
線性表的一大結構:鏈式存儲結構的一些知識點總結歸納。並使用 JavaScript 來實現簡單的單鏈表。循環鏈表以及雙向鏈表是在單鏈表的基礎上擴展。
1. 什麼是鏈表。
來看一下 LeetCode 上對鏈表的描述:
鏈表(Linked List)是一種常見的基礎數據結構,是一種線性表,但是並不會按線性的順序存儲數據,
而是在每一個節點裏存到下一個節點的指針(Pointer)。
由於不必須按順序存儲,鏈表在插入的時候可以達到 O(1)O(1) 的複雜度,比另一種線性表 —— 順序表
快得多,但是查找一個節點或者訪問特定編號的節點則需要 O(n)O(n) 的時間,而順序表相應的時間複雜
度分別是 O(log\ n)O(log n) 和 O(1)O(1)。
使用鏈表結構可以克服數組鏈表需要預先知道數據大小的缺點,鏈表結構可以充分利用計算機內存空間,
實現靈活的內存動態管理。但是鏈表失去了數組隨機讀取的優點,同時鏈表由於增加了結點的指針域,
空間開銷比較大。
在計算機科學中,鏈表作爲一種基礎的數據結構可以用來生成其它類型的數據結構。鏈表通常由一連串節
點組成,每個節點包含任意的實例數據(data fields)和一或兩個用來指向上一個/或下一個節點的位置
的鏈接(links)。鏈表最明顯的好處就是,常規數組排列關聯項目的方式可能不同於這些數據項目在記憶
體或磁盤上順序,數據的訪問往往要在不同的排列順序中轉換。而鏈表是一種自我指示數據類型,因爲它包
含指向另一個相同類型的數據的指針(鏈接)。
鏈表允許插入和移除表上任意位置上的節點,但是不允許隨機存取。鏈表有很多種不同的類型:單向鏈表,
雙向鏈表以及循環鏈表。
鏈表通常可以衍生出循環鏈表,靜態鏈表,雙鏈表等。對於鏈表使用,需要注意頭結點的使用。
以上大概就是對鏈表的一個說明,那用代碼來展示一個鏈表的話是什麼樣子呢?
我們先從一個簡單的單向鏈表嘗試做一下:
2. 鏈表的表現形式
const linkedList = {
head: {
value: 0,
next: {
value: 1,
next: {
value: 2,
next: null
}
}
}
};
與圖片上展示的鏈表結構相同:每個節點都有一個 value
屬性,展示當前節點的值,以及一個指向下一個節點(保存指向下一個節點的指針)的屬性 next
。這構成了一個節點。多個節點構成了鏈表。同時也可以看到在鏈表的頭部,使用 head
來表示,在鏈表的末端節點 next
指向爲 null
。
既然知道了鏈表的展現形式,那就嘗試使用代碼將鏈表生成:
3. 鏈表的生成
// 節點
class LinkedNode {
value: any
next: any
constructor(nodeValue) {
this.value = nodeValue;
this.next = null
}
}
// 鏈表
class LinkedList {
constructor() {
// 生成鏈表頭部,鏈表頭部的數據域中可以不存儲任何信息
// 也可以存儲鏈表長度等信息
// 這裏爲了區分,使頭節點的數據爲 'head'
this.head = new LinkedNode("head")
}
private head: LinkedNode
}
4. 鏈表的操作
鏈表也應該支持查詢 / 插入 / 刪除 等操作,先來實現鏈表的查詢:
4.1 鏈表的查詢
查找一個元素是否在鏈表中是鏈表操作中比較常用的操作:
/**
* @author Frank Wang
* @method
* @name find
* @description 查詢鏈表中是否有符合條件的節點
* @param nodeValue {any} 所要查詢的 node 節點的 value 值
* @return {LinkedNode}
* @example 創建例子。
* @public
*/
find(nodeValue: any): LinkedNode {
let curNode: LinkedNode = this.head
while (curNode.value != nodeValue) {
if (!curNode.next) {
// 如果到鏈表尾部還沒有循環到符合條件的節點
// 則返回 value 值爲 error 對象的節點
return new LinkedNode(new Error("無此節點"))
}
curNode = curNode.next
}
return curNode
}
主要是一個內部的循環,遍歷整個鏈表,判斷當前節點的 value
值是否與要查找的值相同。
既然有了查找,那我們向鏈表中插入某一節點就有思路了:
4.2 向鏈表中插入節點
向傳入的節點的後方插入節點
/**
* @author Frank Wang
* @method
* @name insert
* @description 向鏈表中插入節點,第二個參數爲空的話,向尾部插入
* @param value 向鏈表中插入的節點的 value
* @param nodeValue 已經存在的 node 節點的 value
* @return {LinkedNode}
* @example 創建例子。
* @public
*/
insert(value: any, nodeValue: any = null):LinkedNode {
let newNode: LinkedNode = new LinkedNode(value)
let curNode: LinkedNode
if (!nodeValue) {
// append 的方法
// 查找倒數第二個節點
curNode = this.head
while (curNode.next !== null) {
curNode = curNode.next
}
curNode.next = newNode
newNode.next = null
} else {
curNode = this.find(nodeValue)
newNode.next = curNode.next
curNode.next = newNode
}
return this.head
}
插入的方法,接受兩個參數:一個是新的 node 節點的 value,另一個是可選的,已經存在的 node 節點
的 value,如果只傳入一個參數,則往鏈表尾部插入。函數體首先是根據傳入的 value 創建一個 node
實例,以及找到需要插入的node節點。然後將當前節點的 next 賦值給新的節點實例,在將新節點,
賦值給當前節點的 next 值。尤其注意這兩步的操作,是不能調換的,可以想像爲什麼。
4.3 展示鏈表
插入鏈表之後我們肯定是想知道鏈表的當前情況,或者是我們在某個時刻想要查看鏈表的情況,那我們就需要
一個方法來展示鏈表:
/**
* @author Frank Wang
* @method
* @name console
* @description 將鏈表所有的值在控制檯打印出來
* @param
* @return {void}
* @example 創建例子。
* @public
*/
console(): void {
let currNode: LinkedNode = this.head;
while (currNode.next) {
console.log(currNode.next.value);
currNode = currNode.next;
}
}
這個是打印出除了頭節點的其他所有節點的 value 值。如果只是單純的獲取節點對象,那麼可以使用:
show() {
return this.head
}
同樣的我們還會遇到刪除某一節點的需求:
4.4 刪除鏈表中某一節點
在解決這個需求之前,我們可以先來分析一下,如何刪除一個鏈表中的節點,就是將此節點的上一節點的
next 指向當前節點的 next。
所以如果我們要刪除某一節點,那麼我們就需要知道當前節點的上一個節點
private findPrev(value: any) {
let curNode: LinkedNode = this.head
while (curNode.next !== null && curNode.next.value !== value) {
curNode = curNode.next
}
return curNode
}
/**
* @author Frank Wang
* @method
* @name delete
* @description 刪除鏈表中的節點
* @param {nodeValue} 需要刪除的節點的 value
* @return {LinkedNode}
* @example 創建例子。
* @public
*/
delete(nodeValue: any) {
const err = "[object Error]"
const showType = Object.prototype.toString
let curNode = this.find(nodeValue)
if (showType.call(curNode) === err) {
return curNode
}
const prevNode = this.findPrev(nodeValue);
if (prevNode.next !== null) {
prevNode.next = prevNode.next.next;
}
return this.head;
}
解釋一下上面的邏輯:首先是進行判斷,對錯誤條件儘早返回。然後是使用內部方法 findPrev
,查找
所要刪除節點的上一節點: prevNode
,
然後就是簡單的 next
指針的改變。
4.5 單向鏈表的總結
單鏈表結構和順序存儲結構做對比:
- 存儲分配方式
- 順序存儲結構用一段連續的存儲單元一次存儲線性表的數據元素。
- 單鏈表採用鏈式存儲結構,用一組任意的存儲單元存放線性表的元素
- 時間性能
- 查找
- 順序存儲結構O(1)
- 單鏈表O(n)
- 插入和刪除
- 順序存儲結構需要平均移動表長一半的元素,時間爲 O(n)
- 單鏈表再找出某位置的指針後,插入和刪除時間僅爲O(1)
- 查找
- 空間性能
- 順序存儲結構需要預分配存儲空間,分大了,浪費。分小了,易發生上溢。
- 單鏈表不需要分配存儲空間,只要有就可以分配,元素個數也不受限制。
5. 循環鏈表
將單鏈表中終端節點的指針端由空指針改爲指向頭節點,就使整個單鏈表形成了一個環,這種頭尾相接的單鏈表稱爲單循環鏈表,簡稱循環鏈表。
6. 雙向鏈表
雙向鏈表是在單鏈表的每個節點中,再設置一個指向其前驅節點的指針域。
所以在雙向鏈表中的節點都有兩個指針域,一個指向直接後繼,另一個指向直接前驅。
6.1 插入
如果需要將 s 節點,插入到 p 節點和 p.next 節點之中,那這個插入操作的順序是啥?
- 把 p 賦值給 s 的前驅
- 把 p.next 賦值給 s 的後繼節點
- 把 s 賦值給 p.next 的前驅
- 把 s 賦值給 p 的後繼
7. 總結
線性表 | |||||
---|---|---|---|---|---|
順序存儲結構 | 鏈式存儲結構 | ||||
單鏈表 | 靜態鏈表 | 循環鏈表 | 雙向鏈表 |
這是鏈表屬於線性表的兩大結構之一:鏈式存儲結構。