链表是一种常见的数据结构,它的应用场景非常广泛,比如在前端框架源码中就有许多使用链表的实现。本篇技术博客将结合LeetCode题目和前端框架源码等实际例子,介绍如何使用TypeScript来实现链表,并帮助读者更好地掌握链表的基础知识与应用。
TypeScript实现链表
在TypeScript中实现链表,可以定义一个节点类和一个链表类。节点类包含一个值和一个指向下一个节点的指针,链表类则包含一个头节点和一些基本的操作方法,比如增加节点、删除节点等。
class ListNode<T> {
public val: T;
public next: ListNode<T> | null;
constructor(val: T) {
this.val = val;
this.next = null;
}
}
class LinkedList<T> {
public head: ListNode<T> | null;
constructor() {
this.head = null;
}
public add(val: T): void {
if (!this.head) {
this.head = new ListNode(val);
} else {
let curr = this.head;
while (curr.next) {
curr = curr.next;
}
curr.next = new ListNode(val);
}
}
public remove(val: T): boolean {
if (!this.head) {
return false;
} else if (this.head.val === val) {
this.head = this.head.next;
return true;
} else {
let curr = this.head;
while (curr.next && curr.next.val !== val) {
curr = curr.next;
}
if (curr.next && curr.next.val === val) {
curr.next = curr.next.next;
return true;
} else {
return false;
}
}
}
public print(): void {
let curr = this.head;
let str = '';
while (curr) {
str += `${curr.val} -> `;
curr = curr.next;
}
str += 'null';
console.log(str);
}
}
上述代码中,我们使用了泛型来定义节点和链表的类型,使得它们可以适用于不同类型的值。在节点类中,我们定义了一个值和一个指向下一个节点的指针;在链表类中,我们定义了一个头节点和一些基本的操作方法,比如增加节点、删除节点、打印链表等。
LeetCode题目实战
接下来,我们将通过LeetCode上的一些链表题目来实践链表的使用和应用。
题目1:反转链表
题目描述:反转一个单链表。
例如,给定一个链表 1->2->3->4->5,反转后的链表为 5->4->3->2->1。
解题思路:使用三个指针prev、curr和next,分别表示前一个节点、当前节点和下一个节点。初始时,prev为null,curr为头节点,next为curr的下一个节点。依次遍历链表,将curr的指针指向prev,然后将prev、curr、next向后移动,直到遍历完整个链表。
代码实现:
function reverseList(head: ListNode | null): ListNode | null {
let prev: ListNode | null = null;
let curr: ListNode | null = head;
while (curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
题目2:删除链表的倒数第N个节点
题目描述:给定一个链表,删除链表的倒数第n个节点,并返回链表的头节点。
例如,给定一个链表 1->2->3->4->5,n = 2,则删除链表的倒数第二个节点后,链表变为 1->2->3->5。
解题思路:使用快慢指针的方法,先让快指针移动n步,然后让快指针和慢指针同时移动,当快指针到达链表 末尾时,慢指针指向的节点就是要删除的节点。
代码实现:
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
const dummy = new ListNode(0);
dummy.next = head;
let fast: ListNode | null = dummy;
let slow: ListNode | null = dummy;
for (let i = 0; i < n; i++) {
fast = fast.next;
}
while (fast && fast.next) {
fast = fast.next;
slow = slow.next;
}
if (slow && slow.next) {
slow.next = slow.next.next;
}
return dummy.next;
}
题目3:合并两个有序链表
题目描述:将两个升序链表合并为一个新的升序链表并返回。新链表应该通过拼接给定的两个链表的所有节点组成。
例如,链表1为 1->2->4,链表2为 1->3->4,合并后的链表为 1->1->2->3->4->4。
解题思路:使用递归的方法,比较两个链表的头节点的值大小,将小的节点作为合并后的链表的头节点,然后递归合并剩下的节点。
代码实现:
function mergeTwoLists(l1: ListNode | null, l2: ListNode | null): ListNode | null {
if (!l1) {
return l2;
} else if (!l2) {
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;
}
}
前端框架源码实战
除了LeetCode题目,链表在前端框架源码中也有许多使用实例。下面以Vue.js为例,介绍如何在前端框架中使用链表。
Vue.js中的响应式原理
Vue.js中的响应式原理是通过使用链表来实现的。Vue.js通过将每个组件实例的数据对象包装成一个响应式对象,当数据对象发生变化时,会自动触发视图的重新渲染。Vue.js使用了一个Watcher类来实现这个功能,Watcher类中维护了一个链表,用于存储当前组件实例对应的所有响应式对象的Dep对象。
class Watcher {
public deps: Dep[] = [];
// ...
public addDep(dep: Dep) {
if (!this.deps.includes(dep)) {
this.deps.push(dep);
dep.addSub(this);
}
}
}
class Dep {
public subs: Watcher[] = [];
// ...
public addSub(sub: Watcher) {
this.subs.push(sub);
}
public notify() {
this.subs.forEach(sub => sub.update());
}
}
在Watcher类中,我们维护了一个deps数组,deps数组中存储了当前组件实例对应的所有响应式对象的Dep对象。在addDep方法中,我们向deps数组中添加新的Dep对象,并调用Dep对象的addSub方法将当前Watcher对象添加到Dep对象的subs数组中。
在Dep类中,我们维护了一个subs数组,subs数组中存储了所有订阅该Dep对象的Watcher对象。在notify方法中,我们遍历subs数组,依次调用每个Watcher对象的update方法,触发对应的视图更新。
总结
本文介绍了链表的基本概念和常见操作,以及链表在LeetCode题目和前端框架源码中的应用实例。链表作为一种常用的数据结构,在算法和框架开发中都有着重要的作用,它的学习和掌握对于提高编程能力和开发效率都具有重要的意义。