算法随笔-数据结构(链表)

192 阅读7分钟

算法随笔-数据结构(链表)

本文主要介绍数据结构中的链表的特点、使用场景、ES6 实现 LinkedList 类和题解 leetCode 真题。供自己以后查漏补缺,也欢迎同道朋友交流学习。

引言

链表是一件很常见的数据结构,生活中也有很多应用场景,比如:

  • 交通路线:城市中的交通路线,如公交线路,可以看作是一个链表,每个站点是一个节点,按照路线顺序连接。
  • 家族家谱:从祖先到子孙的家族树也是一种链表结构,一代一代延续。
  • 审批流程:我们平时请假填的请假单也是一种链表结构,每个节点代表一个审批人,按照顺序连接。

对于前端来说,学习链表这一基本数据结构,对理解更复杂的会更有帮助。

链表的主要特点

链表是一种基本的数据结构,具有以下主要特点:

  • 灵活的结构:链表的元素可以不连续的存储在某一个地址空间中,可以动态的增删元素,改变大小,不需要预先分配内存。
  • 指针链接:每个节点通常包含两个部分:一个是存储数据的元素,另一个是指向列表中下一个节点的指针(在双向链表中,可能还包括指向前一个节点的指针)。
  • 头部引用:链表通过一个称为头指针或头节点的引用开始,它指向链表的第一个节点。
  • 尾部无指针:在单向链表中,最后一个节点的指针指向null,表示链表的结束。
  • 插入和删除操作:链表允许在任意位置快速插入和删除节点,只需改变相邻节点的指针即可,无需移动其他元素。

链表的应用场景

链表在计算机科学和软件开发中有多种应用场景,以下是一些常见的例子:

  1. 数据库索引:数据库管理系统使用链表来实现索引,如B树哈希表,以提高数据检索效率。

  2. 网络数据包处理:网络协议栈中,链表用于管理数据包的队列,特别是在 TCP/IP 协议中。

  3. 事件处理系统:事件驱动的编程模型中,链表用于存储待处理的事件队列

  4. 游戏开发:在游戏开发中,链表可以用于管理游戏对象,如敌人、子弹等。

  5. 社交网络服务:社交网络中,链表可以用于维护用户的好友列表社交图谱

  6. 图形编辑器:在图形编辑器中,链表可以用于管理图形元素,如节点和边。

  7. 文本编辑器:在文本编辑器中,链表可以用于实现撤销/重做功能。

链表因其动态性灵活性,在需要频繁插入和删除操作的场景中特别有用。然而,它们可能不适用于需要快速随机访问数据的情况,此时数组或其他数据结构可能是更好的选择。

JS链表简单实现

在 JS 中用next一个个链接对象,就实现了一个简单的链表:

var a = {key: 'a'}
var b = {key: 'b'}
var c = {key: 'c'}

a.next = b
b.next = c
c.next = null

// 遍历链表
var obj = a;
while(obj && obj.key) {
  console.log(obj.key)
  obj = obj.next
}

// 插入链表
let m = {key: 'm'}
c.next = m
m.next = d

// 删除操作
c.next = d;

// 双向链表
// 使用prev 和 next 两个指针,可以方便的实现链表的插入和删除操作

我们JS的原型链也是这种数据结构,我们可以模拟 instanceOf 这个实现:

const MyInstanceOf = function (target, obj) {
  let proto = Object.getPrototypeOf(target)

  while (proto) {
    if (proto === obj.prototype) {
      return true
    }
    proto = Object.getPrototypeOf(proto)
  }

  return false
}

ES6实现单向链表(LinkedList)

ES6 中实现链表,我们可以实现链表中的节点链表本身。链表是一种线性数据结构,其中元素以节点的形式存在,每个节点包含数据部分和指向下一个节点的指针。

下面是一个简单的单向链表实现的例子:

// 实现链表中的节点
class ListNode {
  constructor(value) {
    this.value = value;
    this.next = null;
  }
}

// 实现链表本身
class LinkedList {
  constructor() {
    // 节点
    this.head = null;
    // 链表长度
    this.size = 0;
  }

