TypeScript数据结构与算法系列(一) —— 链表

图源:你好算法
内存空间是所有程序的公共资源,在一个复杂的系统运行环境下,空闲的内存空间可能散落在内存各处。我们知道,存储数组的内存空间必须是连续的,而当数组非常大时,内存可能无法提供如此大的连续空间。此时链表的灵活性优势就体现出来了。

链表(linked list)是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接。引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点。

链表的设计使得各个节点可以分散存储在内存各处,它们的内存地址无须连续。
在这里插入图片描述
观察图例,链表的组成单位是节点(node)对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”

链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”
尾节点指向的是“空”,它在 Java、C++ 和 Python 中分别被记为 nullnullptr None
在 C、C++、Go 和 Rust 等支持指针的语言中,上述“引用”应被替换为“指针”
如以下代码所示,链表节点 ListNode 除了包含值,还需额外保存一个引用(指针)。因此在相同数据量下,链表比数组占用更多的内存空间

/* 链表节点类 */
class ListNode {
    val: number;
    next: ListNode | null;
    constructor(val?: number, next?: ListNode | null) {
        this.val = val === undefined ? 0 : val;        // 节点值
        this.next = next === undefined ? null : next;  // 指向下一节点的引用
    }
}

链表常用操作

1.初始化链表

建立链表分为两步,第一步是初始化各个节点对象,第二步是构建节点之间的引用关系。初始化完成后,我们就可以从链表的头节点出发,通过引用指向 next 依次访问所有节点。

/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
const n0 = new ListNode(1);
const n1 = new ListNode(3);
const n2 = new ListNode(2);
const n3 = new ListNode(5);
const n4 = new ListNode(4);
// 构建节点之间的引用
n0.next = n1;
n1.next = n2;
n2.next = n3;
n3.next = n4;

数组整体是一个变量,比如数组nums包含元素nums[0]nums[1] 等,而链表是由多个独立的节点对象组成的。我们通常将头节点当作链表的代称,比如以上代码中的链表可记作链表 n0

2. 插入节点

在链表中插入节点非常容易。如图例所示,假设我们想在相邻的两个节点n0n1 之间插入一个新节点 P ,则只需改变两个节点引用(指针)即可,时间复杂度为 O(1)
相比之下,在数组中插入元素的时间复杂度为O(n) ,在大数据量下的效率较低。
在这里插入图片描述

/* 在链表的节点 n0 之后插入节点 P */
function insert(n0: ListNode, P: ListNode): void {
    const n1 = n0.next;
    P.next = n1;
    n0.next = P;
}

3. 删除节点

如图 4-7 所示,在链表中删除节点也非常方便,只需改变一个节点的引用(指针)即可。

请注意,尽管在删除操作完成后节点 P 仍然指向 n1 ,但实际上遍历此链表已经无法访问到 P ,这意味着 P 已经不再属于该链表了。
在这里插入图片描述

/* 删除链表的节点 n0 之后的首个节点 */
function remove(n0: ListNode): void {
    if (!n0.next) {
        return;
    }
    // n0 -> P -> n1
    const P = n0.next;
    const n1 = P.next;
    n0.next = n1;
}

4. 访问节点

在链表中访问节点的效率较低。如上一节所述,我们可以在O(1)时间下访问数组中的任意元素。链表则不然,程序需要从头节点出发,逐个向后遍历,直至找到目标节点。也就是说,访问链表的第i个节点需要循环 i-1轮,时间复杂度为 O(n)。

/* 访问链表中索引为 index 的节点 */
function access(head: ListNode | null, index: number): ListNode | null {
    for (let i = 0; i < index; i++) {
        if (!head) {
            return null;
        }
        head = head.next;
    }
    return head;
}

5. 查找节点

遍历链表,查找其中值为 target的节点,输出该节点在链表中的索引。此过程也属于线性查找。代码如下所示:

/* 在链表中查找值为 target 的首个节点 */
function find(head: ListNode | null, target: number): number {
    let index = 0;
    while (head !== null) {
        if (head.val === target) {
            return index;
        }
        head = head.next;
        index += 1;
    }
    return -1;
}

数组VS链表
在这里插入图片描述
双向链表示例:

