数据结构-链表

363 阅读5分钟

链表是一种有序的数据结构,不同于数组,链表从起点或者中间插入或移动项的成本很小。因为链表是使用指针指向下一个元素。每个元素存储元素的本身节点和指向下一个元素的引用。相对于传统的数组,链表需要使用指针,在数组中,我们可以直接访问位置的任何元素,而想要访问链表中间的一个元素,则需要从(表头)开始迭代链表,知道找到所需的元素。

30511572a171.png

创建链表类

由于js没有天生提供链表的类,这里我们用数组快速生成链表。生成链表,我们提供一个基础的createNode方法,用于生成链表节点。利用count存储链表的长度,利用head存放链表(表头)。

class NodeList {
  constructor(arr) {
    if (arr.length) {
      this.head = this.createNode(arr[0]);
      let p = this.head;
      this.count = arr.length;
      for (let i = 1; i < arr.length; i++) {
        p.next = this.createNode(arr[i]);
        p = p.next;
      }
    } else {
      this.head = null;
      this.count = 0;
    }
  }

  createNode(element, next) {
    return next ? { val: element, next } : { val: element, next: null };
  }
}

遍历链表

不同于数组,我们遍历数组有现成的方法,例如map、forEach,对于链表,我们需要使用指针遍历。

对于以下链表:

const {head} = new NodeList(['a','b','c','d','e']);

遍历操作如下:


let p = head;

while(p) {
    console.log(p.val);
    p = p.next;
}

移除链表中的元素

对于移除链表中的元素,我们提供一个参数index。参考数组中移除行为,我们将移除的链表元素返回出去。

  1. 移除链表分为两种情况,一种情况是移除表头第一个元素。 这样我们可以直接让head赋值上head的next。这样就直接删除了表头的第一个元素。
  2. 移除链表的第二种情况,就是删除中间或者后续的元素。这块我们也是一样的思路,利用一个指针prev存储curr的上一个元素。 然后用将上一个元素的next指向curr的next。这样,curr元素就从链表当中断开了。

class NodeList {
  removeNode(index) {
    if (index >= 0 && index < this.count) {
      let curr = this.head;
      if (index === 0) {
        this.head = curr.next;
      } else {
        let prev;
        for (let i = 0; i < index; i++) {
          prev = curr;
          curr = curr.next;
        }
        prev.next = curr.next;
      }
      this.count -= 1;
      return curr.val;
    } else {
      return undefined;
    }
  }
}

链表元素的删除,基本上是需要获得上一个链表元素,然后将上一个链表元素指向下一个链表元素的next的。

算法题巩固

删除链表中的节点leetCode 237

image.png 链接:leetcode-cn.com/problems/de…

代码思路:

因为是非末尾节点, 且直接给定我们一个节点。 要我们删除元素。 这里边,因为我们删除节点通常都是需要节点的上一个节点的。 但是,这里只给出了当前节点,这里我们就没办法获取上一个节点了。 我们可以换一个思路。 就是将当前节点变成下一个节点, 然后再删除下一个节点。达到目的。

  1. 将当前节点变成下一个节点的值。
  2. 删除下一个节点。
/**
 * @param {ListNode} node
 * @return {void} Do not return anything, modify node in-place instead.
 */
var deleteNode = function(node) {
    node.val = node.next.val;
    node.next = node.next.next;
};

反转链表leetCode206

image.png

链接:leetcode-cn.com/problems/re…

代码思路:

反转链表的思路很简单,我们只需要用双指针,不断的将curr.next指向prev就行。而且,因为我们反转链表的开始节点其实是原来的结束节点。所以,我们需要将prev指针先设置成null。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let prev = null
    let curr = head;
    while(curr) {
        const next = curr.next;
        curr.next = prev; // 因为此处需要更改curr,但是prev又要指向原有的curr去移动指针,所以,我们需要先将curr去存储起来。
        prev = curr;
        curr = next;
    }
    return prev;
};

两数相加

image.png 链接:leetcode-cn.com/problems/re…

代码思路:

因为得到的是两个倒序的链表,返回值也是倒序的链表,所以,我们刚好可以像基础数学的相加一样进行链表相加操作,对每一位进行相加,如果有进位就将其存储到carry当中。下一位相加时,加上carry即可。

  1. 用两个指针遍历链表。
  2. 只要有一个链表没有遍历完成,那么就得继续进行相加操作。如果链表一长一短,则短的那个链表取的节点值为0进行相加操作。并加上carry
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
 
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    let p1 = l1;
    let p2 = l2;
    let resultList = new ListNode(0);
    let p3 = resultList;
    while(p1 || p2 || carry){ 
    // 因为当两个链表都加完的时候,如果有进位,我们还需要加上一位,所以这里将carry也加入到while条件中
        const num1 = p1 && p1.val || 0;
        const num2 = p2 && p2.val || 0;
        let carry = 0;
        const res = (num1 + num2 + carry) % 10; // 得到当前位的值
        carry = Math.floor((num1 + num2 + carry) / 10); // 得到进位的值
        p3.next = new ListNode(res); // 生成赋值节点
        p3 = p3.next; // 控制结果链表的指针指向下一个节点
        if (p1) { p1 = p1.next} // 控制链表节点走向下一个节点
        if (p2) { p2 = p2.next}
    }
    return resultList.next;
};

删除排序链表中的重复元素 leetCode 83

image.png

链接: leetcode-cn.com/problems/re…

代码思路:

因为是排序链表,所以如果元素重复了。那么下一个元素肯定等于当前节点的元素。所以,是否删除下一个节点的依据就是下一个元素是否相等。然后我们可以控制指针继续往下移动。

注意点:

如果多个重复节点的出现,可能会漏删元素。 例如[1,1,1]在这种情况下,我们的指针就先不要移动,继续删除一个节点。再进行移动。

代码实现:

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    let p = head;
    
    while(p && p.next) {
        if (p.val === p.next.val) {
            p.next = p.next.next;
        } else {
            p = p.next;
        }
    }
    return head;
};

环形链表 leetCode 141

image.png

链接: leetcode-cn.com/problems/li…

代码思路:

这里我们可以从生活中的一些现象中去进行考虑。 例如,如果两个赛车存在速度差。那么在一个环形跑道中就一定会重新相遇。 即超过一圈。 那么,只要链表中两个指针能够重合,那么就证明是"环形跑道"。如果链表能遍历完成,则一定不是环形跑道。

c0e01eaed3bd44aaa6ffe582358c8fe0.gif

  1. 定义一个慢指针,每次步进1个元素。
  2. 定义一个快指针,每次步进2个元素。
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    let p1 = head;
    let p2 = head;
    
    while(p1 && p2 && p2.next) { 
    // 因为我们需要一个快指针,快指针是步进2个元素的,为了防止链表不够长度报错,所以必须存在p2.next
        p1 = p1.next;
        p2 = p2.next.next;
        if (p1 === p2) {
            return true;
        }
    }
    return false; // 链表能正常遍历完成,则肯定不是环形链表
};