js数据结构和算法04 双向链表

61 阅读5分钟

认识双向链表

在这里插入图片描述

image.png

图解

在这里插入图片描述

双向链表结构封装

代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script>
    function DoublyLinkedList(){
      // 属性
      this.head = null;
      this.tail = null;
      this.length = 0;

      // 内部类
      function Node(data){
        this.data = data;
        this.prev = null;
        this.next = null;
      }
    }
  </script>
</body>
</html>

菜鸟反正现在感觉写这个结构封装很轻松,因为只要明白了需要哪些参数指向哪里,然后内部类就是节点的封装,里面封装的就是节点内容!

双向链表的常见操作

image.png

其实和单向链表差不多,只是封装的方式不同,提供给外面使用的方法大致是一致的,毕竟都是链表,都是增删改查!其实你又会发现链表的方法又和数组的很像,毕竟链表就是用来在某些时候代替数组而使操作更加简单!

append实现 -- 尾部添加项

菜鸟看视频,结果发现这个没有视频,只好自己写了,思路和单链表一样,后面找到了视频代码,发现自己写复杂了!

我的代码

// 1 append方法
DoublyLinkedList.prototype.append = function(data){
  // 创建节点
  let newNode = new Node(data);

  // 判断插入地点
  if(this.length === 0){
    this.head = newNode;
    this.tail = newNode;
  }else{
    let current = this.head;
    // 找到最后一个节点
    while(current.next){
      current = current.next;
    }
    current.next =newNode;
    newNode.prev = current;
  }

  // length+1
  this.length += 1;
}

视频代码

// 2 append2方法 视频
DoublyLinkedList.prototype.append2 = function(data){
  // 1 创建节点
  let newNode = new Node(data);

  // 2 判断添加的是否是第一个节点
  if(this.length === 0){
    this.head = newNode;
    this.tail = newNode;
  }else{
    newNode.prev = this.tail;
    this.tail.next =newNode;
    this.tail = newNode;
  }

  // length+1
  this.length += 1;
}

其实这里是菜鸟没有跳出单链表的思维,毕竟这里都已经有一个参数指向最后一个节点了,结果菜鸟还在靠循环去找,哈哈哈哈!

字符串方法实现

代码

// 3 backwardString方法 -- 从前往后
DoublyLinkedList.prototype.backwardString = function(){
  // 定义变量
  let current = this.head;
  let string = '';

  // 依次向后遍历
  while(current){
    string += current.data + " ";
    current = current.next;
  }

  return string;
}

// 4 forwardString方法 -- 从后往前
DoublyLinkedList.prototype.forwardString = function(){
  let current = this.tail;
  let string = '';
  // 依次向前遍历
  while(current){
    string += current.data + " ";
    current = current.prev;
  }
  return string;
}

// 5 toStirng方法
DoublyLinkedList.prototype.toString = function(){
  return this.backwardString();
}

insert实现 -- 指定位置插入

这个相比于单链表,要考虑更多的指针指向,更多的情况(多了一个tail指针),但是利用好tail、head,可以简化代码!还有就是要考虑指针的变动,如果变动了而使后面的无法找到就要改变顺序!

代码

// 6 insert方法
DoublyLinkedList.prototype.insert = function(position,data){
  // 1 越界判断
  if(position < 0 || position > this.length) return false;

  // 创建节点
  let newNode = new Node(data);

  // 判断原来的链表是否为空
  if(this.length === 0){
    this.head = newNode;
    this.tail = newNode;
  }else{
    // 判断position是否为0
    if(position === 0){
      newNode.next = this.head;
      // 写单链表多了就容易忘记这一步
      this.head.prev = newNode; 
      this.head = newNode;
    }else if(position === this.length){
      // 这一步和单向链表不一样,不能和插入中间归为一类,因为tail
      // 也不需要调用this.append,重复了一部分操作
      newNode.prev = this.tail;
      this.tail.next =newNode;
      this.tail = newNode;
    }else{
      let current = this.head;
      let index = 0;
      while(index++ < position){
        current = current.next;
      }
      // 修改指针 -- 4个 -- 第二和第四的顺序不能乱了(其它无所谓),不然会因为指向改变而出错
      newNode.next = current;
      current.prev.next = newNode;
      newNode.prev = current.prev;
      current.prev = newNode;
    }
  }

  this.length += 1;
  return true;
}

