# 链表
# 链表的基本概念
链表是一种包含数据域和指针域的数据结构,一般用头结点代表整个链表,与数组不同的是,链表的内存并不是连续的,概括自维基百科。
# 链表家族成员介绍
# 单向链表
单向链表包含两个域,一个数据域和一个指针域。这个链接指向列表中的下一个结点,而最后一个结点则指向一个空值。
# 双向链表
双向链表每个结点有两个连接:一个指向前一个结点,(当此“连接”为第一个“连接”时,指向空值或者空列表);而另一个指向下一个结点,(当此“连接”为最后一个“连接”时,指向空值或者空列表)。
# 循环链表
循环链表是一种“无头无尾”的链表,首结点和末结点被连接在一起。
这里有些参考书是节点,有些是结点, 其实他们所表述的是一个意思。笔者查阅了相关资料,最终认为结点比较合理,所以文中以结点来表述,英文单词 node 翻译过来本身就有结点的意思,另一方面,结点强调空间方面,比如说打结,而节点更加强调时间方面,比如一个事件的时间新闻节点
# 链表的操作方式
这里以单向链表的实现为例进行讲解
# 链表的创建
吃过冰糖葫芦的朋友,可以发现冰糖葫芦这个结构啊,它很像链表。我们把山楂清洗干净,在外面糊上一层冰糖,准备好大牙签,一个一个串起来,每个山楂的头都指向下一个山楂,直到冰糖葫芦串最后一个山楂指向空气,像这样的模型,我们可以把它称之为“冰糖链表模型”。
# 链表结点的定义
根据楼上的模型,我们把山楂抽象出来就是链表的结点了,它包含本身的数据和指向下一个结点的指针。
/**
* Definition for singly-linked list.
*/
export default class SinglyListNode {
val: number;
next: SinglyListNode | null;
constructor(val?: number, next?: SinglyListNode | null) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
}
是的,可能细心的读者已经观察到了,楼上的就是笔者刷 leetcode 时,leetcode 上面给出的链表结点的实现。这里的数据域是 number 类型的变量,但是在很多我们现实生活的场景,这里的数据它难道不可以是 string 类型,boolean 类型等等吗?想一下,我们需要什么啊?我们其实是需要一个模板,这个模板到时候你可以根据自己的业务场景去框定它是 number、string 还是 boolean 等等。这里就引入了 typescript 中的泛型,通过泛型我们可以轻松地实现我们楼上的需求。
在type.ts
下定义参数类型如:
export type paramsType = number | string | boolean;
基于泛型实现的结点类如下:
import { paramsType } from './type';
/**
* Definition for singly-linked list.
*/
export default class SinglyListNode<T extends paramsType> {
val: T | undefined;
next: SinglyListNode<T> | null;
constructor(val?: T, next?: SinglyListNode<T> | null) {
this.val = val;
this.next = next === undefined ? null : next;
}
}
这里我们先定义了一个联合类型 paramsType,然后通过SinglyListNode<null>
, SinglyListNode<undefined>
这种就不会触发编译报错,而通过上面约束泛型的形式可以避免这个问题
# 链表的定义
一个单链表,它应该包含获取链表中第 n 个结点的方法, 在链表头部添加结点的方法,在链表尾部添加结点的方法,在链表的第 n 个结点添加链表的方法,删除第 n 个结点的方法,获取整个链表的方法等等
import SinglyListNode from './singly-list-node';
export class SinglyLinkedList {
dummy: SinglyListNode;
length: number;
constructor() {
this.dummy = new SinglyListNode();
this.length = 0;
}
/**
* @description 获取链表中第 index 个结点的值。如果索引无效,则返回-1。
* @param {number} index
* @return {number}
*/
get(index: number): number {}
/**
* @description 在链表的第一个元素之前添加一个值为 val 的结点。插入后,新结点将成为链表的第一个结点。
* @param {number} val
* @return {void}
*/
addAtHead(val: number): void {}
/**
* @description 将值为 val 的结点追加到链表的最后一个元素。
* @param {number} val
* @return {void}
*/
addAtTail(val: number): void {}
/**
* @description 在链表中的第 index 个结点之前添加值为 val 的结点。如果 index 等于链表的长度,则该结点将附加到链表的末尾。如果 index 大于链表长度,则不会插入结点。如果index小于0,则在头部插入结点。
* @param {number} index
* @param {number} val
* @return {void}
*/
addAtIndex(index: number, val: number): void {}
/**
* @description 如果索引 index 有效,则删除链表中的第 index 个结点。
* @param {number} index
* @return {void}
*/
deleteAtIndex(index: number): void {}
/**
* @description 获取整个链表
* @return {SinglyListNode}
*/
getLinkedList(): SinglyListNode {}
}
那么,创建一个链表就是这样的啦。
/**
* Your SinglyLinkedList object will be instantiated and called as such:
*/
const obj = new SinglyLinkedList();
const param = obj.get(0);
obj.addAtHead(1);
obj.addAtTail(2);
obj.addAtIndex(1, 3);
obj.deleteAtIndex(2);
这里我们会在初始化链表的时候,创建一个 dummy 结点,有了它你会发现,后面获取整个链表,包括对链表的一些操作会简单很多
同样的,既然有基于泛型的链表结点,那自然是有基于泛型的链表
import { paramsType } from './type';
import SinglyListNode from './singly-list-node-generics';
export class SinglyLinkedList<T extends paramsType> {
dummy: SinglyListNode<T>;
length: number;
constructor() {
this.dummy = new SinglyListNode<T>();
this.length = 0;
}
/**
* @description 获取链表中第 index 个结点的值。如果索引无效,则返回undefined。
* @param {number} index
* @return {T | undefined}
*/
get(index: number): T | undefined {}
/**
* @description 在链表的第一个元素之前添加一个值为 val 的结点。插入后,新结点将成为链表的第一个结点。
* @param {T} val
* @return {void}
*/
addAtHead(val: T): void {}
/**
* @description 将值为 val 的结点追加到链表的最后一个元素。
* @param {T} val
* @return {void}
*/
addAtTail(val: T): void {}
/**
* @description 在链表中的第 index 个结点之前添加值为 val 的结点。如果 index 等于链表的长度,则该结点将附加到链表的末尾。如果 index 大于链表长度,则不会插入结点。如果index小于0,则在头部插入结点。
* @param {number} index
* @param {T} val
* @return {void}
*/
addAtIndex(index: number, val: T): void {}
/**
* @description 如果索引 index 有效,则删除链表中的第 index 个结点。
* @param {number} index
* @return {void}
*/
deleteAtIndex(index: number): void {}
/**
* @description 获取整个链表
* @returns {SinglyListNode<T>}
*/
getLinkedList(): SinglyListNode<T> {}
}
由于泛型的链表实现和链表并没有太大的差别,为了不占用篇幅,这里就点一下,在后面的子标题介绍中,就介绍针对于链表的实现
# 链表的遍历
这里我们实现一个 toString 方法,将链表的每个元素遍历出来。实现的思路是我们将 dummy 结点的 next 指针赋值给变量 cur,如果变量 cur 为 null 的话表示是空链表,如果 cur 存在的话,一把 while 循环遍历它,然后扁平化输出。
/**
* @description 转成字符串
* @return {string}
*/
toString(): string {
let res: string = '';
let cur: SinglyListNode | null = this.dummy.next;
if (!cur) {
return res;
} else {
while (cur) {
res = `${res}, ${cur.val}`;
cur = cur.next!;
}
}
return res.slice(2);
}
# 获取链表的某个结点
这里和楼上的链表的遍历有点像,只不过判断依据不一样,首先我们先判断这个结点在不在(事先追加一个 length 属性去记录链表的长度),如果不在直接返回-1,如果在的话那么就按索引循环,终止的条件是索引小于 0,最后再返回对于的结点的值
/**
* @description 获取链表中第 index 个结点的值。如果索引无效,则返回-1。
* @param {number} index
* @return {number}
*/
get(index: number): number {
if (index < 0 || index >= this.length) {
return -1;
}
let cur: SinglyListNode = this.dummy;
while (index >= 0) {
cur = cur.next!;
index--;
}
return cur.val;
}
# 链表的插入
插入这个操作有两个比较特殊的情况,就是在头结点插入和尾结点插入,普通的就是在任意位置插入。
# 头结点插入
我们把 dummy 结点的 next 指针指向的结点赋值给 cur,然后创建一个新的结点,将它的 next 指针指向 cur,最后我们将 dummy 结点的 next 指针指向我们刚才创建好的结点,最后长度+1。
/**
* @description 在链表的第一个元素之前添加一个值为 val 的结点。插入后,新结点将成为链表的第一个结点。
* @param {number} val
* @return {void}
*/
addAtHead(val: number): void {
const cur = this.dummy.next;
const singlyListNode = new SinglyListNode(val, cur);
this.dummy.next = singlyListNode;
this.length++;
}
# 尾结点插入
我们把 dummy 结点赋值给变量 cur,然后根据条件cur && cur.next
循环,直到 cur 为最后一个结点时,我们将其 next 指针指向刚才创建的结点
/**
* @description 将值为 val 的结点追加到链表的最后一个元素。
* @param {number} val
* @return {void}
*/
addAtTail(val: number): void {
const singlyListNode = new SinglyListNode(val);
let cur: SinglyListNode = this.dummy;
while (cur && cur.next) {
cur = cur.next;
}
cur.next = singlyListNode;
this.length++;
}
# 任意位置插入
任意位置这里就要分类讨论啦, 前面我们已经实现了在头节点和尾结点插入的,那么还有种就是不是在这两个位置插入的,当然还要排除所插入结点的位置在链表不存在的情况,具体的如下:
/**
* @description 在链表中的第 index 个结点之前添加值为 val 的结点。如果 index 等于链表的长度,则该结点将附加到链表的末尾。如果 index 大于链表长度,则不会插入结点。如果index小于0,则在头部插入结点。
* @param {number} index
* @param {number} val
* @return {void}
*/
addAtIndex(index: number, val: number): void {
if (index <= 0) {
this.addAtHead(val);
} else if (index === this.length) {
this.addAtTail(val);
} else if (index > this.length) {
// do nothing
} else {
let cur: SinglyListNode = this.dummy;
while (index > 0) {
cur = cur.next!;
index--;
}
const singlyListNode = new SinglyListNode(val, cur.next);
cur.next = singlyListNode;
this.length++;
}
}
# 链表的删除
讲道理,链表的插入有三种特殊的,链表的删除也应该有的,但是删除这个操作吧,就没必要搞得太繁琐,干就完事了,我不管它是谁,我删就完事了,因此我们需要写一种符合特殊位置删除的方法。
具体的思路是,如果删除的索引结点在范围内的话,那么我们就根据索引遍历,然后将其指向 next 指针的 next 指针
/**
* @description 如果索引 index 有效,则删除链表中的第 index 个结点。
* @param {number} index
* @return {void}
*/
deleteAtIndex(index: number): void {
if (index >= 0 || index < this.length) {
let cur = this.dummy;
while (index > 0) {
cur = cur.next!;
index--;
}
if (cur && cur.next) {
cur.next = cur.next.next;
this.length--;
}
}
}
# 获取整个链表
有了 dummy 结点,获取整个链表就轻松很多了,一句话哈return this.dummy;
/**
* @description 获取整个链表
* @return {SinglyListNode}
*/
getLinkedList(): SinglyListNode {
return this.dummy;
}
好啦, 到这里楼上关于链表的相关操作均已讲完,源代码如下:
import SinglyListNode from './singly-list-node';
export class SinglyLinkedList {
dummy: SinglyListNode;
length: number;
constructor() {
this.dummy = new SinglyListNode();
this.length = 0;
}
/**
* @description 获取链表中第 index 个结点的值。如果索引无效,则返回-1。
* @param {number} index
* @return {number}
*/
get(index: number): number {
if (index < 0 || index >= this.length) {
return -1;
}
let cur: SinglyListNode = this.dummy;
while (index >= 0) {
cur = cur.next!;
index--;
}
return cur.val;
}
/**
* @description 在链表的第一个元素之前添加一个值为 val 的结点。插入后,新结点将成为链表的第一个结点。
* @param {number} val
* @return {void}
*/
addAtHead(val: number): void {
const cur = this.dummy.next;
const singlyListNode = new SinglyListNode(val, cur);
this.dummy.next = singlyListNode;
this.length++;
}
/**
* @description 将值为 val 的结点追加到链表的最后一个元素。
* @param {number} val
* @return {void}
*/
addAtTail(val: number): void {
const singlyListNode = new SinglyListNode(val);
let cur: SinglyListNode = this.dummy;
while (cur && cur.next) {
cur = cur.next;
}
cur.next = singlyListNode;
this.length++;
}
/**
* @description 在链表中的第 index 个结点之前添加值为 val 的结点。如果 index 等于链表的长度,则该结点将附加到链表的末尾。如果 index 大于链表长度,则不会插入结点。如果index小于0,则在头部插入结点。
* @param {number} index
* @param {number} val
* @return {void}
*/
addAtIndex(index: number, val: number): void {
if (index <= 0) {
this.addAtHead(val);
} else if (index === this.length) {
this.addAtTail(val);
} else if (index > this.length) {
// do nothing
} else {
let cur: SinglyListNode = this.dummy;
while (index > 0) {
cur = cur.next!;
index--;
}
const singlyListNode = new SinglyListNode(val, cur.next);
cur.next = singlyListNode;
this.length++;
}
}
/**
* @description 如果索引 index 有效,则删除链表中的第 index 个结点。
* @param {number} index
* @return {void}
*/
deleteAtIndex(index: number): void {
if (index >= 0 || index < this.length) {
let cur = this.dummy;
while (index > 0) {
cur = cur.next!;
index--;
}
if (cur && cur.next) {
cur.next = cur.next.next;
this.length--;
}
}
}
/**
* @description 获取整个链表
* @return {SinglyListNode}
*/
getLinkedList(): SinglyListNode {
return this.dummy;
}
/**
* @description 转成字符串
* @return {string}
*/
toString(): string {
let res: string = '';
let cur: SinglyListNode | null = this.dummy.next;
if (!cur) {
return res;
} else {
while (cur) {
res = `${res}, ${cur.val}`;
cur = cur.next!;
}
}
return res.slice(2);
}
}
基于泛型的实现
import { paramsType } from './type';
import SinglyListNode from './singly-list-node-generics';
export class SinglyLinkedList<T extends paramsType> {
dummy: SinglyListNode<T>;
length: number;
constructor() {
this.dummy = new SinglyListNode<T>();
this.length = 0;
}
/**
* @description 获取链表中第 index 个结点的值。如果索引无效,则返回undefined。
* @param {number} index
* @return {T | undefined}
*/
get(index: number): T | undefined {
if (index < 0 || index >= this.length) {
return undefined;
}
let cur: SinglyListNode<T> = this.dummy;
while (index >= 0) {
cur = cur.next!;
index--;
}
return cur.val;
}
/**
* @description 在链表的第一个元素之前添加一个值为 val 的结点。插入后,新结点将成为链表的第一个结点。
* @param {T} val
* @return {void}
*/
addAtHead(val: T): void {
const cur = this.dummy.next;
const singlyListNode = new SinglyListNode<T>(val, cur);
this.dummy.next = singlyListNode;
this.length++;
}
/**
* @description 将值为 val 的结点追加到链表的最后一个元素。
* @param {T} val
* @return {void}
*/
addAtTail(val: T): void {
const singlyListNode = new SinglyListNode<T>(val);
let cur: SinglyListNode<T> = this.dummy;
while (cur && cur.next) {
cur = cur.next;
}
cur.next = singlyListNode;
this.length++;
}
/**
* @description 在链表中的第 index 个结点之前添加值为 val 的结点。如果 index 等于链表的长度,则该结点将附加到链表的末尾。如果 index 大于链表长度,则不会插入结点。如果index小于0,则在头部插入结点。
* @param {number} index
* @param {T} val
* @return {void}
*/
addAtIndex(index: number, val: T): void {
if (index <= 0) {
this.addAtHead(val);
} else if (index === this.length) {
this.addAtTail(val);
} else if (index > this.length) {
// do nothing
} else {
let cur: SinglyListNode<T> = this.dummy;
while (index > 0) {
cur = cur.next!;
index--;
}
const singlyListNode = new SinglyListNode<T>(val, cur.next);
cur.next = singlyListNode;
this.length++;
}
}
/**
* @description 如果索引 index 有效,则删除链表中的第 index 个结点。
* @param {number} index
* @return {void}
*/
deleteAtIndex(index: number): void {
if (index >= 0 || index < this.length) {
let cur = this.dummy;
while (index > 0) {
cur = cur.next!;
index--;
}
if (cur && cur.next) {
cur.next = cur.next.next;
this.length--;
}
}
}
/**
* @description 获取整个链表
* @returns {SinglyListNode<T>}
*/
getLinkedList(): SinglyListNode<T> {
return this.dummy;
}
/**
* @description 转成字符串
* @return {string}
*/
toString(): string {
let res: string = '';
let cur: SinglyListNode<T> | null = this.dummy.next;
if (!cur) {
return res;
} else {
while (cur) {
res = `${res}, ${cur.val}`;
cur = cur.next!;
}
}
return res.slice(2);
}
}
# 链表的合并
leetcode 原题-合并两个有序的链表:https://leetcode-cn.com/problems/merge-two-sorted-lists/
好在是有序的链表,那么我们可以创建一个头结点,然后当两个链表结点不为空时,循环遍历里面的元素,取 list1 和 list2 中较小的值,将新结点的 next 指针指向它,最后还要判断下两个结点是不是都走完了,将没走完的结点接上去就好了。
// code/leetcode/21-merge-two-sorted-lists.ts
import ListNode from '../base/list-node';
/**
*
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode | null }
*/
export function mergeTwoLists(list1: ListNode | null, list2: ListNode | null): ListNode | null {
const head = new ListNode();
let cur = head;
while (list1 && list2) {
if (list1.val < list2.val) {
cur.next = list1;
list1 = list1.next;
} else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
cur.next = list1 ? list1 : list2;
return head.next;
}
测试用例如下:
// /test/code/21-merge-two-sorted-lists.test.ts
import ListNode from '../../code/base/list-node';
import { mergeTwoLists } from '../../code/leetcode/21-merge-two-sorted-lists';
describe('test function mergeTwoLists:', () => {
test('test case l1 = [], l2 = []', () => {
const l1 = new ListNode();
const l2 = new ListNode();
const expected = new ListNode();
expected.next = new ListNode();
expect(mergeTwoLists(l1, l2)).toEqual(expected);
});
test('test case l1 = [], l2 = [0]', () => {
const l1 = new ListNode();
const l2 = new ListNode(0);
const expected = new ListNode();
expected.next = new ListNode(0);
expect(mergeTwoLists(l1, l2)).toEqual(expected);
});
test('test case l1 = [1,2,4], l2 = [1,3,4]', () => {
const l1 = new ListNode(1);
const l12 = new ListNode(2);
const l13 = new ListNode(4);
l1.next = l12;
l12.next = l13;
const l2 = new ListNode(1);
const l22 = new ListNode(3);
const l23 = new ListNode(4);
l2.next = l22;
l22.next = l23;
const expected = new ListNode(1);
const n1 = new ListNode(1);
const n2 = new ListNode(2);
const n3 = new ListNode(3);
const n4 = new ListNode(4);
const n5 = new ListNode(4);
expected.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;
n4.next = n5;
expect(mergeTwoLists(l1, l2)).toEqual(expected);
});
});
# 链表的反转
leetcode 原题-反转链表: https://leetcode-cn.com/problems/reverse-linked-list/
这里就是之前我们是头朝尖的那头的冰糖葫芦串,现在要屁股朝尖的那头,实际上就是改变指针的指向,所以这里需要知道先前的结点和当前的结点,这样子循环到最后一个的话就返回,这就是我们想要的答案
// code/leetcode/206-reverse-linked-list.ts
import ListNode from '../base/list-node';
/**
*
* @param {ListNode | null} head
* @return { ListNode | null}
*/
export function reverseList(head: ListNode | null): ListNode | null {
let pre = null;
let cur = head;
while (cur) {
let next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
测试用例如下:
// test/leetcode/206-reverse-linked-list.test.ts
import ListNode from '../../code/base/list-node';
import makeListNodes from '../../code/utils/make-list-nodes';
import { reverseList } from '../../code/leetcode/206-reverse-linked-list';
describe('test function reverseList:', () => {
test('test case head = [1, 2, 3, 4, 5]', () => {
const head = makeListNodes([1, 2, 3, 4, 5]);
const expected = makeListNodes([5, 4, 3, 2, 1]);
expect(reverseList(head)).toEqual(expected);
});
test('test case head = [1, 2]', () => {
const head = makeListNodes([1, 2]);
const expected = makeListNodes([2, 1]);
expect(reverseList(head)).toEqual(expected);
});
test('test case head = []', () => {
const head = makeListNodes([]);
const expected = makeListNodes([]);
expect(reverseList(head)).toEqual(expected);
});
});
# 参考文献
- 维基百科 - 链表:https://zh.wikipedia.org/wiki/%E9%93%BE%E8%A1%A8