在这篇文章中,我将用代码的方式,给大家呈现出链表在程序里面是什么样子,以及链表有什么方法对外暴露
我们需要做以下准备
- typescript开发语言
- nodejs环境
链表的定义
-
特点:通过“指针”将一组零散的内存块串联起来。插入和删除效率高(只需要改变指针指向,时间复杂度 O(1))。但访问元素需要从头遍历(时间复杂度 O(n)),且需要额外的空间存储指针。
-
常见变种:
- 单向链表:每个节点只指向下一个节点。
- 双向链表:每个节点指向前一个和后一个节点,支持双向遍历。
- 循环链表:尾节点指向头节点,形成一个环。
-
场景:频繁进行插入和删除操作、不确定数据规模的情况(如实现队列、LRU缓存)。
那接下来我们用typescript实现一个双向链表
代码实现
1. 首先我们需要先定义链表里面节点的类型
// 定义链表节点类
class DoublyLinkedListNode<T> {
value: T;
next: DoublyLinkedListNode<T> | null;
prev: DoublyLinkedListNode<T> | null;
constructor(value: T) {
this.value = value;
this.next = null;
this.prev = null;
}
}
2. 接下来开始编写我们的链表类 DoublyLinkedList
// 定义双向链表类
class DoublyLinkedList<T> {
private head: DoublyLinkedListNode<T> | null;
private tail: DoublyLinkedListNode<T> | null;
private _size: number;
constructor() {
this.head = null;
this.tail = null;
this._size = 0;
}
}
熟悉数据结构的同学都知道,双向链表或单向链表结构里面会有头部和尾部两个元素
3. 接下来我们给我们的链表类添加方法
获取链表长度
// 获取链表大小
get size(): number {
return this._size;
}
头部添加元素
这个方法还是很简单的,
- 如果长度为0
1.1 将头部和尾部都指向新节点
- 如果长度大于0,那么我们看看怎么插入的
// 在链表头部添加节点
prepend(value: T): void {
const newNode = new DoublyLinkedListNode(value);
if (this._size === 0) {
this.head = newNode;
this.tail = newNode;
} else {
newNode.next = this.head;
this.head!.prev = newNode;
this.head = newNode;
}
this._size++;
}
尾部添加元素
尾部添加元素和头部添加元素有异曲同工之处
// 在链表尾部添加节点
append(value: T): void {
const newNode = new DoublyLinkedListNode(value);
if (this._size === 0) {
this.head = newNode;
this.tail = newNode;
} else {
this.tail!.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
}
this._size++;
}
指定位置添加元素
// 在指定位置插入节点
insertAt(index: number, value: T): boolean {
if (index < 0 || index > this._size) return false;
if (index === 0) {
this.prepend(value);
return true;
}
if (index === this._size) {
this.append(value);
return true;
}
const newNode = new DoublyLinkedListNode(value);
let current = this.head;
for (let i = 0; i < index - 1; i++) {
current = current!.next;
}
newNode.next = current!.next;
newNode.prev = current;
current!.next!.prev = newNode;
current!.next = newNode;
this._size++;
return true;
}
删除指定位置的节点
removeAt(index: number): T | null {
if (index < 0 || index >= this._size) return null;
let current = this.head;
if (index === 0) {
this.head = current!.next;
if (this.head) {
this.head.prev = null;
} else {
this.tail = null;
}
} else if (index === this._size - 1) {
current = this.tail;
this.tail = current!.prev;
this.tail!.next = null;
} else {
for (let i = 0; i < index; i++) {
current = current!.next;
}
current!.prev!.next = current!.next;
current!.next!.prev = current!.prev;
}
this._size--;
return current!.value;
}
查找元素
// 查找节点值
find(value: T): number {
let current = this.head;
let index = 0;
while (current) {
if (current.value === value) {
return index;
}
current = current.next;
index++;
}
return -1;
}
获取指定位置的元素
// 获取指定位置的节点值
getAt(index: number): T | null {
if (index < 0 || index >= this._size) return null;
let current = this.head;
for (let i = 0; i < index; i++) {
current = current!.next;
}
return current!.value;
}
其他功能函数
// 转换为数组
toArray(): T[] {
const result: T[] = [];
let current = this.head;
while (current) {
result.push(current.value);
current = current.next;
}
return result;
}
// 清空链表
clear(): void {
this.head = null;
this.tail = null;
this._size = 0;
}
// 打印链表
print(): void {
let current = this.head;
let output = "";
while (current) {
output += `${current.value} <-> `;
current = current.next;
}
console.log(output + "null");
}
思考
在removeAt方法里面,我们在删除某个元素是,此元素的pre和next并没有赋为null,只是把该元素的引用全都清空掉了,这时就需要提到 JS 里面的垃圾回收机制
在TypeScript/JavaScript中,内存管理是通过垃圾回收机制自动处理的。垃圾回收器会定期检查哪些对象不再被任何变量引用,然后释放这些对象占用的内存。
关键点:
- 引用计数:当一个对象没有被任何其他对象引用时,它就会被标记为可回收
- 循环引用处理:现代垃圾回收器能够检测并处理循环引用的情况
- 手动断开引用:虽然JS有自动垃圾回收,但手动断开不必要的引用可以加速内存释放