get实现 -- 获取对应位置元素

思路和单链表一摸一样!

代码

// 7 get方法 -- 效率不高,可以采用平分,毕竟有一个尾结点!
DoublyLinkedList.prototype.get = function(position){
  if(position < 0 || position >= this.length) return null;
  let current = this.head;
  let index = 0;
  while(index++ < position){
    current = current.next;
  }
  return current.data;
}

这里平分的代码菜鸟就不写了,思路就是把position和length/2进行比较,大于就用tail,小于就用head!

indexOf实现 -- 判断是否含有元素

这个也和单链表思路一模一样!

代码

DoublyLinkedList.prototype.indexOf = function(data){
  let current = this.head;
  let index = 0;
  while(current){
    if(current.data === data){
      return index;
    }
    current = current.next;
    index++;
  }
  return -1;
}

update实现 -- 更改某一位置的值

这里菜鸟使用的是平分的思路,上面get没用平分写出来的读者,可以借鉴一下这里!

代码

// 9 update方法
DoublyLinkedList.prototype.update = function(position,data){
  if(position < 0 || position >= this.length) return false;
  if(position < this.length/2){
    let current = this.head;
    let index = 0;
    while(index++ < position){
      current = current.next;
    }
    current.data = data;
  }else{
    let current2 = this.tail;
    // 注意:index等于length-1,因为length是长度,而下标是从0开始,所以index最大只能是length-1
    let index = this.length - 1;
    while(index-- > position){
      current2 = current2.prev;
    }
    current2.data = data;
  }
  return true;
}

removeAt实现 -- 移除某位置的元素

这里情况有点多,eg:

  1. 只有一个节点
  2. 移除的是第二个节点
  3. 移除的是最后一个节点
  4. 移除中间节点

2、3之所以有,是因为要改变head、tail的指向!

菜鸟一开始自己写代码没有考虑到这么多情况,然后就是看视频的时候想到了,就用自己的思路加上视频的情况写了。

代码

// 11 removeAt1 视频方法
DoublyLinkedList.prototype.removeAt1 = function(position){
  if(position < 0 || position >= this.length) return false;
  // 判断是否只有一个节点
  if(this.length === 1){
    this.head = null;
    this.tail = null;
  }else {
    if(position === 0){ //判断删除的是否是第一个节点
      this.head.next.prev = null;
      this.head = this.head.next;
    }else if(position === this.length - 1){ // 判断删除的是否是最后一个节点
      this.tail.prev.next = null;
      this.tail = this.tail.prev;
    }else{
      // 平分思路
      if(position < this.length/2){
        let current = this.head;
        let index = 0;
        while(index++ < position - 1){
          current = current.next;
        }
        current.next.neaxt.prev = current;
        current.next = current.next.next;
      }else{
        let current = this.tail;
        let index = this.length - 1;
        while(index-- > position+1){
          current = current.prev;
        }
        current.prev.prev.next = current;
        current.prev =current.prev.prev;
      }
    }
  }

  this.length -= 1;
  return true;
}

菜鸟这里和视频思路不一样,保存的是要删除的节点的前一个或后一个,而不是删除的节点本身,所以返回不了值,但是感觉视频的更好(保存删除的节点),因为双向链表不像单向链表,需要保存两个节点信息,因为其既可以访问前节点也可以访问后节点!

视频代码

在这里插入图片描述

remove实现 -- 移除某值

这里直接调用两个方法就行,菜鸟就不写了

在这里插入图片描述

其它方法

在这里插入图片描述

总结

其实不难发现,单向链表和双向链表的难点都在于 insert 和 removeAt 方法,只要搞明白这两个方法的一些特殊情况,其实写起来也不是特别困难了;双向链表的其它操作真的和单向链表没什么很大的区别,有的甚至因为有了tail指针而更加简单