js資料結構-連結串列
連結串列和陣列
大家都用過js中的陣列,陣列其實是一種線性表的順序儲存結構,它的特點是用一組地址連續的儲存單元依次儲存資料元素。而它的缺點也正是其特點而造成,比如對陣列做刪除或者插入的時候,可能需要移動大量的元素。
這裡大致模擬一下陣列的插入操作:
function insert(arr, index, data) { for (let i = arr.length; i >index; i--) { arr[i] = arr[i - 1]; } arr[index] = data; }
從上面的程式碼可以看出陣列的插入以及刪除都有可能會是一個O(n)的操作。從而就引出了連結串列這種資料結構,連結串列不要求邏輯上相鄰的元素在物理位置上也相鄰,因此它沒有順序儲存結構所具有的缺點,當然它也失去了陣列在一塊連續空間內隨機存取的優點。
單向連結串列
單向連結串列的特點:
- 用一組任意的記憶體空間去儲存資料元素(這裡的記憶體空間可以是連續的,也可以是不連續的)
- 每個節點(node)都由資料本身和一個指向後續節點的指標組成
- 整個連結串列的存取必須從頭指標開始,頭指標指向第一個節點
- 最後一個節點的指標指向空(NULL)
連結串列中的幾個主要操作
- 建立節點
- 插入節點
- 搜尋/遍歷節點
- 刪除節點
- 合併
初始化節點
- 指標指向空
- 儲存資料
class Node {
constructor(key) {
this.next = null;
this.key = key;
}
}
初始化單向連結串列
- 每個連結串列都有一個頭指標,指向第一個節點,沒節點則指向NULL
class List { constructor() { this.head = null; } }
建立節點
static createNode(key) {
return new createNode(key);
}
這裡說明一下,這一塊我是向外暴露了一個靜態方法來建立節點,而並非直接把它封裝進插入操作裡去,因為我感覺這樣的邏輯會更加正確一些。 從建立一個連結串列 -> 建立一個節點 -> 將節點插入進連結串列中。可能你會遇到一些文章介紹的方式是直接將一個數據作為引數去呼叫insert操作,在insert內部做了一個建立節點。
插入節點(插入到頭節點之後)
插入操作只需要去調整節點的指標即可,兩種情況:
-
head沒有指向任何節點,說明當前插入的節點是第一個
- head指向新節點
- 新節點的指標指向NULL
-
head有指向的節點
- head指向新的節點
- 新節點的指標指向原本head所指向的節點
insert(node) {
// 如果head有指向的節點
if(this.head){
node.next = this.head;
}else {
node.next = null;
}
this.head = node;
}
搜尋節點
- 從head開始查詢
- 找到節點中的key等於想要查詢的key的時候,返回該節點
find(key) {
let node = this.head;
while(node !== null && node.key !== key){
node = node.next;
}
return node;
}
刪除節點
這裡分三種情況:
-
所要刪除的節點剛好是第一個,也就是head指向的節點
- 將head指向所要刪除節點的下一個節點(node.next)
-
要刪除的節點為最後一個節點
- 尋找到所要刪除節點的上一個節點(prevNode)
- 將prevNode中的指標指向NULL
-
在列表中間刪除某個節點
- 尋找到所要刪除節點的上一個節點(prevNode)
- 將prevNode中的指標指向當前要刪除的這個節點的下一個節點
delete(node) {
// 第一種情況
if(node === this.head){
this.head = node.next;
return;
}
// 查詢所要刪除節點的上一個節點
let prevNode = this.head;
while (prevNode.next !== node) {
prevNode = prevNode.next;
}
// 第二種情況
if(node.next === null) {
prevNode.next = null;
}
// 第三種情況
if(node.next) {
prevNode.next = node.next;
}
}
單向連結串列整體的程式碼
class ListNode {
constructor(key) {
this.next = null;
this.key = key;
}
}
class List {
constructor() {
this.head = null;
this.length = 0;
}
static createNode(key) {
return new ListNode(key);
}
// 往頭部插入資料
insert(node) {
// 如果head後面有指向的節點
if (this.head) {
node.next = this.head;
} else {
node.next = null;
}
this.head = node;
this.length++;
}
find(key) {
let node = this.head;
while (node !== null && node.key !== key) {
node = node.next;
}
return node;
}
delete(node) {
if (this.length === 0) {
throw 'node is undefined';
}
if (node === this.head) {
this.head = node.next;
this.length--;
return;
}
let prevNode = this.head;
while (prevNode.next !== node) {
prevNode = prevNode.next;
}
if (node.next === null) {
prevNode.next = null;
}
if (node.next) {
prevNode.next = node.next;
}
this.length--;
}
}
雙向連結串列
如果你把上面介紹的單向列表都看明白了,那麼這裡介紹的雙向列表其實差不多。
從上面的圖可以很清楚的看到雙向連結串列和單向連結串列的區別。雙向連結串列多了一個指向上一個節點的指標。
初始化節點
- 指向前一個節點的指標
- 指向後一個節點的指標
- 節點資料
class ListNode {
this.prev = null;
this.next = null;
this.key = key;
}
初始化雙向連結串列
- 頭指標指向NULL
class List {
constructor(){
this.head = null;
}
}
建立節點
static createNode(key){
return new ListNode(key);
}
插入節點((插入到頭節點之後)
- 看上圖中head後面的第一個節點可以知道,該節點的prev指向NULL
- 節點的next指標指向後一個節點, 也就是當前頭指標所指向的那個節點
- 如果head後有節點,那麼原本head後的節點的prev指向新插入的這個節點(因為是雙向的嘛)
- 最後將head指向新的節點
insert(node) {
node.prev = null;
node.next = this.head;
if(this.head){
this.head.prev = node;
}
this.head = node;
}
搜尋節點
這裡和單向節點一樣,就直接貼程式碼了
search(key) {
let node = this.head;
while (node !== null && node.key !== key) {
node = node.next;
}
return node;
}
刪除節點
和之前單向連結串列一樣,分三種情況去看:
-
刪除的是第一個節點
- head指向所要刪除節點的下一個節點
- 下一個節點的prev指標指向所要刪除節點的上一個節點
-
刪除的是中間的某個節點
- 所要刪除的前一個節點的next指向所要刪除的下一個節點
- 所要刪除的下一個節點的prev指向所要刪除的前一個節點
-
刪除的是最後一個節點
- 要刪除的節點的上一個節點的next指向null(也就是指向刪除節點的next所指的地址)
delete(node) {
const {prev,next} = node;
delete node.prev;
delete node.next;
if(node === this.head){
this.head = next;
}
if(next){
next.prev = prev;
}
if(prev){
prev.next = next;
}
}
雙向連結串列整體程式碼
class ListNode {
constructor(key) {
// 指向前一個節點
this.prev = null;
// 指向後一個節點
this.next = null;
// 節點的資料(或者用於查詢的鍵)
this.key = key;
}
}
/**
* 雙向連結串列
*/
class List {
constructor() {
this.head = null;
}
static createNode(key) {
return new ListNode(key);
}
insert(node) {
node.prev = null;
node.next = this.head;
if (this.head) {
this.head.prev = node;
}
this.head = node;
}
search(key) {
let node = this.head;
while (node !== null && node.key !== key) {
node = node.next;
}
return node;
}
delete(node) {
const { prev, next } = node;
delete node.prev;
delete node.next;
if (node === this.head) {
this.head = next;
}
if (prev) {
prev.next = next;
}
if (next) {
next.prev = prev;
}
}
}
總結
這裡做一個小總結吧,可能有一部分人讀到這裡還不是特別的明白,我的建議是先好好看懂上面的單向連結串列。 其實只要你明白了連結串列的基礎概念,就是有一個head,然後在有好多的節點(Node),然後用一個指標把他們串起來就好了,至於裡面的插入操作也好,刪除也好,其實都是在調整節點中指標的指向。
後續
後續可能還會是資料結構,可能是講二叉堆,也可能回過頭來講一些佇列和棧的思想在程式中的應用。歡迎大家指出文章的錯誤,如果有什麼寫作建議也可以提出。我會持續的去寫關於前端的一些技術文章,如果大家喜歡的話可以關注一下哈