連結串列-雙向連結串列

致于数据科学家的小陈發表於2024-06-07

之前實現的是單向連結串列, 即每個節點有一個元素值和一個指向下一個元素的指標, 是單向的. head ->a ->b ->c1.

現在來做一個雙向的, 即對每個節點來說, 有兩個指標, 一個鏈向下一個元素, 另一個鏈向上一個元素. head -> <- b -> <- c.

連結串列初始化

基本的操作套路和單連結串列是差不多的啦.

// 節點類
class Node {
  constructor(element, next = null, prev = null) {
    this.element = element
    this.next = next
    // 新增
    this.prev = prev 

  }
}


// 連結串列類
class doublyLinkedList {
  constructor() {
    this.count = 0           // 連結串列大小
    this.head = undefined    // 指向頭節點元素
    this.tail = undefined    // 指向尾節點元素
  }
}

公共方法

連結串列的長度, 查詢頭, 尾元素, 列印連結串列, 根據元素值查索引, 根據索引查詢值等連結串列公用方法.

// 連結串列長度
size() {
    return this.count
}

getHead() {
    return this.head
}

getTail() {
    return this.tail
}

// 將連結串列節點元素以字串形式列印
toString() {
    if (this.head == null) return undefined
    
    let str = `${this.head.element}`
    let current = this.head.next
    for (let i = 1; i < this.count && current != null; i++) {
        str = `${str}, ${currrent.element}`
        current = current.next
    }
    return str
}	

// 查詢元素的索引
indexOf(element) {
    let current = this.head
    for (let i = 0; i < index && current != null; i++) {
        if (current.element == element) return i
        current = current.next
    }
    // 沒找到則返回 -1
    return -1
}


// 根據索引返回對應節點
getElementAt(index) {
    if (index < 0 || index > this.count) return undefined
    let node = this.head
    for (let i = 0; i < index && node != null; i++) {
        node = node.next
    }
    return node
}

從任意位置插入元素

  • 頭部插入
    • 空連結串列, 讓 head, tail 指標都指向目標元素 node
    • 非空連結串列, 讓 node 變成首元素 (前插)
  • 尾部插入, 透過 tail 先定位到尾部元素, 然後追加 node, 要記得 prev 指向
  • 中間插入, 找出目標元素的前後元素進行斷鏈, 插入, 再連結
// 從任意位置插入元素
insert(index, element) {
    // 越界檢查
    if (index < 0 || index > this.count) return false
    const node = new Node(element)
    let current = this.head  // 讓 current 預設指向頭節點
    
    if (index == 0) {
        // 鏈首插入
        if (this.head == null) {
            // 空連結串列則讓 head, tail 都指向 node 即可
            this.head = node
            this.tail = node
        } else {
            // 頭結點有值時, 則讓 node 前插
            node.next = current
            current.prev = node
            this.head = node
            
        } else if (inde == this.count) {
            // 尾部插入, 透過 tail 定位到尾部元素, 然後追加 node
            current = this.tail
            current.next = node
            node.prev = current
            this.tail = node
            
        } else {
            // 中間插入, 調整 previous, current 關係即可
            current = this.getElementAt(index)
            previous = current.prev
            
            previous.next = node
            node.prev = previous
            
            node.next = current
            current.prev = node
        }
    }
    // 記得要更新連結串列長度
    this.count++
    return true
}

關鍵就是要能在腦海裡想象出這個節點, 連結的畫面就很輕鬆寫出來了.

從任意位置刪除元素

  • 頭節點刪
    • 空連結串列, 返回 false
    • 連結串列有一個元素, 更新 head, tail
    • 連結串列多個元素, 將後面"補位"的元素的 prev 指向 null
  • 尾節點刪, 透過 tail 定位到尾元素, 讓尾元素的 prev 指向 原來的倒2元素, next 則指向 null
  • 從中間刪, 對 previous, current, current.next 進行中間跳躍