/* 双向链表节点类 */
class ListNode {
    val: number;
    next: ListNode | null;
    prev: ListNode | null;
    constructor(val?: number, next?: ListNode | null, prev?: ListNode | null) {
        this.val = val  ===  undefined ? 0 : val;        // 节点值
        this.next = next  ===  undefined ? null : next;  // 指向后继节点的引用
        this.prev = prev  ===  undefined ? null : prev;  // 指向前驱节点的引用
    }
}

在这里插入图片描述

单向链表通常用于实现栈、队列、哈希表和图等数据结构。

  • 栈与队列:当插入和删除操作都在链表的一端进行时,它表现的特性为先进后出,对应栈;当插入操作在链表的一端进行,删除操作在链表的另一端进行,它表现的特性为先进先出,对应队列
  • 哈希表:链式地址是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表中。
  • 图:邻接表是表示图的一种常用方式,其中图的每个顶点都与一个链表相关联,链表中的每个元素都代表与该顶点相连的其他顶点。

双向链表常用于需要快速查找前一个和后一个元素的场景。

  • 高级数据结构:比如在红黑树B 树中,我们需要访问节点的父节点,这可以通过在节点中保存一个指向父节点的引用来实现,类似于双向链表.

  • 浏览器历史:在网页浏览器中,当用户点击前进或后退按钮时,浏览器需要知道用户访问过的前一个和后一个网页。双向链表的特性使得这种操作变得简单。

  • LRU 算法:在缓存淘汰(LRU)算法中,我们需要快速找到最近最少使用的数据,以及支持快速添加和删除节点。这时候使用双向链表就非常合适。

环形链表常用于需要周期性操作的场景,比如操作系统的资源调度

  • 时间片轮转调度算法:在操作系统中,时间片轮转调度算法是一种常见的 CPU调度算法,它需要对一组进程进行循环。每个进程被赋予一个时间片,当时间片用完时,CPU将切换到下一个进程。这种循环操作可以通过环形链表来实现。
  • 数据缓冲区:在某些数据缓冲区的实现中,也可能会使用环形链表。比如在音频、视频播放器中,数据流可能会被分成多个缓冲块并放入一个环形链表,以便实现无缝播放。
  • 39
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
TypeScript是一种静态类型的编程语言,它提供了丰富的类型系统和面向对象的特性,使得开发者可以更好地组织和管理代码。在TypeScript,高阶数据结构算法可以通过类和泛型等特性来实现。 高阶数据结构是指那些在基本数据结构的基础上进行扩展或组合得到的数据结构。例如,堆、图、树等都可以被视为高阶数据结构。在TypeScript,我们可以使用类来定义这些高阶数据结构,并通过泛型来指定其内部存储的数据类型。 下面是一个使用TypeScript实现的堆(Heap)数据结构的示例: ```typescript class Heap<T> { private data: T[] = []; public size(): number { return this.data.length; } public isEmpty(): boolean { return this.data.length === 0; } public insert(value: T): void { this.data.push(value); this.siftUp(this.data.length - 1); } public extractMin(): T | null { if (this.isEmpty()) { return null; } const min = this.data[0]; const last = this.data.pop()!; if (!this.isEmpty()) { this.data[0] = last; this.siftDown(0); } return min; } private siftUp(index: number): void { while (index > 0) { const parentIndex = Math.floor((index - 1) / 2); if (this.data[index] >= this.data[parentIndex]) { break; } [this.data[index], this.data[parentIndex]] = [this.data[parentIndex], this.data[index]]; index = parentIndex; } } private siftDown(index: number): void { const size = this.size(); while (index * 2 + 1 < size) { let childIndex = index * 2 + 1; if (childIndex + 1 < size && this.data[childIndex + 1] < this.data[childIndex]) { childIndex++; } if (this.data[index] <= this.data[childIndex]) { break; } [this.data[index], this.data[childIndex]] = [this.data[childIndex], this.data[index]]; index = childIndex; } } } ``` 以上是一个最小堆的实现,使用了数组来存储数据,并提供了插入和提取最小值的操作。堆是一种常见的高阶数据结构,用于解决许多问题,如优先队列和排序等。 通过使用TypeScript,我们可以更加清晰地定义和使用高阶数据结构算法,并通过类型检查来减少错误和提高代码的可维护性。当然,这只是其一个例子,还有许多其他高阶数据结构算法可以在TypeScript实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

真的很上进

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值