夯下数据结构和算法基础,JS 里没有栈、队列、链表巴拉巴拉明显的结构,只能用类去伪造,不然做算法题真的费劲。
这次用对象构建造链表相关类:单向链表、有序链表、循环链表、双向链表。
基本概念
- 链表是一种线性数据结构,其中的元素在内存中不是顺序存储的,而是通过指针相互连接
- 每个元素由一个存储元素本身的节点和一个指向下一个元素的引用组成
- 链表的第一个节点称为头节点,最后一个节点称为尾节点
- 链表的尾节点指向 null
- 链表的优点是添加和删除元素的时候不需要移动其他元素
- 链表的缺点是访问元素的时候需要从头节点开始遍历,直到找到指定元素
应用场景
队列的应用场景:寻宝游戏、火车。。。。书上没说编程的例子,先这么着吧
基本方法
- push: 向链表尾部添加节点
- getElementAt: 获取指定位置的节点
- removeAt: 删除指定位置的节点
- insert: 向指定位置插入节点
- indexOf: 获取指定节点的位置
- remove: 删除指定节点
- isEmpty: 判断链表是否为空
- size: 获取链表长度
- getHead: 获取链表头节点
- toString: 将链表转换成字符串
普通链表类实现
哦,对,先声明一个链表节点类:
/**
* @description: 链表节点
* value: 节点值
* next: 下一个节点
* @example:
* new NodeLink(1, new NodeLink(2, new NodeLink(3)))
*/
class NodeLink {
value: any;
next: NodeLink | null;
constructor(value: any, next: NodeLink | null = null) {
this.value = value;
this.next = next;
}
}
链表类:
class LinkedList {
count: number;
head: NodeLink | null;
tail: NodeLink | null;
constructor() {
this.count = 0;
this.head = null;
}
// 添加节点
push(value: any): void {
const node = new NodeLink(value);
// 如果head为null,将head指向新节点,然后返回
if (this.head === null) {
this.head = node;
return;
}
// 否则遍历链表,找到最后一个节点,将最后一个节点的next指向新节点
let current = this.head;
while (current.next !== null) {
current = current.next;
}
current.next = node;
}
// 获取指定位置的节点
getElementAt(index: number): NodeLink | undefined | null {
// 如果index小于0或者大于链表长度,直接返回false
if (index < 0 || index >= this.count) return undefined;
// 否则遍历链表,找到指定位置的节点,返回该节点
let current = this.head;
for (let i = 0; i < index; i++) {
current = (current as NodeLink).next;
}
return current;
}
// 删除指定位置的节点
removeAt(index: number): NodeLink | undefined | null {
// 如果index小于0或者大于链表长度,直接返回false
if (index < 0 || index >= this.count) return undefined;
let current = this.head;
// 如果删除第一个节点,需要将head指向当前节点的next,然后将当前节点的next指向null,最后返回当前节点
if (index === 0) {
this.head = (current as NodeLink).next;
this.count--;
return current;
}
// 如果删除其他节点,需要找到前一个节点,将前一个节点的next指向当前节点的next,然后将当前节点的next指向null,最后返回当前节点
let previous = this.getElementAt(index - 1);
current = (previous as NodeLink).next;
(previous as NodeLink).next = (current as NodeLink).next;
this.count--;
return current;
}
// 向指定位置插入节点
insert(value: any, index: number): boolean {
// 如果index小于0或者大于链表长度,直接返回false
if (index < 0 || index > this.count) return false;
// 如果index等于0,需要将head指向新节点,然后将新节点的next指向原来的head,最后返回true
const node = new NodeLink(value);
if (index === 0) {
node.next = this.head;
this.head = node;
this.count++;
return true;
}
// 如果index大于0,需要找到前一个节点,将前一个节点的next指向新节点,然后将新节点的next指向当前节点,最后返回true
let previous = this.getElementAt(index - 1);
node.next = (previous as NodeLink).next;
(previous as NodeLink).next = node;
this.count++;
return true;
}
// 获取指定节点的位置
indexOf(value: any): number {
let current = this.head;
// 遍历链表,如果当前节点的value等于value,返回当前节点的index
for (let i = 0; i < this.count; i++) {
if (current && current.value === value) return i;
current = (current as NodeLink).next;
}
// 如果遍历完链表都没有找到,返回-1
return -1;
}
// 删除指定节点
remove(value: any) {
// 找到value的index,然后调用removeAt方法删除
const index = this.indexOf(value);
return this.removeAt(index);
}
isEmpty() {
return this.count === 0;
}
size() {
return this.count;
}
getHead() {
return this.head;
}
toString() {
// 遍历链表,将每个节点的value拼接成字符串返回
if (this.head === null) return '';
let current = this.head;
let str = `${current.value}`;
while (current.next !== null) {
current = current.next;
str = `${str},${current.value}`;
}
return str;
}
}
可以头上顶个注释,就容易看到方法了
/**
* @description: 链表
* @param {*}
* @return {*}
* 链表是一种线性数据结构,其中的元素在内存中不是顺序存储的,而是通过指针相互连接
* 每个元素由一个存储元素本身的节点和一个指向下一个元素的引用组成
* 链表的第一个节点称为头节点,最后一个节点称为尾节点
* 链表的尾节点指向null
* 链表的优点是添加和删除元素的时候不需要移动其他元素
* 链表的缺点是访问元素的时候需要从头节点开始遍历,直到找到指定元素
* 链表的常见操作有:push,getElementAt,removeAt,insert,indexOf,remove,isEmpty,size,getHead,toString
*
* 属性:count,head
* count: 链表长度
* head: 链表头节点
* 方法:push,getElementAt,removeAt,insert,indexOf,remove,isEmpty,size,getHead,toString
* push: 向链表尾部添加节点
* getElementAt: 获取指定位置的节点
* removeAt: 删除指定位置的节点
* insert: 向指定位置插入节点
* indexOf: 获取指定节点的位置
* remove: 删除指定节点
* isEmpty: 判断链表是否为空
* size: 获取链表长度
* getHead: 获取链表头节点
* toString: 将链表转换成字符串
* @example:
* const linkedList = new LinkedList()
* linkedList.push(1)
* linkedList.push(2)
* linkedList.push(3)
* linkedList.push(4)
* linkedList.push(5)
* linkedList.removeAt(2)
* linkedList.insert(6, 2)
* linkedList.indexOf(4)
* linkedList.remove(4)
* linkedList.isEmpty()
* linkedList.size()
* linkedList.getHead()
* linkedList.toString() // 1,2,6,3,5
*/
有序链表类
有序链表是链表的一种,链表中的元素是有序的。
有序链表的元素必须是可比较的,因此需要传入一个比较函数。
有序链表继承自链表,只需要重写 insert 方法即可。
重写 insert 方法时,需要找到第一个比 value 大的元素的索引,然后调用父类的 insert 方法
class SortedLinkedList extends LinkedList {
compareFn: Function;
constructor(compareFn) {
super();
this.compareFn = compareFn;
}
// 获取下一个排序元素的索引,如果链表为空,返回0,如果链表不为空,返回第一个比value大的元素的索引
getIndexNextSortedElement(value: any): number {
let current = this.head;
let i = 0;
for (; i < this.count && current; i++) {
if (
this.compareFn(value, (current as any).element) === Compare.LESS_THAN
) {
return i;
}
current = (current as any).next;
}
return i;
}
// 重写insert方法,插入元素时,需要找到第一个比value大的元素的索引,然后调用父类的insert方法
insert(value: any): boolean {
if (this.isEmpty()) {
return super.insert(value, 0);
}
const index = this.getIndexNextSortedElement(value);
return super.insert(value, index);
}
}
循环链表类
循环链表和单向链表的区别在于,循环链表的最后一个节点的 next 指向 head,而单向链表的最后一个节点的 next 指向 null。 循环链表继承自单向链表,只需要重写 insert、removeAt 方法即可
class CircularLinkedList extends LinkedList {
constructor() {
super();
}
insert(value: any, index: number): boolean {
// 如果index小于0或者大于链表长度,直接返回false
if (index < 0 || index > this.count) return false;
// 如果index等于0,需要找到最后一个节点,将最后一个节点的next指向新节点,然后将新节点的next指向原来的head,最后将head指向新节点
const node = new NodeLink(value);
if (index === 0) {
let current = this.head;
while ((current as any).next !== this.head) {
current = (current as any).next;
}
(current as any).next = node;
node.next = this.head;
this.head = node;
this.count++;
return true;
}
// 如果index大于0,需要找到前一个节点,将前一个节点的next指向新节点,然后将新节点的next指向当前节点,最后返回true
let previous = this.getElementAt(index - 1);
node.next = (previous as NodeLink).next;
(previous as NodeLink).next = node;
this.count++;
return true;
}
removeAt(index: number): NodeLink | null {
// 如果index小于0或者大于等于链表长度,直接返回null
if (index < 0 || index >= this.count) return null;
// 如果index等于0,需要找到最后一个节点,将最后一个节点的next指向新节点,然后将新节点的next指向原来的head,最后将head指向新节点
if (index === 0) {
let current = this.head;
while ((current as any).next !== this.head) {
current = (current as any).next;
}
(current as any).next = (this.head as any).next;
const head = this.head;
this.head = (this.head as any).next;
this.count--;
return head;
}
// 如果index大于0,需要找到前一个节点,将前一个节点的next指向前一个节点的next.next,然后返回被删除的节点
let previous = this.getElementAt(index - 1);
const current = (previous as NodeLink).next;
(previous as NodeLink).next = (current as any).next;
this.count--;
return current;
}
}
双向链表类
双向链表是单向链表的改进版,每个节点有两个指针,一个指向前一个节点,一个指向后一个节点。
双向链表继承自单向链表,只需要重写 push、insert、removeAt、remove 方法即可。
class DoublyLinkedList extends LinkedList {
tail: DoublyNodeLink | null;
constructor() {
super();
this.tail = null;
}
push(value: any) {
// 向链表尾部添加节点,如果链表为空,将head和tail都指向新节点,
// 如果链表不为空,找到最后一个节点,将最后一个节点的next指向新节点,将新节点的prev指向最后一个节点,然后将tail指向新节点
const node = new DoublyNodeLink(value);
if (this.head === null) {
this.head = node;
this.tail = node;
this.count++;
return;
}
let current = this.head;
while (current.next !== null) {
current = current.next;
}
current.next = node;
(node as any).prev = current;
this.tail = node;
this.count++;
}
insert(value: any, index: number) {
// 如果index小于0或者大于链表长度,直接返回false
if (index < 0 || index > this.count) return false;
// 如果index等于0,需要将head指向新节点,然后将新节点的next指向原来的head,最后返回true
const node = new DoublyNodeLink(value);
if (index === 0) {
(node as any).next = this.head;
(this.head as DoublyNodeLink).prev = node;
this.head = node;
this.count++;
}
// 如果index等于链表长度,需要将tail指向新节点,然后将新节点的prev指向原来的tail,最后返回true
if (index === this.count) {
(this.tail as DoublyNodeLink).next = node;
node.prev = this.tail;
this.tail = node;
this.count++;
return true;
}
// 如果index大于0,需要找到前一个节点,将前一个节点的next指向新节点,然后将新节点的next指向当前节点,最后返回true
let previous = this.getElementAt(index - 1);
node.next = (previous as DoublyNodeLink).next;
(previous as DoublyNodeLink).next = node;
(node as any).prev = previous;
(node.next as DoublyNodeLink).prev = node;
this.count++;
return true;
}
removeAt(index: number) {
// 如果index小于0或者大于等于链表长度,直接返回null
if (index < 0 || index >= this.count) return null;
// 如果index等于0,需要将head指向head.next,然后返回被删除的节点
if (index === 0) {
const current = this.head;
this.head = (this.head as DoublyNodeLink).next;
this.count--;
return current;
}
// 如果index等于链表长度-1,需要将tail指向tail.prev,然后返回被删除的节点
if (index === this.count - 1) {
const current = this.tail;
this.tail = (this.tail as DoublyNodeLink).prev;
this.count--;
return current;
}
// 如果index大于0,需要找到前一个节点,将前一个节点的next指向前一个节点的next.next,然后返回被删除的节点
let previous = this.getElementAt(index - 1);
const current = (previous as DoublyNodeLink).next;
(previous as DoublyNodeLink).next = (current as DoublyNodeLink).next;
(current as any).next.prev = previous;
this.count--;
return current;
}
}
引用
- 《JavaScript 数据结构与算法》(希望我能看完)