数据结构与算法-链表
《学习JavaScript数据结构与算法(第3版)》
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。
链表数据结构(单向链表)
链表的第一个节点为头部,链表最后一个节点的下一个元素始终是undefined或null。
// 比较链表中的元素是否相等
function defaultEquals(a, b) {
return a === b;
}
// 节点类
class Node {
constructor(element, next) {
this.element = element; // 表示要加入链表元素的值
this.next = next; // 指向链表中下一个元素的指针
}
}
class LinkedList {
constructor(equalsFn = defaultEquals) {
this.equalsFn = equalsFn;
this.count = 0; // 存储链表中的元素数量
this.head = undefined; // 保存引用
}
// 向链表尾部添加一个新元素
push(element) {
const node = new Node(element); // 创建节点类
let current; // 插入位
if (this.head == null) {
// catches null && undefined
this.head = node;
} else {
current = this.head; // 默认初始插入位为头部
while (current.next != null) { // 一直找到链表的最后一项
current = current.next;
}
// 将插入位的next赋为新元素,建立链接
current.next = node;
}
this.count++; // 增加链表长度
}
// 返回链表中特定位置的元素。如果链表中不存在这样的元素,则返回undefined
getElementAt(index) {
// 检查越界值
if (index >= 0 && index <= this.count) {
// 从头开始循环
let node = this.head;
for (let i = 0; i < index && node != null; i++) {
node = node.next;
}
return node;
}
return undefined;
}
// 向链表的特定位置插入一个新元素
insert(element, index) {
// 检查越界值
if (index >= 0 && index <= this.count) {
const node = new Node(element);
if (index === 0) {
// 在头部添加
const current = this.head;
// 将新插入的元素下一个节点指向原链表的head
node.next = current;
// 替换链表的head
this.head = node;
} else {
// 查找到目标位的前一个元素
const previous = this.getElementAt(index - 1);
// 替换前一个元素的下一个引用 从而插入元素 形成链接
node.next = previous.next;
previous.next = node;
}
// 链表长度增加
this.count++;
return true;
}
// 越界了就无法添加到链表中
return false;
}
// 从链表的特定位置移除一个元素
removeAt(index) {
// 检查越界值
if (index >= 0 && index < this.count) {
let current = this.head; // 默认移除点为头部
if (index === 0) { // 移除第一项(头部)
this.head = current.next;
} else {
// 查找到需要移除的位置前一项 解除链接关系
const previous = this.getElementAt(index - 1);
// 循环列表的当前元素引用 移除点
current = previous.next;
// 将previous与current的下一项链接起来:跳过current,从而实现移除
previous.next = current.next;
}
// 链表长度减少
this.count--;
// 返回被移除项的值
return current.element;
}
// 未有移除项
return undefined;
}
// 从链表中移除一个元素 先找到索引值后根据索引值删除元素
remove(element) {
const index = this.indexOf(element);
return this.removeAt(index);
}
// 能够在链表中找到一个特定的元素
indexOf(element) {
let current = this.head;
// 为了运行不会发生错误,需要验证一下current的有效性
for (let i = 0; i < this.size() && current != null; i++) {
if (this.equalsFn(element, current.element)) {
// 返回查找的索引值
return i;
}
current = current.next;
}
// 没有找到返回-1
return -1;
}
// 如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false
isEmpty() {
return this.size() === 0;
}
// 返回链表包含的元素个数
size() {
return this.count;
}
// 返回链表第一个元素
getHead() {
return this.head;
}
// 清空链表
clear() {
this.head = undefined;
this.count = 0;
}
// 返回表示整个链表的字符串。由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值
toString() {
if (this.head == null) {
return '';
}
let objString = `${this.head.element}`;
let current = this.head.next;
for (let i = 1; i < this.size() && current != null; i++) {
objString = `${objString},${current.element}`;
current = current.next;
}
return objString;
}
}
双向链表
双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接;而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素。
双向链表提供了两种迭代的方法:从头到尾,或者从尾到头。可以根据位置确认从头遍历还是从尾部遍历。可以优化单向列表通过索引查找元素的方法。
// 比较链表中的元素是否相等
function defaultEquals(a, b) {
return a === b;
}
// 双向链表节点
class DoublyNode extends Node {
constructor(element, next, prev) {
super(element, next);
this.prev = prev; // 指向上一个节点
}
}
// 扩展普通链表类 继承LinkedList
class DoublyLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn);
this.tail = undefined; // 尾部 保存对链表的最后一个元素的引用
}
push(element) {
const node = new DoublyNode(element);
if (this.head == null) {
// 如果没有头部元素,则表示链表为空
// 插入的节点为头部元素同时为尾部元素
this.head = node;
this.tail = node;
} else {
// 链表不为空,则从尾部插入元素
this.tail.next = node; // 原尾部元素next指向待插入点
node.prev = this.tail; // 待插入点prev指向原尾部元素
this.tail = node; // 待插入点替换尾部元素
}
// 链表长度增加
this.count++;
}
insert(element, index) {
// 检查边界值
if (index >= 0 && index <= this.count) {
const node = new DoublyNode(element);
let current = this.head;
if (index === 0) {
// 在链表头部插入元素
if (this.head == null) {
// 如果没有头部 则表示当前链表长度为0 头部元素和尾部元素都是待插入节点
this.head = node;
this.tail = node;
} else {
// 替换链表头部元素,且原头部元素上一个节点指向待插入节点
node.next = this.head;
this.head.prev = node;
this.head = node;
}
} else if (index === this.count) {
// 在链表尾部插入元素
// 替换链表尾部元素,原尾部元素next指向为null/undefined,改为指向插入节点
current = this.tail;
current.next = node;
node.prev = current;
this.tail = node;
} else {
// 查找到插入点的前一个元素
const previous = this.getElementAt(index - 1);
current = previous.next; // 待插入节点位置
node.next = current; // 待插入节点next指向替换previous.next指向
previous.next = node; // previous节点建立与待插入节点的链接
current.prev = node; // current节点与待插入节点建立prev链接关系
node.prev = previous; // 待插入节点prev指向previous节点,建立链接
}
// 链表长度增加
this.count++;
// 插入成功
return true;
}
// 插入失败,超出边界值范围
return false;
}
removeAt(index) {
// 检查边界值
if (index >= 0 && index < this.count) {
// 待删除元素从头遍历
let current = this.head;
if (index === 0) {
// 删除头部元素
this.head = this.head.next;
if (this.count === 1) {
// 如果只有一项,需要删除尾部元素,新的head也为undefined
this.tail = undefined;
} else {
// 需要更新head元素的prev引用
this.head.prev = undefined;
}
} else if (index === this.count - 1) {
// 最后一项
current = this.tail;
this.tail = current.prev; // 更新tail元素为尾部元素的prev
this.tail.next = undefined; // 更新tail元素next指向
} else {
// 查找到待删除元素
current = this.getElementAt(index);
const previous = current.prev;
// 将previous与current的下一项链接起来,删除current
previous.next = current.next;
current.next.prev = previous;
}
// 链表长度减少
this.count--;
// 返回删除元素值
return current.element;
}
// 超出边界值
return undefined;
}
// 查找值所在索引
indexOf(element) {
let current = this.head;
let index = 0;
while (current != null) {
if (this.equalsFn(element, current.element)) {
return index;
}
index++;
current = current.next;
}
return -1;
}
// 返回头部元素
getHead() {
return this.head;
}
// 返回尾部元素
getTail() {
return this.tail;
}
// 清除双向链表
clear() {
super.clear();
this.tail = undefined;
}
// 返回表示整个链表的字符串
toString() {
if (this.head == null) {
return '';
}
let objString = `${this.head.element}`;
let current = this.head.next;
while (current != null) {
objString = `${objString},${current.element}`;
current = current.next;
}
return objString;
}
// 返回表示整个链表的反向字符串
inverseToString() {
if (this.tail == null) {
return '';
}
let objString = `${this.tail.element}`;
let previous = this.tail.prev;
while (previous != null) {
objString = `${objString},${previous.element}`;
previous = previous.prev;
}
return objString;
}
}
循环链表
循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链表之间唯一的区别在于,最后一个元素指向下一个元素的指针(tail.next
)不是引用undefined
,而是指向第一个元素(head
)。
下面的数据结构为单向引用的循环链表:
// 单向引用的循环链表,继承单向链表类
class CircularLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals) {
super(equalsFn);
}
// 向尾部添加新元素
push(element) {
const node = new Node(element);
let current;
if (this.head == null) {
// 链表为空时,插入元素为head元素
this.head = node;
} else {
// 链表的最后一个元素
current = this.getElementAt(this.size() - 1);
current.next = node; // 最后一个元素next指向新元素
}
// 设置最后一个节点next指向头部元素
node.next = this.head;
this.count++;
}
// 插入新元素
insert(element, index) {
// 检查边界值
if (index >= 0 && index <= this.count) {
const node = new Node(element);
let current = this.head;
if (index === 0) {
// 插入位为头部head
if (this.head == null) {
// 空链表中插入元素则为head,head节点的next就需要指向自己
this.head = node;
node.next = this.head;
} else {
// 非空链表在头部插入元素
node.next = current; // next指向原head元素
current = this.getElementAt(this.size()); // 最后一个元素
// 更新最后一个元素
this.head = node;
current.next = this.head;
}
} else {
// 中间位置插入元素
const previous = this.getElementAt(index - 1);
node.next = previous.next;
previous.next = node;
}
this.count++;
return true;
}
return false;
}
// 从任意位置移除元素
removeAt(index) {
if (index >= 0 && index < this.count) {
let current = this.head; // 待删除元素
if (index === 0) {
if (this.size() === 1) {
// 链表长度为1时,只需要修改head元素
this.head = undefined;
} else {
// 链表长度大于1,移除head元素
const removed = this.head;
// 查找到最后一个元素
current = this.getElementAt(this.size() - 1);
this.head = this.head.next; // 更新head
current.next = this.head; // 最后一个元素next指向新的head元素
current = removed; // 移除的元素
}
} else {
// 待删除位的前一个元素
const previous = this.getElementAt(index - 1);
current = previous.next; // 待删除元素
previous.next = current.next;
}
this.count--;
return current.element; // 返回删除元素的值
}
return undefined;
}
}
有序链表
有序链表是指保持元素有序的链表结构。除了使用排序算法之外,我们还可以将元素插入到正确的位置来保证链表的有序性。
const Compare = {
LESS_THAN: -1,
BIGGER_THAN: 1,
EQUALS: 0
};
// 用来比较元素
function defaultCompare(a, b) {
if (a === b) {
return Compare.EQUALS;
}
return a < b ? Compare.LESS_THAN : Compare.BIGGER_THAN;
}
// 比较元素相等
function defaultEquals(a, b) {
return a === b;
}
class SortedLinkedList extends LinkedList {
constructor(equalsFn = defaultEquals, compareFn = defaultCompare) {
super(equalsFn);
this.equalsFn = equalsFn;
this.compareFn = compareFn;
}
// 从尾部插入元素 同插入元素
push(element) {
if (this.isEmpty()) {
super.push(element);
} else {
const index = this.getIndexNextSortedElement(element);
super.insert(element, index);
}
}
// 插入元素,插入位置需要链表查找后确定,index值不使用
insert(element, index = 0) {
if (this.isEmpty()) {
// 空链表,直接插入在index为0的位置
return super.insert(element, 0);
}
// 查找待插入位置
const pos = this.getIndexNextSortedElement(element);
return super.insert(element, pos); // 调用父级插入方法
}
// 获取插入元素的位置
getIndexNextSortedElement(element) {
let current = this.head;
let i = 0;
// 从头遍历整个链表
for (; i < this.size() && current; i++) {
const comp = this.compareFn(element, current.element);
if (comp === Compare.LESS_THAN) {
// 当待插入元素的值小于循环元素的值时 返回当前循环元素的索引
return i;
}
// 下一个待比较元素
current = current.next;
}
return i; // 待插入位置的索引值
}
}
扩展:StackLinkedList
类
使用DoublyLinkedList
(双向链表)创建栈数据结构。对于栈来说,需要向链表尾部添加元素,或者从链表尾部移除元素。DoublyLinkedList
类有列表最后一个元素(tail
)的引用,无须迭代整个链表的元素就能获取它。双向链表可以直接获取头尾的元素,减少过程消耗,它的时间复杂度和原始的Stack实现相同,为O(1)
。
也可以对
LinkedList
类进行优化,保存一个指向尾部元素的引用,并使用这个优化版本来代替DoublyLinkedList
。
export default class StackLinkedList {
constructor() {
// 使用双向链表进行存储数据
this.items = new DoublyLinkedList();
}
push(element) {
this.items.push(element);
}
pop() {
if (this.isEmpty()) {
return undefined;
}
const result = this.items.removeAt(this.size() - 1);
return result;
}
peek() {
if (this.isEmpty()) {
return undefined;
}
return this.items.getElementAt(this.size() - 1).element;
}
isEmpty() {
return this.items.isEmpty();
}
size() {
return this.items.size();
}
clear() {
this.items.clear();
}
toString() {
return this.items.toString();
}
}
Leetcode
算法题
合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入两个链表的head
,输出新链表的head
// ListNode
function ListNode(val, next) {
this.val = (val === undefined ? 0 : val);
this.next = (next === undefined ? null : next);
}
// 方法1 -- 递归,返回的是新链表
var mergeTwoLists = function(l1, l2) {
if (l1 === null) {
return l2;
} else if (l2 === null) {
return l1;
} else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
};
// 方法2 -- 迭代
var mergeTwoLists = function(l1, l2) {
const prehead = new ListNode(-1); // 哑节点
let current = prehead // 初始化为哑节点,从头开始迭代
while (l1 !== null && l2 !== null) {
if (l1.val <= l2.val) {
current.next = l1
l1 = l1.next // l1重新赋值进入下次循环
} else {
current.next = l2
l2 = l2.next // l2重新赋值进入下次循环
}
current = current.next // current到下一个节点
}
// l1 === null || l2 === null 有一个链表最后的节点未进入新链表中
current.next = l1 === null ? l2 : l1
return prehead.next
}
排序链表
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表,在 O(n log n)
时间复杂度和常数级空间复杂度下,对链表进行排序。
使用归并排序
// 需要使用到上一个算法-合并两个有序链表
// 自顶向下排序 从中间二分成两个链表排序后进行合并
const toSortList = (head, tail) => {
if (head === null) {
return head;
}
if (head.next === tail) {
head.next = null;
return head;
}
// 快慢双指针法,奇数个节点找到重点,偶数个节点找到中心左边的节点
let slow = head, fast = head;
while (fast !== tail) {
slow = slow.next;
fast = fast.next;
if (fast !== tail) {
fast = fast.next;
}
}
const mid = slow;
return mergeTwoLists(toSortList(head, mid), toSortList(mid, tail));
}
var sortList = function(head) {
return toSortList(head, null);
}
// 自底向上排序
var sortList = function(head) {
if (head === null) {
return head;
}
let length = 0;
let node = head;
while (node !== null) {
length++;
node = node.next;
}
const dummyHead = new ListNode(0, head);
// subLength <<= 1 => subLength *= 2
// 细分到两个节点后再合并
for (let subLength = 1; subLength < length; subLength <<= 1) {
let prev = dummyHead, curr = dummyHead.next;
while (curr !== null) {
let head1 = curr;
for (let i = 1; i < subLength && curr.next !== null; i++) {
curr = curr.next;
}
let head2 = curr.next;
curr.next = null;
curr = head2;
for (let i = 1; i < subLength && curr != null && curr.next !== null; i++) {
curr = curr.next;
}
let next = null;
if (curr !== null) {
next = curr.next;
curr.next = null;
}
const merged = mergeTwoLists(head1, head2);
prev.next = merged;
while (prev.next !== null) {
prev = prev.next;
}
curr = next;
}
}
return dummyHead.next;
};
反转单向链表
function ReverseList(pHead) {
// write code here
if (pHead === null) {
return null
}
let pre = null
let cur = pHead
let nex = pHead.next
while (cur) {
cur.next = pre // 当前节点的下一个节点改为前一个节点
pre = cur // 记录前一个节点的值 循环到下一个节点的时候和next互换
cur = nex // 进入下一轮循环
// 判断是否是最后一个节点
nex = nex != null ? nex.next : null
}
return pre
}
环路检测
重建二叉树
根据前序遍历和中序遍历的结果,重建二叉树。
function TreeNode(x) {
this.val = x;
this.left = null;
this.right = null;
}
function reConstructBinaryTree(pre, vin) {
// write code here
// 前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,
// 左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。然后分别对左右子树递归地求解。
// pre 前序遍历结果 根->左->右 vin 中序遍历结果 左->根->右
if (pre.length === 0 || vin.length === 0) {
return null
}
let val = pre[0] // 前序遍历 第一个节点为根节点
// 建立根结点
let node = new TreeNode(val)
// 根节点所在的中序遍历的位置 则左边就是左子树 右边是右子树
let index = vin.indexOf(val)
// 左子树 前序遍历和中序遍历的左子树节点数相同 截断作为前序和中序遍历子数组 递归
node.left = reConstructBinaryTree(pre.slice(1, index + 1), vin.slice(0, index))
// 右子树 中序遍历vin要跳过index 前序遍历根节点为第一个元素
node.right = reConstructBinaryTree(pre.slice(index + 1), vin.slice(index + 1))
// 返回根节点
return node
}
module.exports = {
reConstructBinaryTree : reConstructBinaryTree
};