// 從任意位置刪除元素
removeAt(index) {
    // 越界檢查, 空連結串列檢查
    if (index < 0 || index > this.count) return false
    if (this.count == 0) return undefined

	let current = this.head  // 老規矩, 讓 current 預設指向頭節點
	if (index == 0) {
  		// 刪除頭節點, 則讓 this.head -> this.head.next 即可
        this.head = current.next
        // 如果連結串列只有一個元素, 則讓 tail -> null
        if (this.count == 1) {
            this.tail = null
        } else {
            // 連結串列有多個元素, 後面補位的 prev 則指向 null
            current.next.prev = null
        }

    } else if (index == this.count - 1) {
        // 刪除尾節點, 定位到尾節點, 讓原來到倒2元素變成尾節點即可
        current = this.tail
        this.tail = current.prev
        current.prev.next = null 
        
    } else {
        // 刪除中間節點, 讓 previous 越過 current 指向 current.next 即可
        current = this.getElementAt(index)
		const previous = current.prev
		
		previous.next = current.next
		current.next.prev = previous
		
    }
	// 還是要記得更新連結串列長度
	this.count--
	return current.element
    
}

// 順帶根據 indexOf(element) 實現 remove(element) 方法
remove(element) {
    const index = indexOf(element)
    return this.removeAt(index)
}


感覺這個刪除節點好像比增加節點要簡單一些呢.

完整實現

// 節點類
class Node {
  constructor(element) {
    this.element = element
    this.next = null
    // 新增
    this.prev = null 

  }
}

// 連結串列類
class doublyLinkedList {
  constructor() {
    this.count = 0           // 連結串列大小
    this.head = undefined    // 指向頭節點元素
    this.tail = undefined    // 指向尾節點元素
  }

  size() {
    return this.count
  }

  getHead() {
    return this.head.element
  }

  getTail() {
    return this.tail.element
  }

  // 將元素的值用字串形式列印出來
  toString() {
    if (this.head == null) return undefined

    let str = `${this.head.element}`
    let current = this.head.next
    // 遍歷節點將元素連線起來
    for (let i = 1; i < this.count && current != null; i++) {
      str = `${str}, ${current.element}`
      current = current.next
    }
    return str 
  }

  // 根據索引, 查詢並返回節點
  getElementAt(index) {
    if (index < 0 || index > this.count) return undefined
    let node = this.head 
    for (let i = 0; i < index && node != null; i++) {
      node = node.next 
    }
    return node 
  }

  // 在任意位置插入元素
  insert(index, element) {
    // 越界檢查
    if (index < 0 || index > this.count) return false 
    // 例項化節點物件, 根據位置 (頭, 尾, 中間) 分情況處理
    // current 指標在多處會用到, 用來指向目標位置的元素
    const node = new Node(element)
    let current = this.head  
    
    if (index == 0) {
      // 頭結點插入, 如果當前是空連結串列, head, tail 都指向改節點即可
      if (this.head == null) {
        this.head = node
        this.tail = node 
      } else {
        // 頭結點插入, 如果當前頭結點有值, 則進行前插元素
        node.next = current
        current.prev = node 
        this.head = node 
      }
    } else if (index == this.count) {
      // 尾部插入, 直接透過 tail 定位到尾部元素往後追加 node 即可
      current = this.tail
      current.next = node 
      node.prev = current
      this.tail = node 
    } else {
      // 中間插入則需要用 getElementAt(index) 定位目標前後的元素斷鏈哦
      current = this.getElementAt(index)
      const previous = current.prev
      
      previous.next = node 
      node.prev = previous

      node.next = current
      current.prev = node 
      
    }
    // 不管從哪插, 一定要更新長度
    this.count++
    return true 
  }

