之前我们知道了 单向链表 不知道的请移步这儿
单向链表具备这些特点
- 只能从头遍历到尾或者从尾遍历到头(一般从头到尾),也就是链表相连的过程是单向的
- 实现的原理是上一个链表中有一个指向下一个的引用
- 我们可以轻松的到达下一个节点,但是回到前一个节点是很难的.但是,在实际开发中,经常会遇到需要回到上一个节点的情况
为了解决这个缺点,就有了 双向链表
双向链表
现在我们来说说 双向链表
- 既可以从头遍历到尾,又可以从尾遍历到头 就像地铁,可以顺着开,也能倒着开,简直是拿捏!!
- 一个节点既有向前连接的引用,也有一个向后连接的引用
- 每次在插入或删除某个节点时,需要处理四个引用,而不是两个,实现起来要困难一些
- 并且相当于单向链表,必然占用内存空间更大一些.但是这些缺点和我们使用起来的方便程度相比,是微不足道的
- 可以使用一个head和一个tail分别指向头部和尾部的节点
- 每个节点都由三部分组成: 前一个节点的指针(prev) 保存的元素item) 后一个节点的指针(next)
- 双向链表的第一个节点的prev是null
- 双向链表的最后的节点的next是null
话不多说 直接开干
// 双向链表的基本结构 类
class BothWayLinkList {
constructor() {
this.head = null; // 头
this.tail = null; // 尾
this.length = 0;
};
// add
add(data) {
const newNode = new Node(data);
// 判断是不是第一个
if (this.length === 0) {
// 第一个的时候 这个既是第一个 也是最后一个
this.head = newNode;
this.tail = newNode;
} else {
/*
注意添加的逻辑 :
增加到原来最后一个的后面 即:新元素的上一个(prev)指针指向最后一个
原本的最后一个(tail)的下一个(next)指针是指向新元素的; 这两步是把新旧元素建立双向连接
最后一个这个帽子要给新元素(老幺的称呼顺延了);
*/
newNode.prev = this.tail;
this.tail.next = newNode;
this.tail = newNode;
}
this.length++;
};
// backward
backWardToString() {
let result = "",
current = this.head;
while (current) {
result += current.data;
current = current.next;
}
return result;
};
// forward
forWardToString() {
let result = "",
current = this.tail;
while (current) {
result += current.data;
current = current.prev;
}
return result;
};
// toString
toString = () => {
return this.backWardToString();
};
// insert
insert(position, data) {
if (position < 0 || position > this.length) return;
const newNode = new Node(data);
if (this.length === 0) {
this.add();
return;
} else {
if (position === 0) {
/* 添加到第一个:
原来第一个(head)的 prev指针 指向新元素;
新元素的 next指针 指向原来的第一个;
新元素成了老大(head),享受老大的称呼(head);
*/
this.head.prev = newNode;
newNode.next = this.head;
this.head = newNode;
} else if (position === this.length) {
/*
相当于末尾新增:
原来的最后一个(tail)的 next指针 指向 新元素;
新元素的 prev指针 指向tail;
新元素成了老幺(tail),享受老大的称呼(tail);
*/
this.tail.next = newNode;
newNode.prev = this.tail;
this.tail = newNode;
} else {
let current = this.head,
index = 0;
while (index++ < position) {
current = current.next;
}
/*
又是熟悉的故事 前任和现任的故事 现任要砍断前任和ta的联系 自己取而代之
*/
newNode.prev = current.prev; // 获取到ta的联系方式 斩断了前任对ta的联系
newNode.next = current; //自己的手(next)牢牢牵住ta (current)
/* 目前为止 你只是斩断了 前任对ta的联系 有了ta的联系方式 但是他还没承认你 也还没斩断 ta对前任的联系 */
current.prev.next = newNode; // 斩断ta对前任的联系 然后建立新的联系 进入生活
current.prev = newNode; // ta主动承认你
}
this.length += 1;
}
};
// getData
getData(data) {
let current = this.head;
while (current) {
if (current.data === data) {
return current.data;
}
current = current.next;
}
return "恭喜你,你连一根毛也没找到哦";
};
// getNum 优化效率
getNum(position) {
if (position < 0 || position >= this.length) return;
// 正常的遍历
// let current = this.head,
// index = 0;
// while (index++ < position) {
// current = current.next;
// }
// return current.data;
/*
这儿每次查找 要么是从头到尾 要么就是从尾到头 做了很多无用功
可以判断一下position 在这个号链表中的位置 然后去这个区域 定点捕捞 岂不是更有效果?
this.length / 2 ====>相当于把链表分成了前后两部分
this.length / 2 > position 说明position 很小 在前面那部分 那就从前往后遍历
this.length / 2 < position 说明position 比较大 在后面那部分 那就从后往前遍历
*/
let median = this.length / 2;
const flag = median > position ? true : false; // true:从前往后遍历 false:从后往前遍历
let current = flag ? this.head : this.tail;
let index = flag ? 0 : this.length - 1; // 因为 index是从0 开始算 传过来的数字也是从0 开始算 length 没有算0 就少一个 就减去1
if (flag) {
console.log("从前往后遍历");
while (index++ < position) {
current = current.next;
}
} else {
console.log("从后往前遍历");
while (index-- > position) {
current = current.prev;
}
}
return current.data;
};
// indexOf
indexOf(data) {
let current = this.head,
index = 0;
while (current) {
if (current.data === data) {
return index;
}
current = current.next;
index += 1;
}
return -1;
};
// update
update(position, data) {
// if (position < 0 || position >= this.length) return false;
// let current = this.head,
// index = 0;
// while (index++ < position) {
// current = current.next;
// }
// current.data = data;
// return true;
// 也可以判断从前往后还是从后往前 做优化
let m = this.length / 2;
let flag = m > position ? true : false // true 在左边 false 在右边
let current = flag ? this.head : this.tail;
let index = flag ? 0 : this.length - 1;
if (flag) {
while (index++ < position) {
current = current.next;
}
current.data = data
} else {
while (index-- > position) {
current = current.prev;
}
current.data = data
}
return true
};
// removeAt
/*
四种情况:
1.只有一个
2.删除的是第一个
3.删除的是最后一个
4.删除的是其他的
删除其实就是指 没有引用指向某一个数据,这个数据会被自动回收 (也就是把指针设置为null) 强调的是 斩断指向关系
为什么不直接把 某个数据设置为null 不能够啊 直接设置为null 他的引用关系怎么处理呢(某一个成了null 上下关系就走不通了 短路了 所以就只
有饶过他 直接从他身上过去)
所以直接把他孤立出去 让他掉队 就好了 不像新增那么麻烦,切断就联系还需要建立新的联系
例如;
this.head.next.prev=null ===> this.head.next 是head的下一个 this.head.next.prev 是head自己 这儿强调的是 斩断指向关系
那为什么不直接写this.head==null ? 因为我们的next 和 next都是指针 这个代表指向关系 不代表数据本身
this.head.next.prev只是指向head,但是并不是head this.head.next.prev=null 只是斩断连接关系 不是把head设置成了null
*/
removeAt(position) {
if (position < 0 || position >= this.length) return null;
// 只有一个
let current = this.head;
if (this.length === 1) {
this.head = null;
this.tail = null;
} else {
// 删除的是第一个 只需要把第一个抛弃 然后第二个顶上来成为第一个
if (position === 0) {
this.head.next.prev = null; // head的下一个(老二) 不再往上指
this.head = this.head.next; // 老二顶上来
} else if (position === this.length - 1) {
// 最后一个 只需要把最后一个抛弃 然后倒数第二个顶上来成为最后一个
current = this.tail;
this.tail.prev.next = null; // 拒绝给老幺 供给 切断和他的联系
this.tail = this.tail.prev; // 在团队成立新的老幺 你的一切都有人代替
} else {
// 其他的 那就饶过他 直接通信
let index = 0;
while (index++ < position) {
current = current.next;
}
current.prev.next = current.next; // current.prev :被删除的上一个 current.next:被删除的下一个 这两个直接建立通信 兄弟 你被抛弃了
current.next.prev = current.prev;
// 这儿不像增加 因为增加时要切断 之前旧的联系的同时 还需要建立进的联系 这儿只需要切断旧的联系即可!
}
}
this.length -= 1;
return current.data;
};
// remove
remove(data) {
const num = this.indexOf(data);
if (num >= 0) {
return this.removeAt(num);
}
};
}
// 新数据结构 节点类 构造链表数据的构造函数
class Node {
constructor(data) {
this.data = data;
this.prev = null; // 向上的指针
this.next = null; // 向下的指针
}
}
const myLinkedList = new BothWayLinkList();
// myLinkedList.add("地");
// myLinkedList.add("势");
// myLinkedList.add("坤");
// myLinkedList.add("君");
// myLinkedList.add("子");
// myLinkedList.add("以");
// myLinkedList.add("厚");
// myLinkedList.add("德");
// myLinkedList.add("载");
// myLinkedList.add("物");
// myLinkedList.insert(0, "N");
// myLinkedList.insert(2, "I");
// myLinkedList.insert(12, "lao");
// console.log("getData", myLinkedList.getData("sad"));
// console.log("getData", myLinkedList.getData("N"));
// console.log("getData", myLinkedList.getData("lao"));
// console.log("getNum", myLinkedList.getNum(0));
// console.log("getNum", myLinkedList.getNum(10));
// console.log("getNum", myLinkedList.getNum(8));
// console.log("indexOf", myLinkedList.indexOf("N"));
// console.log("indexOf", myLinkedList.indexOf("德"));
// console.log("indexOf", myLinkedList.indexOf("以"));
// console.log("update", myLinkedList.update(0, "0"));
// console.log("update", myLinkedList.update(8, "0"));
// console.log("myLinkedList", myLinkedList);
// console.log("getNum", myLinkedList.getNum(0));
// console.log("getNum", myLinkedList.getNum(10));
// console.log("getNum", myLinkedList.getNum(8));
// console.log("backWardToString", myLinkedList.backWardToString());
// console.log("forWardToString", myLinkedList.forWardToString());
// console.log("removeAt", myLinkedList.removeAt(0));
// console.log("removeAt", myLinkedList.remove("I"));
// console.log("removeAt", myLinkedList.removeAt(10));
// console.log("toString", myLinkedList.toString());