双向链表实现

之前实现的是单向链表, 即每个节点有一个元素值和一个指向下一个元素的指针, 是单向的. 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

至此, 双向链表的基本实现也就搞定啦.

耐心和恒心, 总会获得回报的.