前端必须要掌握的数据结构 — 链表

181 阅读3分钟

一、链表介绍

维基百科介绍:

计算机科学中,链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)。

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。

二、优缺点

优点:

  1. 存储和删除效率非常高。数组的删除和插入对后面元素有很大影响,需要重新排列。
  2. 与数组相比,可以动态的扩容

缺点:

  1. 不能像数组那样根据下标值取数据,必须要重头遍历
  2. 存储空间比数组大

三、实现单向链表

首先,我们定义一个节点类

// 节点类
class Node {
  constructor(val, next) {
    this.val = val;
    this.next = next;
  }
}

接着定义一个单向链表类

class LinkNode {
  constructor() {
    this.length = 0;
    this.head = null;
  }
}

接着我们只需要往链表类里添加链表常用的方法就可以了。

1. 添加节点

append(data) {
   // 创建节点
  const node = new Node(data);
  // 如果链表为空
  if (this.length === 0) {
    this.head = node;
  } else {
    // 链表不为空
    let current = this.head;
    // 取到最后的节点
    while (current.next) {
      current = current.next;
    }
    current.next = node;
  }
  this.length += 1;
}

2. 字符串化输出

遍历输出链表

toString() {
  let string = "";
  let current = this.head;
  while (current) {
    string += current.val + " ";
    current = current.next;
  }
  return string;
}

3. 插入节点

插入节点相对来说会复杂一点。我们从以下这几个方面考虑

  • 插入的位置是头节点
  • 插入的节点是尾结点
  • 插入的节点是中间节点
insert(position, val) {
  // 校验要插入的位置
  if (position < 0 || position > this.length) {
    return false;
  }

  const node = new Node(val);
  // 链表为空
  if (!this.head) {
    this.head = node;
  } else {
    // 链表不为空
    // 1.position = 0
    if (position === 0) {
      node.next = this.head;
      this.head = node;
    } else {
      // 2. 后面的任意位置
      let current = this.head;
      let previous = null;
      let index = 0;
      // 找到目标位置和目标位置的前一个元素
      while (index < position) {
        previous = current;
        current = current.next;
        index++;
      }
      // 插入
      node.next = current;
      previous.next = node;
      this.length += 1;
    }
  }
}

4. 获取指定位置节点

get(position) {
  if (position < 0 || position >= this.length) {
    return false;
  }

  let current = this.head;
  let index = 0;
  while (index < position) {
    current = current.next;
    index += 1;
  }
  return current.val;
}

5. 更新节点

update(position, val) {
  if (position < 0 || position >= this.length) {
    return false;
  }

  let current = this.head;
  let index = 0;
  while (index < position) {
    current = current.next;
    index += 1;
  }
  current.val = val;
}

6. 根据索引删除节点

我们只需要思考一下这些问题

  • 删除的是头结点
  • 删除的是后面的节点
  • 只需要把要删除的节点的next节点指向要删除节点的next节点
removeAt(position) {
  if (position < 0 || position >= this.length) {
    return false;
  }
  let current = this.head;
  let previous = null;
  let index = 0;
  if (position === 0) {
    this.head = this.head.next;
  } else {
    while (index < position) {
      previous = current;
      current = current.next;
      index += 1;
    }
    // 移除
    previous.next = current.next;
    this.length -= 1;
  }

  return current.val;
}

7. 根据值删除节点

remove(val) {
  let current = this.head;
  let previous = null;
  while (current) {
    if (current.val === val) {
      this.length -= 1;
      // 如果删除的不是第一个元素
      if (previous) {
        previous.next = current.next;
      } else {
        // 删除的是第一个元素
        this.head = current.next;
      }

      return true;
    }
    previous = current;
    current = current.next;
  }
  return false;
}

四、实现双向链表

对于双向链表,我们需要改造一下我们的类

  • 新增前指向
  • 新增尾指针
class Node {
  constructor(val) {
    this.val = val;
    this.next = null;
    this.prev = null;
  }
}
class LinksNode {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }
}

至于方法来说,相对于单向链表更加灵活了,但是思维逻辑要复杂一点。我们来看一下功能方法的实现过程。

1. 添加节点

多了尾指针和前一个节点的指向

append(val) {
  const node = new Node(val);
  // 链表为空
  if (this.length === 0) {
    this.head = node;
    this.tail = node;
  } else {
    this.tail.next = node;
    node.prev = this.tail;
    this.tail = node;
  }
  // 链表长度+1
  this.length += 1;
}

2. 字符串化输出

没有改变,就不展示代码了

3. 插入节点

多了尾指针和前一个节点的指向

append(val) {
  const node = new Node(val);
  // 链表为空
  if (this.length === 0) {
    this.head = node;
    this.tail = node;
  } else {
    this.tail.next = node;
    node.prev = this.tail;
    this.tail = node;
  }
  // 链表长度+1
  this.length += 1;
}

4. 获取指定位置节点

没有改变,就不展示代码了

5. 更新节点

没有改变,就不展示代码了

6. 根据索引删除节点

我们还是需要思考一下这些问题

  • 删除的是头结点
  • 删除的是中间的节点
  • 删除的是尾结点
removeAt(position) {
  if (position < 0 || position >= this.length) {
    return false;
  }
  let current = this.head;
  let index = 0;
  // 当删除的是第一个元素时
  if (position === 0) {
    this.head = current.next;
    current.next.prev = null;
  } else if (position === this.length - 1) {
    // 删除的是尾结点
    while (index < position) {
      current = current.next;
      index++;
    }
    this.tail = current.prev;
    current.prev.next = null;
    this.tail.next = null;
  } else {
   // 删除的中间节点
    while (index < position) {
      console.log(current);
      current = current.next;
      index++;
    }
    // 存储上一个元素
    const previous = current.prev;
    // 删除
    previous.next = current.next;
    current.next.prev = previous;
  }

  this.length -= 1;
  return true;
}

7. 根据值删除节点

和上面删除要考虑的方面一致

remove(val) {
  let current = this.head;
  let index = 0;
  while (current) {
    if (current.val === val) {
      // 头结点
      if (current === this.head) {
        this.head = current.next;
        current.next.prev = null;
        return true;
      } else if (current === this.tail) {
        // 尾节点
        this.tail = current.prev;
        current.prev.next = null;
        this.tail.next = null;
        return true;
      } else {
        // 存储上一个元素
        const previous = current.prev;
        // 删除
        previous.next = current.next;
        current.next.prev = previous;
        return true;
      }
    }
    current = current.next;
  }

  return false;
}

8. 前序字符串输出

从头节点开始遍历

  forwardString() {
    let str = "";
    let current = this.head;
    while (current) {
      str += current.val + " ";
      current = current.next;
    }
    return str;
  }

9. 后续序字符串输出

从尾结点开始遍历

backwardString() {
    let str = "";
    let current = this.tail;
    while (current) {
      str += current.val + " ";
      current = current.prev;
    }
    return str;
  }