  // 從任意位置移除元素
  removeAt(index) {
    // 越界檢查, 連結串列為空檢查
    if (index < 0 || index > this.count) return undefined
    if (this.count == 0) return undefined

    let current = this.head
    if (index == 0) {
      // 場景1: 被刪的是頭節點, 則讓 this.head -> current.next 即可
      this.head = current.next
      // 如果只有一個元素, 則要更新 tail 也指向 null 
      if (this.count == 1) {
        this.tail = undefined
      } else {
        // 原來位置為1 的元素已被幹掉, 後面補位的元素的 prev 則指向為 null
        current.next.prev = undefined
      }
    } else if (index == this.count -1) {
      // 場景2: 被刪的是尾部節點, 直接讓 current 透過 tail 找到尾元素操作
      // 讓尾元素的 prev 指向原來的倒數第二元素, next 則指向 null 
      current = this.tail 
      this.tail = current.prev  
      current.prev.next = null     
    } else {
      // 場景3: 被刪的是中間節點, 則對 previous, current, current.next 中間跳躍
      current = this.getElementAt(index)
      const previous = current.prev 

      previous.next = current.next 
      current.next.prev = previous
      
    }
    // 記得刪除元素要更新連結串列長度哦
    this.count--
    return current.element
  }

  // 查詢元素位置
  indexOf(element) {
    let current = this.head 
    for (let i = 0; i < this.count && current != null; i++) {
      if (current.element == element) return i
      // 移動指標
      current = current.next 
    }
    return -1
  }

  // 刪除元素
  remove(element) {
    const index = this.indexOf(element)
    return this.removeAt(index)
  }
}



// test 
const list = new doublyLinkedList()

list.insert(0, 888)
list.insert(0, 666)
list.insert(0, 111)
list.insert(0, 'first')
console.log('連結串列元素是:',  list.toString());
console.log('連結串列長度是: ', list.size())
console.log('tail: ', list.getTail());

console.log('從位置1處新增 999:',  list.insert(1, 999));
console.log('連結串列元素是:',  list.toString());
console.log('連結串列長度是: ', list.size());
console.log('tail: ', list.getTail());

console.log('從尾部處新增 nb:',  list.insert(list.size(), 'nb'));
console.log('連結串列元素是:',  list.toString());
console.log('連結串列長度是: ', list.size());
console.log('tail: ', list.getTail());

console.log('從頭部處新增 nb1:',  list.insert(0, 'nb1'));
console.log('連結串列元素是:',  list.toString());
console.log('tail: ', list.getTail());

// 刪除相關
console.log('從頭部刪除元素: ', list.removeAt(0));
console.log('連結串列元素是:',  list.toString());
console.log('tail: ', list.getTail());

console.log('從尾部刪除元素: ', list.removeAt(list.size() - 1));
console.log('連結串列元素是:',  list.toString());
console.log('tail: ', list.getTail());

console.log('從位置 1 處刪除元素: ', list.removeAt(1));
console.log('連結串列元素是:',  list.toString());
console.log('tail: ', list.getTail());

// 查詢元素 666 的位置
console.log('查詢元素 666 的位置: ', list.indexOf(666));     // 2
console.log('查詢元素 hhh 的位置: ', list.indexOf('hhh'));  // -1

// 刪除元素
console.log('刪除元素 666 : ', list.remove(666));     
console.log('連結串列元素是:',  list.toString());

console.log('刪除元素 hhh : ', list.remove('hhh'));  
console.log('連結串列元素是:',  list.toString());

測試結果如下:

PS F:\algorithms> node .\double_linked_list.js

連結串列元素是: first, 111, 666, 888
連結串列長度是:  4
tail:  888
從位置1處新增 999: true
連結串列元素是: first, 999, 111, 666, 888
連結串列長度是:  5
tail:  888
從尾部處新增 nb: true
連結串列元素是: first, 999, 111, 666, 888, nb     
連結串列長度是:  6
tail:  nb
從頭部處新增 nb1: true
連結串列元素是: nb1, first, 999, 111, 666, 888, nb
tail:  nb
從頭部刪除元素:  nb1
連結串列元素是: first, 999, 111, 666, 888, nb     
tail:  nb
從尾部刪除元素:  nb
連結串列元素是: first, 999, 111, 666, 888
tail:  888
從位置 1 處刪除元素:  999
連結串列元素是: first, 111, 666, 888
tail:  888
查詢元素 666 的位置:  2
查詢元素 hhh 的位置:  -1
刪除元素 666 :  666
連結串列元素是: first, 111, 888
刪除元素 hhh :  undefined
連結串列元素是: first, 111, 888

至此, 雙向連結串列的基本實現也就搞定啦.

相關文章