认识双向链表
图解
双向链表结构封装
代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function DoublyLinkedList(){
// 属性
this.head = null;
this.tail = null;
this.length = 0;
// 内部类
function Node(data){
this.data = data;
this.prev = null;
this.next = null;
}
}
</script>
</body>
</html>
菜鸟反正现在感觉写这个结构封装很轻松,因为只要明白了需要哪些参数指向哪里,然后内部类就是节点的封装,里面封装的就是节点内容!
双向链表的常见操作
其实和单向链表差不多,只是封装的方式不同,提供给外面使用的方法大致是一致的,毕竟都是链表,都是增删改查!其实你又会发现链表的方法又和数组的很像,毕竟链表就是用来在某些时候代替数组而使操作更加简单!
append实现 -- 尾部添加项
菜鸟看视频,结果发现这个没有视频,只好自己写了,思路和单链表一样,后面找到了视频代码,发现自己写复杂了!
我的代码
// 1 append方法
DoublyLinkedList.prototype.append = function(data){
// 创建节点
let newNode = new Node(data);
// 判断插入地点
if(this.length === 0){
this.head = newNode;
this.tail = newNode;
}else{
let current = this.head;
// 找到最后一个节点
while(current.next){
current = current.next;
}
current.next =newNode;
newNode.prev = current;
}
// length+1
this.length += 1;
}
视频代码
// 2 append2方法 视频
DoublyLinkedList.prototype.append2 = function(data){
// 1 创建节点
let newNode = new Node(data);
// 2 判断添加的是否是第一个节点
if(this.length === 0){
this.head = newNode;
this.tail = newNode;
}else{
newNode.prev = this.tail;
this.tail.next =newNode;
this.tail = newNode;
}
// length+1
this.length += 1;
}
其实这里是菜鸟没有跳出单链表的思维,毕竟这里都已经有一个参数指向最后一个节点了,结果菜鸟还在靠循环去找,哈哈哈哈!
字符串方法实现
代码
// 3 backwardString方法 -- 从前往后
DoublyLinkedList.prototype.backwardString = function(){
// 定义变量
let current = this.head;
let string = '';
// 依次向后遍历
while(current){
string += current.data + " ";
current = current.next;
}
return string;
}
// 4 forwardString方法 -- 从后往前
DoublyLinkedList.prototype.forwardString = function(){
let current = this.tail;
let string = '';
// 依次向前遍历
while(current){
string += current.data + " ";
current = current.prev;
}
return string;
}
// 5 toStirng方法
DoublyLinkedList.prototype.toString = function(){
return this.backwardString();
}
insert实现 -- 指定位置插入
这个相比于单链表,要考虑更多的指针指向,更多的情况(多了一个tail指针),但是利用好tail、head,可以简化代码!还有就是要考虑指针的变动,如果变动了而使后面的无法找到就要改变顺序!
代码
// 6 insert方法
DoublyLinkedList.prototype.insert = function(position,data){
// 1 越界判断
if(position < 0 || position > this.length) return false;
// 创建节点
let newNode = new Node(data);
// 判断原来的链表是否为空
if(this.length === 0){
this.head = newNode;
this.tail = newNode;
}else{
// 判断position是否为0
if(position === 0){
newNode.next = this.head;
// 写单链表多了就容易忘记这一步
this.head.prev = newNode;
this.head = newNode;
}else if(position === this.length){
// 这一步和单向链表不一样,不能和插入中间归为一类,因为tail
// 也不需要调用this.append,重复了一部分操作
newNode.prev = this.tail;
this.tail.next =newNode;
this.tail = newNode;
}else{
let current = this.head;
let index = 0;
while(index++ < position){
current = current.next;
}
// 修改指针 -- 4个 -- 第二和第四的顺序不能乱了(其它无所谓),不然会因为指向改变而出错
newNode.next = current;
current.prev.next = newNode;
newNode.prev = current.prev;
current.prev = newNode;
}
}
this.length += 1;
return true;
}
get实现 -- 获取对应位置元素
思路和单链表一摸一样!
代码
// 7 get方法 -- 效率不高,可以采用平分,毕竟有一个尾结点!
DoublyLinkedList.prototype.get = function(position){
if(position < 0 || position >= this.length) return null;
let current = this.head;
let index = 0;
while(index++ < position){
current = current.next;
}
return current.data;
}
这里平分的代码菜鸟就不写了,思路就是把position和length/2进行比较,大于就用tail,小于就用head!
indexOf实现 -- 判断是否含有元素
这个也和单链表思路一模一样!
代码
DoublyLinkedList.prototype.indexOf = function(data){
let current = this.head;
let index = 0;
while(current){
if(current.data === data){
return index;
}
current = current.next;
index++;
}
return -1;
}
update实现 -- 更改某一位置的值
这里菜鸟使用的是平分的思路,上面get没用平分写出来的读者,可以借鉴一下这里!
代码
// 9 update方法
DoublyLinkedList.prototype.update = function(position,data){
if(position < 0 || position >= this.length) return false;
if(position < this.length/2){
let current = this.head;
let index = 0;
while(index++ < position){
current = current.next;
}
current.data = data;
}else{
let current2 = this.tail;
// 注意:index等于length-1,因为length是长度,而下标是从0开始,所以index最大只能是length-1
let index = this.length - 1;
while(index-- > position){
current2 = current2.prev;
}
current2.data = data;
}
return true;
}
removeAt实现 -- 移除某位置的元素
这里情况有点多,eg:
- 只有一个节点
- 移除的是第二个节点
- 移除的是最后一个节点
- 移除中间节点
2、3之所以有,是因为要改变head、tail的指向!
菜鸟一开始自己写代码没有考虑到这么多情况,然后就是看视频的时候想到了,就用自己的思路加上视频的情况写了。
代码
// 11 removeAt1 视频方法
DoublyLinkedList.prototype.removeAt1 = function(position){
if(position < 0 || position >= this.length) return false;
// 判断是否只有一个节点
if(this.length === 1){
this.head = null;
this.tail = null;
}else {
if(position === 0){ //判断删除的是否是第一个节点
this.head.next.prev = null;
this.head = this.head.next;
}else if(position === this.length - 1){ // 判断删除的是否是最后一个节点
this.tail.prev.next = null;
this.tail = this.tail.prev;
}else{
// 平分思路
if(position < this.length/2){
let current = this.head;
let index = 0;
while(index++ < position - 1){
current = current.next;
}
current.next.neaxt.prev = current;
current.next = current.next.next;
}else{
let current = this.tail;
let index = this.length - 1;
while(index-- > position+1){
current = current.prev;
}
current.prev.prev.next = current;
current.prev =current.prev.prev;
}
}
}
this.length -= 1;
return true;
}
菜鸟这里和视频思路不一样,保存的是要删除的节点的前一个或后一个,而不是删除的节点本身,所以返回不了值,但是感觉视频的更好(保存删除的节点),因为双向链表不像单向链表,需要保存两个节点信息,因为其既可以访问前节点也可以访问后节点!
视频代码
remove实现 -- 移除某值
这里直接调用两个方法就行,菜鸟就不写了
其它方法
总结
其实不难发现,单向链表和双向链表的难点都在于 insert 和 removeAt 方法,只要搞明白这两个方法的一些特殊情况,其实写起来也不是特别困难了;双向链表的其它操作真的和单向链表没什么很大的区别,有的甚至因为有了tail指针而更加简单!