链表
链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同
分类
- 单向链表
- 双向链表
- 单向循环链表
- 双向循环链表
数组与链表
- 要存储多个元素,数组或称为列表可能是最常用的数据结构。
- 每一种语言都有默认实现数组结构
- 但是数组也有很多的缺点,数组得创建通常需要申请一大段连续的空间(一整块内存)
- 大小是固定的,当数组容量不够时,就需要扩容
- `在数组的开头或中间位置插入或者删除元素成本恒高,需要大量元素的位移`
- 链表的优势
- 链表的元素在内存中不必是一段连续的空间,可以充分利用计算机的内存,实现灵活的计算机
动态管理
- 链表的每一个元素由一个存储元素本身的节点和指向下一个元素的引用(
指针
)组成 - 链表不必在创建时就确定大小,并且大小可以无限延伸(和javascript数组差不多哈)
- 链表在插入和删除数据时,
时间复杂度
可以达到O(1),相对于数据结构效率要高很多
- 链表的元素在内存中不必是一段连续的空间,可以充分利用计算机的内存,实现灵活的计算机
- 链表的缺点
- 链表访问任意一个位置的元素时,都需要从头开始访问,(无法跳过第一个元素访问任何一个元素)
- 无法通过下标访问任意一个元素
- 结构
- 就相当于火车结构,火车头通过节点连接(指针指向)下一个车厢,车厢上有乘客(数据),这个
节点指向下一个车厢,车厢上也有乘客(数据),以此类推,最后的车厢上无节点
- 就相当于火车结构,火车头通过节点连接(指针指向)下一个车厢,车厢上有乘客(数据),这个
单向链表图解
实现单向链表
链表的常见操作
- append(ele):向链表尾部添加一个新的数据
- insert(position, ele):向链表的指定位置添加新的项
- get(position):获取指定位置的元素
- indexOf(ele):返回元素在链表中的索引,如果链表中没有该元素则返回-1
- update(position, ele):修改某一个位置的元素
- removeAt(position);删除链表一位置的元素
- remove(ele):从链表中删除一项
- isEmpty():如果链表长度为0,返回true,否则返回false
- size():返回链表的元素个数,与数组的length属性相似
- toString():只输出元素的值
/*
* 手动实现单向链表
* */
// 使用class类实现单向链表
class List {
private next: { data } = null;
private length: number = 0;
// ------------------------
// 链表插入数据方法
append<T>(data: T): object {
// 实例一个新的数据
let obj = new NNode(data);
// 判断链表长度是否为空
if (this.length == 0) {
// 为空则将头部的next指向 指向添加的第一个数据
this.next = obj;
// 链表长度加一
this.length++;
// 返回添加的数据
return obj;
} else {
// 链表长度大于一
// 创建变量指针centerPointer指向当前链表指针
let centerPointer: { data: T, next };
centerPointer = this.next as { data: T, next };
// 判断当前指针变量的next中有没有下一个数据(是否为空,
// 为空则表示现在指针所在的数据是最后一个数据)
while (centerPointer.next) {
// 当前链表循环的数据不是最后的数据时讲下一个指针指向当前数据
centerPointer = centerPointer.next;
}
// 那么将这个链表最后数据指针指向新添加的数据
centerPointer.next = obj;
this.length++;
// 返回最后指针指向的数据
return centerPointer;
}
}
// ------------------------
// 向链表的指定位置添加数据
insert<T>(position: number, data: T): void {
// 判断插入位置是否合理
if (position > this.length) {
throw new Error('此位置无法插入数据');
} else if (position < 0) {
throw new Error('传入数据不合理');
}
// 创建变量
// 临时节点变量
let timeNode: { data: T, next },
// 创建的实例变量
newN: { data: unknown, next?: object },
// 定位变量
index: number,
// 接收添加值后面数据的指针
preNext: { data: T, next };
// 临时节点等于当前header指针
timeNode = this.next as { data: T, next };
// 将传入的数据实例为对象
newN = new NNode(data);
index = 0;
preNext = null;
// 当新增插入位置为0时
if (position === 0) {
// 让当前创建的节点指向头部指向的节点;
newN.next = this.next;
// 使头部节点重新指向新插入的节点
this.next = newN;
} else {
// 循环的目的是获取到插入目标节点位置之前的节点,方便使用之前节点的next指针,
// 获取到插入目标节点
while (index++ < position) {
// 获得定位到的节点前面的节点
preNext = timeNode;
// console.log(preNext)
// 使得临时节点等于当前指针指向的后面的节点,
// 当index === 定位位置的时候,此时临时节点变为定位插入位置的节点
timeNode = timeNode.next;
// console.log(timeNode)
}
// 不满足循环条件时,即找到相应的定位位置
// 将插入节点之前的节点指针指向当前新增节点
preNext.next = newN;
// 将新增节点的指针指向临时节点,临时节点存储的是新增位置的节点
newN.next = timeNode;
}
// 长度改变
this.length++;
}
// ------------------------
// 更新某位置元素
update<T>(position: number, value: T): void {
// 创建临时节点和索引
let index: number,
timeNode: { data: T, next? };
// 初始化索引和节点
index = 0;
timeNode = this.next;
while (index < position) {
// 节点查找
timeNode = timeNode.next;
index++;
}
// 更改查找到节点的data
timeNode.data = value;
}
// ------------------------
// 获取指定位置的元素
get(position): unknown {
// 原理同upDate方法
let index: number,
timeNode: { data: unknown, next? };
index = 0;
timeNode = this.next;
while (index < position) {
timeNode = timeNode.next;
index++;
}
return timeNode.data;
}
// ------------------------
// 获取指定元素的下标索引
indexOf<T>(value: T): number {
let timeNode: { data: T, next? },
index: number;
timeNode = this.next;
index = 0;
do {
if (timeNode.data === value) {
return index;
}
// 使当前临时节点等于当前指针指向的下一节点
timeNode = timeNode.next;
index++;
} while (index < this.length);
return -1;
}
// ------------------------
// 指定位置删除链表
removeAt(position: number): void {
let timeNode: { data: unknown, next? },
index: number,
preNode: { data: unknown, next? };
timeNode = this.next;
index = 0;
preNode = null;
// 判断当删除位置为0时
if (position === 0) {
// 使当前head指针指向指向this.next的指针(跳过了this.next,因为timeNode = this.next)
this.next = timeNode.next;
} else {
while (index++ < position) {
// 获取到目标节点前一项的节点
preNode = timeNode;
// 获取到删除目标节点
timeNode = timeNode.next;
}
// 使删除节点前一节点的指针指向删除节点的指针指向
preNode.next = timeNode.next;
}
// 长度--
this.length--;
}
// ------------------------
//删除某一元素
remove<T>(value: T): void {
let i = this.indexOf<T>(value);
this.removeAt(i);
}
// ------------------------
// 打印链表元素的个数
get size(): number {
return this.length;
}
// ------------------------
// 拼接打印链表的数据
toString(): string {
// 创建节点存储指针
let node: { data: string, next? },
// 创建字符获取链表中各项的数据
str: string | null;
// 使此时的next指针引用到node变量指针上
node = this.next as { data: string, next? };
// 判断指针是否为空(也就是链表中没有插入数据时,此时头部的指针指向null)
if (this.next === null) {
// 添加提示字符
str = '链表中无数据';
} else {
// 指针指向不为空时。获取指针对应节点的数据
str = node.data;
// 遍历指针,当指针不为为空时
while (node.next) {
// 使当前指针引用下一级指针
node = node.next;
//将每一级的指针对应的数据拼接到字符串中
str += ',' + node.data;
}
}
// 返回最终的数据结果
return str;
}
// ------------------------
//判断元素是否为空
isEmpty(): boolean {
return !!this.length;
}
}
// 添加新的节点的类
class NNode {
data: unknown;
constructor(data: unknown) {
this.data = data;
}
}
测试
// 实例链表
let d1 = new List();
console.log('---------------添加---------------');
d1.append<string>('tom');
d1.append<string>('andy');
d1.append<string>('jeck');
d1.append<string>('zhansan');
d1.append<number>(101);
d1.append<string>('lidy');
d1.append<string>('rose');
console.log(d1.toString());
console.log('---------------插入---------------');
console.log(d1.toString());
d1.insert<string>(4, '插入值1');
d1.insert<string>(6, '插入值w');
d1.insert<string>(0, '头');
// d1.insert<string>(30,'错误插入值');
console.log(d1.toString());
console.log('---------------更新---------------');
console.log(d1.toString());
d1.update<string>(3, '更新值1');
console.log(d1.toString());
console.log('---------------获取---------------');
let s1 = d1.get(2);
//ject
console.log(d1.get(3));
//更新值1
console.log(d1.get(6));
//插入值w
console.log('s1:' + s1);
console.log('---------------获取位置---------------');
console.log(d1.toString());
console.log(d1.indexOf<number>(101), d1.get(5));
// 5 101
console.log('---------------指定位置移除---------------');
console.log(d1.toString());
d1.removeAt(2);
d1.removeAt(0);
console.log(d1.toString());
console.log('---------------指定内容移除---------------');
console.log(d1.toString());
d1.remove<string>('插入值w');
console.log(d1.toString());
console.log('---------------元素长度---------------');
console.log(d1.size);
// 9
console.log('---------------元素为空?---------------');
console.log(d1.isEmpty());
---------------添加---------------
tom,andy,jeck,zhansan,101,lidy,rose
---------------插入---------------
tom,andy,jeck,zhansan,101,lidy,rose
头,tom,andy,jeck,zhansan,插入值1,101,插入值w,lidy,rose
---------------更新---------------
头,tom,andy,jeck,zhansan,插入值1,101,插入值w,lidy,rose
头,tom,andy,更新值1,zhansan,插入值1,101,插入值w,lidy,rose
---------------获取---------------
更新值1
101
s1:andy
---------------获取位置---------------
头,tom,andy,更新值1,zhansan,插入值1,101,插入值w,lidy,rose
6 插入值1
---------------指定位置移除---------------
头,tom,andy,更新值1,zhansan,插入值1,101,插入值w,lidy,rose
tom,更新值1,zhansan,插入值1,101,插入值w,lidy,rose
---------------指定内容移除---------------
tom,更新值1,zhansan,插入值1,101,插入值w,lidy,rose
tom,更新值1,zhansan,插入值1,101,lidy,rose
---------------元素长度---------------
7
---------------元素为空?---------------
true
注意在链表的插入和删除的时候要考虑插入头部和尾部时的指针指向问题(上一个节点的下一级指针必须指向新增节点,新增节点的下一级指针指向下一个节点,插入头部和尾部都必须考虑)。
双向链表
- 单向链表
- 只能从头遍历到尾或者从尾遍历到头
- 也就是链表相连的的过程是单向的
- 节点可轻松达到下一节点,但无法回到上一个节点(只能从头遍历)
- 双向链表
- 既可以从头到尾遍历,又可以同尾到头遍历
- 一个节点既有向前连接的引用,也有一个向后连接的引用
- 在每次插入和删除节点的时候,需要处理四个引用,实现起来较为复杂
- 占用空间大一些
双向链表图解
实现双向链表
// 封装实现双向链表
type timeType = { value: unknown, prePointer?, nextPointer? };
class WayList {
// 头部指针
private headNext: timeType = null;
// 尾部指针
private tail: timeType = null;
// 元素长度
private length: number = 0;
// 在尾部添加节点方法
append<T>(value: T): T {
// timeNode存储当前节点
let timeNode: { value: T, prePointer?, nextPointer? },
// preNode存储当前节点的前置指针指向的节点(也就是当前指针的prePointer)
preNode: { value: T, prePointer?, nextPointer? },
//添加的新节点
nNode: timeType;
// 实例节点
nNode = new Obj(value);
// 当前节点为this.headNext
timeNode = this.headNext as { value: T, prePointer?, nextPointer? };
// 前置指针指向节点默认为null
preNode = null;
// 判断添加节点时链表元素长度是否为空
if (this.length === 0) {
// 为空则将头部指向为添加的新节点
this.headNext = nNode;
// 尾部指针指向添加的新节点
this.tail = nNode;
} else {
// 不为空时,循环临时的当前节点
while (timeNode.nextPointer) {
// 获得当前节点(timeNode并未next的情况)的上一节点
preNode = timeNode;
// 使当前临时节点等于指针指向的下一节点
timeNode = timeNode.nextPointer;
}
// 使当前节点指向新添加的节点,相当于在链表末尾添加了新的节点
timeNode.nextPointer = nNode;
// 使当前节点的上一级指针指向当前节点的上一级节点
timeNode.prePointer = preNode;
// 新增节点的上一级指针指向当前节点
nNode.prePointer = timeNode;
// 使尾部指针指向新添加的节点
this.tail = timeNode.nextPointer;
}
// 长度+1
this.length++;
return value;
}
// 在指定位置插入数据
insert<T>(position: number, value: T): T {
// 创建当前节点,新增节点, 遍历控制变量
let timeNode: timeType,
nNode: timeType,
index: number = 0,
// 上一级节点
preNode: timeType;
// 初始化
timeNode = this.headNext;
nNode = new Obj(value);
preNode = null;
// 越界判断
if (position < 0 || position > this.length) {
throw new Error('此位置能插入数据');
}
// 当从第一个位置插入数据时
if (position === 0) {
// 当前新增节点指向的是头部指向的节点
nNode.nextPointer = timeNode;
// 使之前第一个元素的上一级指向更改为新增节点
// 这样新增元素节点就是第一个元素节点了
timeNode.prePointer = nNode;
// 使头部元素节点重新指向新增元素
this.headNext = nNode;
} else if (position === this.length) {
// 插入位置为最后一项
while (index++ < position) {
// 获取当前节点的上级节点
preNode = timeNode;
// 获取当前节点
timeNode = timeNode.nextPointer;
}
// 当前节点为null
// 新增节点的上级指针设置为当前节点的上一个节点
nNode.prePointer = preNode;
// 当前节点的上一个节点的下级指针指向新增节点
preNode.nextPointer = nNode;
// 在最后添加节点,尾部指向改变
this.tail = nNode;
} else {
// 新增位置并非首位
while (index++ < position) {
// 获取当前节点的上级节点
preNode = timeNode;
// 获取当前节点
timeNode = timeNode.nextPointer;
}
// !!!结合图
// 使上一节点下一级指针指向新增节点
preNode.nextPointer = nNode;
// 新增节点的上一级指针指向上一级节点
nNode.prePointer = preNode;
// 新增节点的下一级指针指向当前节点
nNode.nextPointer = timeNode;
// 当前节点的上一级指针指向新增节点
timeNode.prePointer = nNode;
}
this.length++;
return value;
}
//更新方法----和单向链表更新方法相似
update<T>(position: number, value: T): T {
let timeNode: timeType,
index: number;
timeNode = this.headNext;
index = 0
// 越界判断
if (position < 0 || position >= this.length) {
throw new Error('此位置无值');
}
while (index++ < position) {
timeNode = timeNode.nextPointer;
}
timeNode.value = value;
return value;
}
//获取指定位置的元素
get(position: number): unknown {
let timeNode: timeType,
index: number;
timeNode = this.headNext;
index = 0;
// 越界判断
if (position < 0 || position > this.length) {
throw new Error('此位置无值');
}
while (index++ < position) {
timeNode = timeNode.nextPointer;
}
return timeNode.value;
}
//指定元素返回对应下标(返回找到的第一个满足条件的元素)----与单向链表结构相似
indexOf<T>(value: T): number {
let timeNode: timeType,
index: number = 0;
timeNode = this.headNext;
while (timeNode.value !== value) {
timeNode = timeNode.nextPointer;
index++;
}
return index;
}
//指定位置删除元素
removeAt(position: number): number {
let timeNode: timeType,
//上一级节点
preNode: timeType,
index: number;
timeNode = this.headNext;
preNode = null;
index = 0;
// 越界判断
if (position < 0 || position > this.length) {
throw new Error('此位置无值');
}
//当删除第一个双向链表位置的元素
if (position === 0) {
// 将头部指向为第二个元素
timeNode = timeNode.nextPointer;
//让头部指向指向为创建的当前节点
this.headNext = timeNode;
// 使新的第一个元素的上一级指向指向为null
timeNode.prePointer = null;
} else {
while (index++ < position) {
// 获取到当前节点的上一级节点
preNode = timeNode;
// 获取到当前满足位置条件的节点
timeNode = timeNode.nextPointer;
}
//使上一级节点的下一级指向指向当前节点的下一级,即可满足删除当前节点的任务
preNode.nextPointer = timeNode.nextPointer;
/*
* 当删除的是最后一个元素时,我们要求尾部指针要变为最新的尾部节点
* 删除不是最后的节点,那么要处理好前后的指针问题
* */
// 当删除不是最后一个元素时
if (position !== this.length - 1) {
// 获得当前节点得下一个节点
timeNode = timeNode.nextPointer;
// 使下一级节点的上一级节点指向当前节点的上一级
timeNode.prePointer = preNode;
} else {
// 当删除最后一个元素时,更改尾部指针指向删除节点的前一个数据
this.tail = preNode;
}
}
this.length--;
return 1;
}
// 移除指定节点
remove<T>(value: T): number {
let i: number = this.indexOf(value);
this.removeAt(i);
return 0
}
//toString方法----与单向列表相似
toString(): string {
let timeNode: { value: unknown, prePointer?, nextPointer? },
str: string;
timeNode = this.headNext;
str = timeNode.value as string;
while (timeNode.nextPointer) {
timeNode = timeNode.nextPointer;
str += ' ' + timeNode.value;
}
return str;
}
// 反向遍历字符
backWordString(): string {
let timeNode: timeType,
str: string;
timeNode = this.tail;
str = this.tail.value + '\t';
while (timeNode.prePointer) {
timeNode = timeNode.prePointer;
str += timeNode.value + '\t';
}
console.log(str);
return '';
}
get Head() {
return this.headNext;
}
get tailNode() {
return this.tail;
}
}
// 添加一个实例节点类
class Obj {
// 新增节点的值
value: unknown;
// 新增节点的上一级指针
prePointer: timeType;
// 新增节点的下一级指针
nextPointer: timeType;
constructor(item: unknown) {
this.value = item;
this.prePointer = null;
this.nextPointer = null;
}
}
测试
let wlist = new WayList();
wlist.append<number>(101);
wlist.append<string>('张三');
wlist.append<string>('李四');
wlist.append<string>('王五');
wlist.append<string>('赵六');
console.log('---------------指定位置插入元素------------------');
console.log(wlist.toString());
// 头部插入
wlist.insert<number>(0, 100);
// 尾部插入
wlist.insert(6, '尾部');
wlist.insert<string>(3, '插入值1');
// wlist.insert<number>(7, 2000);
console.log(wlist.toString());
console.log('尾部: ' + wlist.tailNode.value);
console.log('---------------更新元素------------------');
wlist.update(1, 102);
wlist.update(0, 101);
console.log(wlist.toString());
console.log('---------------删除指定元素位置------------------');
console.log(wlist.indexOf<number>(101), wlist.indexOf('王五'));//0, 5
console.log('---------------获取指定位置元素------------------');
console.log(wlist.toString());
console.log(wlist.get(1));//102
console.log(wlist.get(4));//李四
console.log('---------------删除指定位置元素------------------');
wlist.insert<boolean>(4, false);
console.log(wlist.toString());
wlist.removeAt(8);
wlist.removeAt(4);
console.log(wlist.toString());
console.log('尾部: ' + wlist.tailNode.value);
console.log('---------------删除指定元素------------------');
wlist.remove<number>(101);
wlist.remove<string>('王五');
console.log('尾部: ' + wlist.tailNode.value);
// console.log(wlist.Head)
console.log(wlist.toString());
// // 反向遍历
console.log(wlist.backWordString());
---------------指定位置插入元素------------------
101 张三 李四 王五 赵六
100 101 张三 插入值1 李四 王五 赵六 尾部
尾部: 尾部
---------------更新元素------------------
101 102 张三 插入值1 李四 王五 赵六 尾部
---------------删除指定元素位置------------------
0 5
---------------获取指定位置元素------------------
101 102 张三 插入值1 李四 王五 赵六 尾部
102
李四
---------------删除指定位置元素------------------
101 102 张三 插入值1 false 李四 王五 赵六 尾部
101 102 张三 插入值1 李四 王五 赵六
尾部: 赵六
---------------删除指定元素------------------
尾部: 赵六
102 张三 插入值1 李四 赵六
赵六 李四 插入值1 张三 102
注意点大致和单向链表差不多,但是要注意多了一个尾部指针,而且每个节点还多了一个上一级指针。
也要注意插入删除的头部尾部情况