温故知新,数据结构链表之单链表

180 阅读5分钟

温故知新,程序猿打野基本功之数据结构~

开局一张图,其余全靠编😄。此篇的核心话题即是:链表中的单链表。

image.png

基础

链表,一种线性数据结构。如下图所示,每个元素都是一个单独的对象,所有的元素是通过引用将彼此串联起来的。

image.png 链表有两种结构,一种是单链表,如上图所示;另一种是双链表,如下图所示:

image.png 大多数情况下,我们都是使用头节点(即第一个节点)来表示整个链表。

注意⚠️:相比于数组,链表最大的特点就是其在内存上的存储结构不一定是连续的。

单链表

结构

如最开始的那张图所示,链表的每一个元素都是一个简单的对象,其包括两个部分,一个是当前值value,另一个是指向下一个引用的next指针。通过next指针,我们可以从头节点很顺利的查找到整个数据链条上的每一个元素。

其结构的简单定义如下:

  constructor(value,next){    
    this.value = value?value:0;    
    this.next = next?next:null;  
  }}

操作

查找(遍历)

由于链表在内存单元上的存储特殊性,我们无法在常量时间内访问任意指定的元素,只能从头到尾依次遍历进行查找。按索引访问元素,其时间复杂度为O(N)。

插入

与数组不同,我们不需要在插入一个元素后再逐次调整其后的元素,只需要调整指针索引即可,其时间复杂度仅为O(1)。譬如,我们在prev和next两个节点之间插入一个新的节点cur,其操作过程简单如下:

  • 初始化新节点cur
  • 将cur的next指针指向next节点

image.png

  • 将prev的next指针指向cur节点

image.png

边界

首尾节点的插入稍微有点特殊。

  • 头部添加新节点

通常情况下,采用头节点来代表整个链表。因此,在添加新节点到首部时,需要注意head指针。简单步骤如下:

  1. 初始化节点cur
  2. 将cur节点的next指针指向原始的头节点head
  3. 将cur节点指定为新的头节点head

简单实例,在列表开头添加一个新节点9,如下图:

初始化value值为9的新节点;将其next指针指向原始的头节点head,也就是当前value值为23的节点。

image.png 将value值为9的新节点指定为新的头节点。

image.png

  • 尾部添加新节点

    1. 初始化一个新节点cur
    2. 遍历整个链表,直到链表尾部
    3. 将链表尾部元素的next指针指向cur元素

示例:将新建节点9插入链表尾部

  1. 遍历链表,找到尾节点

image.png

  1. 修改尾节点的next指针的指向,使其指向新节点9

image.png

简单的讲,插入的要点是让新加入的节点有所指向。

\

删除

从链表中删除现有节点cur,可以简单的分两步走:

  1. 找到cur节点的上一个节点prev和下一个节点next

image.png

  1. 将prev的next指针指向next节点。

image.png 在基于已知节点cur寻找prev和next节点的过程中,next无疑是最好找的,直接通过cur节点的next指针即可找到。但是查找prev却只能从头遍历链表进行查找,其平均时间是O(N),N为链表的长度。其只需要一个常量大小的空间来存储指针,故空间复杂度为O(1)。综上,删除一个链表节点的时间复杂度是O(N),空间复杂度为O(1).

边界

删除同样需要考虑边界情形:头部、尾部。

  • 首部删除较为简单,即将头节点head指向第二个节点,这样依照我们用头节点来表示一个链表的规范,我们就做到了删除一个首部节点。示例如下:从头部删除头节点9

image.png

  • 尾部节点删除比较麻烦。大致思路如下:

    1. 首先我们要遍历整个链表,确认倒数第二个和最后一个节点
    2. 将倒数第二个节点的next指针指向null,将尾节点指向倒数第二个节点。

示例如下:删除尾节点6

我们需要遍历查找,找到原始尾节点和倒数第二个节点,也就是下图的23和6两个所在的节点

image.png

修改倒数第二个节点的next指针指向,使其指向null。

image.png

设计实现

以上为纯理论与简单图示,接下来基于javascript实现一个单链表,要求如下:

假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

我太菜,实现的一个普通版本😂:

 * Initialize your data structure here.
 */
var MyLinkedList = function () {
  this.head = null;
  this.length = 0;//必要,向指定索引插入或者删除时需要
};

/**
 * Get the value of the index-th node in the linked list. If the index is invalid, return -1.
 * @param {number} index
 * @return {number}
 */
MyLinkedList.prototype.get = function (index) {
  if (index < 0 || index >= this.length) return -1;
  let current = this.head,
    count = 0;
  if (index === 0) return current.val;
  else {
    while (count < index) {
      current = current.next;
      count++;
    }
    return current.val;
  }
};

/**
 * Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtHead = function (val) {
    //判断头节点是否存在
  if (!this.head) this.head = { val, next: null };
  else {
    //若存在头节点
    let node = { val, next: this.head };
    this.head = node;//修改头指针head指向
  }
  this.length++;
};

/**
 * Append a node of value val to the last element of the linked list.
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtTail = function (val) {
  let node = { val, next: null };//尾节点的next指针指向null
  if (!this.head) this.head = node;
  else {
    let current = this.head;
    //遍历到尾部进行添加
    while (current.next) {
      //当current.next为null说明current就是最后一个节点
      current = current.next;
    }
    current.next = node;
  }
  this.length++;
};

/**
 * Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
 * @param {number} index
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtIndex = function (index, val) {
  if (index > this.length) return; //边界情况,超出链表长度直接return,不执行插入
  let node = { val, next: null };
  let current = this.head,
    previous,
    count = 0;
  if (index <= 0) {//边界:头部插入
    return this.addAtHead(val); 
  } else if (index === this.length) {//边界:在末尾进行追加
    return this.addAtTail(val); 
  } else {
    while (count <= index - 1) {
      //找到index位置的前一个节点
      previous = current;
      current = current.next;
      count++;
    }
    previous.next = node;
    node.next = current;
  }
  this.length++;
};

/**
 * Delete the index-th node in the linked list, if the index is valid.
 * @param {number} index
 * @return {void}
 */
MyLinkedList.prototype.deleteAtIndex = function (index) {
  if (index < 0 || index >= this.length) return;//处理边界
  let current = this.head,
    previous,
    countIndex = 0;
  if (index === 0) this.head = current.next;
  //删除头部的情况
  else {
    while (countIndex++ < index) {
      //找到删除位置的前一个节点
      previous = current;
      current = current.next;
    }
    if (current) {
      //直接链接到删除位置的下一个节点
      previous.next = current.next;
    } else {
      previous.next = null;
    }
  }
  this.length--;
};

注意

  • 头节点的指针很重要,尤其是涉及到操作头节点位置的操作,比如头部插入、头部删除,节点调整后要及时更新头指针head的指向。
  • 设计单链表的时候,需要初始化一个length来度量操作时候的count,链表虽然有index的说辞,但是鉴于链表的在存储空间上是(大概率)不连续的,真正起作用的还是每个节点的next指针。 索引index更像是为了直观计量而存在的辅助。

参考资料:

[力扣]  leetcode-cn.com/leetbook/re… 力扣

算法(第四版)