前言
日常开发中我们用的最多的应该就是数组了,数组可以满足我们大部分的业务开发要求,但是如果是一些对性能要求高的工具或者是框架实现来说,数组可能并不是一个最佳的方案,比如react的最新实现就是用了链表
数组
定义
是一个连续的结构,靠下标查找速度会比较快,时间复杂度O(1)
特点
插入和删除会比较慢,因为需要移动元素,时间复杂度O(N)
链表
定义
是一个不连续的结构,只能遍历查找速度会比较慢,时间复杂度O(N)
特点
插入和删除会比较快,不需要移动元素,只需要考虑相邻结点的指针改变,时间复杂度O(1)
单向链表
单向链表的结构大概是这样的
{
element: first,
next: {
element: second,
next: {
element: third,
next: null
}
}
}
实现
class LinkList {
constructor() {
}
#head = null;
#length = 0;
#Node = class{
constructor(element) {
this.element = element;
this.next = null;
}
}
append = (element) => {
const node = new this.#Node(element);
let currentNode = this.#head;
if(this.#head){
while(currentNode.next){
currentNode = currentNode.next;
}
currentNode.next = node;
} else{
this.#head = node;
}
this.#length++;
}
insert = (position, element) => {
if(position < 0 || position >= this.#length){
return false;
}
const node = new this.#Node(element);
let previousNode;
let currentNode = this.#head;
if(position === 0){
node.next = this.#head;
this.#head = node;
}
for(let i = 0; i < position; i++){
previousNode = currentNode;
currentNode = currentNode.next;
}
previousNode.next = node;
node.next = currentNode;
this.#length ++;
}
// 从链表的特定位置移除一项
removeAt = (position) => {
if ((position < 0 && position >= this.#length) || this.#length === 0) {
// 越界
return false;
} else {
var currentNode = this.#head;
var previousNode;
if (position === 0) {
this.#head = currentNode.next;
} else {
// 循环找到位置
for(let i = 0; i < position; i++){
previousNode = currentNode;
currentNode = currentNode.next;
}
// 把当前节点的 next 指针 指向 当前节点的 next 指针,即是 删除了当前节点
previousNode.next = currentNode.next;
}
this.#length--;
}
};
// 从链表中移除指定项
remove = (element) => {
var index = this.indexOf(element);
return this.removeAt(index);
};
list = () => {
return this.#head;
};
// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
toString = () => {
var currentNode = this.#head;
var string = '';
while (currentNode) {
string += ',' + currentNode.element;
currentNode = currentNode.next;
}
return string.slice(1);
};
// 打印链表数据
print = () => {
console.log(this.toString());
};
// 返回元素在链表的索引,如果链表中没有该元素则返回 -1
indexOf = (element) => {
var currentNode = this.#head;
var index = 0;
while (currentNode) {
if (currentNode.element === element) {
return index;
}
index++;
currentNode = currentNode.next;
}
return -1;
};
}
const linkList = new LinkList();
linkList.append('Tom');
linkList.append('Peter');
linkList.append('Paul');
linkList.insert(1, 'Jack');
linkList.print(); // "Tom,Jack,Peter,Paul"
双向链表
双向链表的结构大概是这样的,其实和单向链表差不多
{
element: first,
next: {
element: second,
next: {
element: third,
next: null
},
prev: {
element: first,
next: {
element: second,
next: {
element: third,
next: null
},
prev: ...
}
},
},
prev: null,
}
实现
class DoubleLinkList {
constructor() {
}
#head = null;
#length = 0;
#tail = null;
#Node = class {
constructor(element) {
this.element = element;
this.next = null;
this.previous = null; //上一个节点指针
}
}
append = (element) => {
const node = new this.#Node(element);
let currentNode = this.#tail;
if (currentNode) {
currentNode.next = node;
node.prev = currentNode;
this.#tail = node;
} else {
this.#head = node;
this.#tail = node;
}
this.#length++;
}
insert = (position, element) => {
if (position < 0 || position > this.#length) {
return false;
}
const node = new this.#Node(element);
let previousNode;
let currentNode = this.#head;
if (position === 0) {
if (this.#head) {
node.next = currentNode;
currentNode.prev = node;
this.#head = node;
} else {
node.next = this.#head;
this.#head = node;
}
} else if (position === length) {
this.append(element);
} else {
for (let i = 0; i < position; i++) {
previousNode = currentNode;
currentNode = currentNode.next;
}
previousNode.next = node;
node.next = currentNode;
node.prev = previousNode;
currentNode.prev = node;
}
this.#length ++;
}
// 从链表的特定位置移除一项
removeAt = (position) => {
if ((position < 0 && position >= this.#length) || this.#length === 0) {
// 越界
return false;
} else {
let currentNode = this.#head;
let previousNode;
if (position === 0) {
// 移除第一项
if (this.#length === 1) {
this.#head = null;
this.#tail = null;
} else {
this.#head = currentNode.next;
this.#head.prev = null;
}
} else if (position === this.#length - 1) {
// 移除最后一项
if (this.#length === 1) {
this.#head = null;
this.#tail = null;
} else {
currentNode = this.#tail;
this.#tail = currentNode.prev;
this.#tail.next = null;
}
} else {
for (let i = 0; i < position; i++) {
previousNode = currentNode;
currentNode = currentNode.next;
}
previousNode.next = currentNode.next;
previousNode = currentNode.next.prev;
}
this.#length--;
return true;
}
}
// 从链表中移除指定项
remove = (element) => {
const index = this.indexOf(element);
return this.removeAt(index);
};
list = () => {
return this.#head;
};
// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
toString = () => {
let currentNode = this.#head;
let string = '';
while (currentNode) {
string += ',' + currentNode.element;
currentNode = currentNode.next;
}
return string.slice(1);
};
// 打印链表数据
print = () => {
console.log(this.toString());
};
// 返回元素在链表的索引,如果链表中没有该元素则返回 -1
indexOf = (element) => {
let currentNode = this.#head;
let index = 0;
while (currentNode) {
if (currentNode.element === element) {
return index;
}
index++;
currentNode = currentNode.next;
}
return -1;
};
}
const doubleLinkList = new DoubleLinkList();
doubleLinkList.append('Tom');
doubleLinkList.append('Peter');
doubleLinkList.append('Paul');
doubleLinkList.insert(1, 'Jack'); // "Tom,Jack,Peter,Paul"
单向链表与双向链表比较
- 双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。 所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。 虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
- 双向链表提供了两种迭代列表的方法:从头到尾,或者从尾到头。 我们可以访问一个特定节点的下一个或前一个元素。
- 在单向链表中,如果迭代链表时错过了要找的元素,就需要回到链表起点,重新开始迭代。
- 在双向链表中,可以从任一节点,向前或向后迭代,这是双向链表的一个优点。
- 所以,双向链表可以支持 O(1) 时间复杂度的情况下找到前驱结点,正是这样的特点,也使双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。
循环链表
循环链表的结构大概是这样的,就是尾部会指回头部形成一个闭环
{
element: first,
next: {
element: second,
next: {
element: first,
next: {
element: second,
next: {
element: first,
next: {
element: second,
next: ...
}
}
}
}
}
}
实现
class CircularLinkList {
constructor() {
}
#head = null;
#length = 0;
#Node = class{
constructor(element) {
this.element = element;
this.next = null;
}
}
append = (element) => {
const node = new this.#Node(element);
let currentNode = this.#head;
if(this.#head){
while(currentNode.next !== this.#head){
currentNode = currentNode.next;
}
currentNode.next = node;
node.next = this.#head;
} else{
this.#head = node;
node.next = this.#head;
}
this.#length++;
}
insert = (position, element) => {
if(position < 0 || position >= this.#length){
return false;
}
const node = new this.#Node(element);
let previousNode;
let currentNode = this.#head;
if(position === 0){
node.next = this.#head;
this.#head = node;
}
for(let i = 0; i < position; i++){
previousNode = currentNode;
currentNode = currentNode.next;
}
previousNode.next = node;
node.next = currentNode;
this.#length ++;
}
// 从链表的特定位置移除一项
removeAt = (position) => {
if ((position < 0 && position >= this.#length) || this.#length === 0) {
// 越界
return false;
} else {
let currentNode = this.#head;
let previousNode;
if (position === 0) {
this.#head = currentNode.next;
} else {
// 循环找到位置
for(let i = 0; i < position; i++){
previousNode = currentNode;
currentNode = currentNode.next;
}
// 把当前节点的 next 指针 指向 当前节点的 next 指针,即是 删除了当前节点
previousNode.next = currentNode.next;
}
this.#length--;
}
};
// 从链表中移除指定项
remove = (element) => {
const index = this.indexOf(element);
return this.removeAt(index);
};
list = () => {
return this.#head;
};
// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
toString = () => {
let currentNode = this.#head;
let string = '';
for(let i = 0; i < this.#length && currentNode; i++){
string += ',' + currentNode.element;
currentNode = currentNode.next;
}
return string.slice(1);
};
// 打印链表数据
print = () => {
console.log(this.toString());
};
// 返回元素在链表的索引,如果链表中没有该元素则返回 -1
indexOf = (element) => {
let currentNode = this.#head;
let index = 0;
while (currentNode) {
if (currentNode.element === element) {
return index;
}
index++;
currentNode = currentNode.next;
}
return -1;
};
}
const circularLinkList = new CircularLinkList();
circularLinkList.append('Tom');
circularLinkList.append('Peter');
circularLinkList.append('Paul');
circularLinkList.insert(1, 'Jack');
circularLinkList.print(); // "Tom,Jack,Peter,Paul"
总结
为什么面试官喜欢问链表,其实链表的实现挺难的,很多地方都是依赖同一个指针,很容易就会出现bug,想彻底掌握只有多写多练,没什么其他的捷径了