开启掘金成长之旅!这是我参与「掘金日新计划 12 月更文挑战」的第 4 天,点击查看活动详情
最近在学习 Javascript 数据结构与算法相关知识,数据结构与算法对程序员来说就像是内功心法,只有不断修炼自己的内功,才能从容不迫的应对市场上各种让人应接不暇的框架,达到以不变应万变。学习数据结构与算法的过程中不仅要能看懂更要多写多练,今天就来手写下链表数据结构。
每种语言都实现了数组。这种数据结构有一个缺点:(在大多数语言中)数组的大小是固定的,从数组的起点或中间插入或移 除项的成本很高,因为需要移动元素。
数组需要一块连续的内存空间来存储,对内存的要求比较高。 链表不需要一块连续的内存空间来存储,它通过“指针”将一组零散的内存块串联起来使用
手写单链表结构
链表基本操作方法有:
- push(element):向链表尾部添加一个新元素。
- insert(element, position):向链表的特定位置插入一个新元素。
- getElementAt(index):返回链表中特定位置的元素。如果链表中不存在这样的元素,则返回 undefined。
- remove(element):从链表中移除一个元素。
- indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回-1。
- removeAt(position):从链表的特定位置移除一个元素。
- isEmpty():如果链表中不包含任何元素,返回 true,如果链表长度大于 0 则返回 false。
- size():返回链表包含的元素个数,与数组的 length 属性类似。
- toString():返回表示整个链表的字符串。由于列表项使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。
class Node<T> {
constructor(public element: T, public next?: Node<T>) {}
}
class LinkedList<T> {
protected count = 0; // 存储链表中元素数量
protected head: Node<T> | undefined; // 头指针
push(element: T) {
const node = new Node(element);
let current: Node<T>;
if (this.head) {
current = this.head;
// 遍历链表
while (current.next && current.next !== null) {
current = current.next;
}
// 将current的next指针指向新节点
current.next = node;
} else {
this.head = node;
}
this.count++;
}
// 根据下标查找节点
getElementAt(index: number) {
if (index >= 0 && index < this.count) {
let node = this.head;
for (let i = 0; i < index && node !== null; i++) {
node = node?.next;
}
return node;
}
return undefined;
}
//从链表中移除元素
removeAt(index: number) {
// 越界检查
if (index >= 0 && index < this.count) {
let current = this.head;
if (index === 0) {
//头节点
this.head = current?.next;
} else {
// 中间节点
const previous = this.getElementAt(index - 1); // 当前节点的前节点引用
if (previous) {
current = previous.next;
// 跳过current节点 从而将其删除
previous.next = current?.next;
}
}
this.count--;
return current?.element;
}
return undefined;
}
// 链表中间插入节点
insert(element: T, index: number) {
if (index >= 0 && index < this.count) {
const node = new Node(element);
if (index === 0) {
const current = this.head;
node.next = current;
this.head = node;
} else {
const previous = this.getElementAt(index - 1);
if (previous) {
const current = previous.next;
node.next = current;
previous.next = node;
}
}
this.count++;
return true;
}
return false;
}
// 返回一个元素的位置
indexOf(element: T) {
let current = this.head;
for (let i = 0; i < this.count && current !== null; i++) {
if (element === current?.element) {
return i;
}
current = current?.next;
}
return -1;
}
// 从链表中移除元素
remove(element: T) {
const index = this.indexOf(element);
return this.removeAt(index);
}
size() {
return this.count;
}
isEmpty() {
return this.size() === 0;
}
toString() {
if (this.head === null) {
return "";
}
let objString = `${this.head?.element}`;
let current = this.head?.next;
for (let i = 1; i < this.size() && current !== null; i++) {
objString = `${objString},${current?.element}`;
current = current?.next;
}
return objString;
}
}
const list = new LinkedList();
list.push(5);
list.push(10);
list.push(15);
list.removeAt(1);
console.log(list.insert(20, 1));
console.log(list.toString());
使用场景
leetcode 练习题:83
删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
/*
* @param {ListNode} head
* @return {ListNode}
*/
var deleteDuplicates = function (head) {
let p = head;
while (p && p.next) {
if (p.val === p.next.val) {
p.next = p.next.next; // 删除重复节点
} else {
p = p.next;
}
}
return head;
};
解题思路
由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。
具体地,我们从指针 p 指向链表的头节点,随后开始对链表进行遍历。如果当前 p 与 p.next 对应的元素相同,那么我们就将 p.next 从链表中移除;否则说明链表中已经不存在其它与 p 对应的元素相同的节点,因此可以将 p 指向 p.next。
当遍历完整个链表之后,我们返回链表的头节点即可。