  // 向链表尾部添加元素
  append(value) {
    let newNode = new ListNode(value);
    if (!this.head) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = newNode;
    }
    this.size++;
  }

  // 根据索引获取链表中的元素
  get(index) {
    let current = this.head;
    let count = 0;
    while (current) {
      if (count === index) {
        return current.value;
      }
      count++;
      current = current.next;
    }
    return null; // 如果索引超出范围
  }

  // 根据值查找元素的索引,如果不存在返回-1
  indexOf(value) {
    let current = this.head;
    let count = 0;
    while (current) {
      if (current.value === value) {
        return count;
      }
      count++;
      current = current.next;
    }
    return -1;
  }

  // 打印链表
  print() {
    let current = this.head;
    while (current) {
      process.stdout.write(current.value + ' -> ');
      current = current.next;
    }
    process.stdout.write('null\n');
  }
}

// 使用示例
let list = new LinkedList();
list.append(1);
list.append(2);
list.append(3);
list.print(); // 1 -> 2 -> 3 -> null
console.log(list.get(1)); // 输出 2
console.log(list.indexOf(3)); // 输出 2

这个例子中,我们定义了ListNode类来表示链表的节点,每个节点包含一个值和一个指向下一个节点的引用。LinkedList类表示链表本身,包含头节点和链表的大小。我们实现了一些基本的操作,如添加元素、获取元素、查找元素的索引和打印链表。

LeetCode真题

141. 环形链表

给你一个链表的头节点 head,判断链表中是否有环

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。

如果链表中存在,则返回 true。 否则,返回 false

示例 1:

示例1

输入:head = [3,2,0,-4], pos = 1

输出:true

解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

示例2

输入:head = [1,2], pos = 0

输出:true

解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

示例3

输入:head = [1], pos = -1

输出:false

解释:链表中没有环。

题解

我们可以使用快慢指针,快指针每次走两步,慢指针每次走一步,如果快慢指针相遇,说明有环,否则没有环。

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
  let fast = head, slow = head;

  while(fast && fast.next) {
    fast = fast.next.next;
    slow = slow.next;

    if(f === slow) return true;
  }

  return false;
};

当然我们还有另一种结题思路,就是标记法,走个一遍的设置flag,如果遍历查到有flag,说明有环,否则没环。

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
  while(head) {
    if (head.flag) {
      return true;
    }
    head.flag = true
    head = head.next
  }
  return false;
};

237. 删除链表中的节点

有一个单链表的 head,我们想删除它其中的一个节点 node

给你一个需要删除的节点 node。你将 无法访问 第一个节点 head

链表的所有值都是 唯一的,并且保证给定的节点 node 不是链表中的最后一个节点。

删除给定的节点。注意,删除节点并不是指从内存中删除它。

示例 1:

示例1

输入:head = [4,5,1,9], node = 5

输出:[4,1,9]

解释:指定链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9

题解

我们可以改变 node 的指向,让他 valnext 都指向 node.next

/**
 * @param {ListNode} node
 * @return {void} 
 */
var deleteNode = function(node) {
  node.val = node.next.val
  node.next = node.next.next
};

206. 反转链表

给你单链表的头节点 head,请你反转链表,并返回反转后的链表。

示例 1:

示例1

输入:head = [1,2,3,4,5]

输出:[5,4,3,2,1]

题解

我们可以设置 prevcurrent 去保存链表指向,循环的设置到 prev 上:

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
  let prev = null;
  let current = head;

  // 循环current
  while(current) {
    // 缓存下当前节点
    const node = current.next;
    // 设置当前下一个节点为 prev
    current.next = prev;
    // 设置 prev 为 当前节点
    prev = current;
    // current赋值为当前节点
    current = node;
  }
  // [1,2,3,4,5]
  // 第一次循环: current -> [2,3,4,5]; current.next -> null; prev -> [1]; 
  // 第二次循环: current -> [3,4,5]; current.next -> [1]; prev -> [2,1]; 
  // 第三次循环: current -> [4,5]; current.next -> [2,1]; prev -> [3,2,1]; 
  // ...

  return prev
};