每种编程语言都实现了数组,但在大多数语言中,数组大小是固定的(创建时指定),从数组起点或中间插入或移除元素的成本很高,因为后面的元素都需要挨个挪位置。
1. 链表数据结构
链表是一种动态的数据结构,用于存储有序的元素集合,可以从中任意添加或移除元素,不需要移动其他元素,可按需扩容(如众人手拉手,加个人和减个人都很简单),链表中的元素并不是连续放置的,每个元素由一个存储元素本身的节点,和一个指向下一个元素的引用(也称指针或链接)组成:
数组可以直接访问任何位置的元素(可以理解为是物理电路,访问任意位置的元素都一样快),而想要访问链表中间的元素,就需要从表头开始迭代链表中的节点,挨个寻找,就像藏宝游戏,不断地根据线索寻找。
各自优势:
- 链表:增、删
- 数组:改、查
1.1 创建链表
- append(element):向列表尾部添加一个新的项。
- insert(position, element):向列表的特定位置插入一个新的项。
- remove(element):从列表中移除一项。
- indexOf(element):返回元素在列表中的索引。如果列表中没有该元素则返回-1。
- removeAt(position):从列表的特定位置移除一项。
- isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false。
- size():返回链表包含的元素个数。与数组的length属性类似。
- toString():由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的值。
function LinkedList() {
// 单个节点类,表示要加入到链表中的项
let Node = function (element) {
this.element = element;
this.next = null;
};
let length = 0; // 内部私有变量,记录链表长度
let head = null; // 头指针
/**
* 向链表尾部添加一个新的项
* 两种场景:链表为空,添加的是第一个元素;链表不为空,向其追加元素。
*
* @param element
*/
this.append = function (element) {
let node = new Node(element), current;
if (head === null) {
head = node;
} else {
current = head;
// 从表头开始循环找到最后一项
while (current.next !== null) {
current = current.next;
}
// 把节点链接到最后一项的 next 上
current.next = node;
}
length++; // 更新链表长度
};
/**
* 从任意位置移除元素并返回被移除的元素
* 两种场景:移除第一个元素;移除第一个以外的任一元素。
* 被移除的元素被丢弃在计算机内存中,等待被垃圾回收器清理。
*
* @param {number} position
* @returns {element|null}
*/
this.removeAt = function (position) {
// 检查是否越界
if (position >= 0 && position < length) {
let current = head,
previous,
index = 0;
// 分两种情况:表头和非表头
if (position === 0) {
head = current.next;
} else {
// position 的前一个位置时终止循环
while (index++ < position) {
previous = current;
current = current.next;
}
// 上下节点进行链接,跳过中间将被移除的 current 节点
previous.next = current.next;
}
length--;
return current.element;
} else {
return null;
}
};
/**
* 在任意位置插入元素
*
* @param {number} position
* @param element
* @returns {boolean}
*/
this.insert = function (position = 0, element) {
// 检查是否越界,注意这里包含了空链表时的情形
if (position >= 0 && position <= length) {
let node = new Node(element),
current = head,
previous,
index = 0;
if (position === 0) {
node.next = current;
head = node;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
node.next = current; // 即新节点插入到目标位置的前面,这里current可能是null
previous.next = node;
}
length++;
return true;
} else {
return false;
}
};
/**
* 把 LinkedList 对象转换成字符串
*
* @returns {string}
*/
this.toString = function () {
let current = head,
string = '',
index = 0;
while (current) {
string += index++ + ': ' + current.element + (current.next ? '\n' : '');
current = current.next;
}
return string;
};
/**
* 查找给定元素的索引,找不到则返回 -1
*
* @param element
* @returns {number}
*/
this.indexOf = function (element) {
let current = head,
index = -1;
while (current) {
index++;
if (element === current.element) {
return index;
}
current = current.next;
}
return -1;
};
/**
* 移除给定的元素
*
* @param element
* @returns {element|null}
*/
this.remove = function (element) {
const index = this.indexOf(element);
return this.removeAt(index);
};
/**
* 链表是否为空
* @returns {boolean}
*/
this.isEmpty = function () {
return length === 0;
};
/**
* 链表大小
* @returns {number}
*/
this.size = function () {
return length;
};
/**
* 获取表头
* 方便实例外部访问和迭代链表
*
* @returns {element|null}
*/
this.getHead = function () {
return head;
};
}
1.2 双向链表
function DoublyLinkedList() {
let Node = function (element) {
this.element = element;
this.next = null;
this.prev = null;
};
let length = 0;
let head = null;
let tail = null;
/**
* 从任意位置移除元素
*
* @param position
* @returns {element|null}
*/
this.removeAt = function (position) {
if (position >= 0 && position < length) {
let current = head,
previous,
index = 0;
if (position === 0) { // 第一项
head = current.next;
// 如果只有一项,需要更新tail
if (length === 1) {
tail = null;
} else {
head.prev = null;
}
} else if (position === length - 1) { // 最后一项
current = tail;
tail = current.prev;
tail.next = null;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = current.next;
current.next.prev = previous;
}
length--;
return current.element;
} else {
return null;
}
};
/**
* 从任意位置添加节点
*
* @param {number} position
* @param element
* @returns {boolean}
*/
this.insert = function (position, element) {
if (position >= 0 && position <= length) {
let node = new Node(element),
current = head,
previous,
index = 0;
if (position === 0) { // 在第一个位置添加
if (!head) { // 当前链表为空
head = node;
tail = node;
} else {
node.next = current;
current.prev = node;
head = node;
}
} else if (position === length) { // 最后一项
current = tail;
current.next = node;
node.prev = current;
tail = node;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = node;
node.next = current;
current.prev = node;
node.prev = previous;
}
length++;
return true;
} else {
return false;
}
};
// 其他方法和单向链表类似
}
1.3 循环链表
循环链表 CircularLinkedList 可以只有单向引用,也可以像双向链表一样有双向引用。
单向循环链表中:循环链表中 tail.next 指向 head。
双向循环链表中:tail.next 指向 head,head.prev 指向 tail。
2. 链表相关问题
2.1 判断链表是否存在环形
// https://leetcode.com/problems/linked-list-cycle/
// 1. 暴力解法(brute force): 迭代节点, 存储或进行标记, 如果遇见已经存在的记录, 则说明存在环形
// 2. 快慢双指针(龟兔赛跑):
// slow速度为1, fast为2, 若fast遇见了slow(地球是圆的), 则说明存在环形
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* 判断链表是否存在环形
*
* @param {ListNode} head
* @return {boolean}
*/
const hasCycle = function (head) {
let slow = head,
fast = head;
while (fast !== null && fast.next !== null) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
return true;
}
}
return false;
};
2.2 求两个链表交集时的节点
// https://leetcode.com/problems/intersection-of-two-linked-lists/
// 1. 暴力解法(brute force): 使用数组或者set存储一条链表所有节点,然后迭代第二条链表进行查找是否存在(略)
// 2. 双指针 two pointers
// 两个链表指针同时从各自表头移动, 速度一致, 当较短的链表(A)跑到头后, 较长的链表(B)就开始顺便重定向头部指针,
// 此后B的前后两个指针之差始终等于A的总长度,
// 然后等到B的后指针也跑到头后, 再同时从A和B的左侧指针开始迭代比较是否相等,找出交点.
// Time complexity : O(m+n)O(m+n).
// Space complexity : O(1)O(1).
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* 求两个链表交集时的节点
*
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode|null}
*/
const getIntersectionNode = function (headA, headB) {
let a = headA,
b = headB;
while (a !== null || b !== null) {
if (a !== null) {
a = a.next;
} else {
headB = headB.next;
}
if (b !== null) {
b = b.next;
} else {
headA = headA.next;
}
}
while (headA !== headB) {
headA = headA.next;
headB = headB.next;
}
return headA;
};