目录
大家好!欢迎大家来到利物浦痛失欧冠冠军的第一期文章。
一、双向链表的简单理解
1、双向链表是什么?
在前面的文章中,我们仔细讲解了单向链表,并且用代码实现了单向链表。单向链表的好处很多,虽然单链表能 100% 解决逻辑关系为 "一对一" 数据的存储问题,但在解决某些特殊问题时,单链表并不是效率最优的存储结构。比如说,如果算法中需要大量地找某指定结点的前趋结点,使用单链表无疑是灾难性的,因为单链表更适合 "从前往后" 找,而 "从后往前" 找并不是它的强项,因此我们就有了双向链表这个东西,双向链表顾名思义就是链表的高级版,他与单向链表有所不同的一点在于在具有尾巴结点,并且双向链表的每一个结点都会有两个指向,一个指向前面的结点,一个指向后面的结点,当然头结点的前端指向为null,后结点的后端指向为null。
2、双向链表长什么样子?
双向链表的一个结点,具有包括三个信息:1、前端指针域。2、数据域。3、后端指针域
如图所示
双向链表正是由这些一个个结点组合而成
接下来我们将会用代码的方式来实现双向链表
二、代码演示
1、结点的构建
和单向链表一样,链表的实现离不开结点,这构建也与单向链表相似,只不过不同的是我们需要加一个前端指针
class ListNode{
public ListNode prev;
public ListNode next;
public int val;
public ListNode(int num){
this.val = num;
}
public ListNode(){
}
}
2、链表的创建
链表的创建中有个很重要的东西,就是我们需要给链表设置一个头结点指针与尾巴结点指针,因为链表初始化只有一个结点,因此头结点指针 == 尾巴结点指针
public ListNode head;
public ListNode last;
public TowWayNodeList(int num){
this.head = new ListNode(num);
this.last = this.head;
}
3、打印链表
public void display(){
ListNode cur = this.head;
while(cur != null){
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
4、得到链表长度
public int size(){
ListNode cur = this.head;
int size = 0;
while(cur != null){
size++;
cur = cur.next;
}
return size;
}
5、查找元素
public boolean contains(int key){
ListNode cur = this.head;
while(cur != null){
if(cur.val == key){
return true;
}
}
return false;
}
6、插入元素
(1)、头插法
当我们想把新的结点插入到第一个结点位置处,可以先建立一个结点,然后把头结点的prev变为我们新建立结点的next值,然后将我们新建立的结点值变为null,最后将头结点指向新的插入的结点。
注意我们需要首先判断这个链表是否为空,假如为空就直接构建链表即可
//头插法
public void addFirst(int num){
ListNode node = new ListNode(num);
if(head == null){
this.head = node;
this.last = node;
}else{
node.next = this.head;
this.head.prev = node;
this.head = node;
}
}
(2)、尾插法
尾插法顾名思义就是从结尾插入新的结点,这个和头插法过程差不多,只不过一个是改变head的位置,一个是改变last的位置。
和头插法一样,这个同样需要判断链表是否初始为空
public void addLast(int data){
ListNode node = new ListNode(data);
if(head == null){
this.head = node;
this.last = node;
}else{
this.last.next = node;
node.prev = this.last;
this.last = node;
}
}
(3)、中间插入
这个是最复杂的一种插入方式,我们需要先找到需要插入位置的结点cur,然后利用cur就可以得出前后两个结点,直接插入即可。
首先我们建立一个方法来查找cur的位置,一个返回值为结点的元素
public ListNode searchIndex (int index){
ListNode cur = this.head;
while(index != 0){
cur = cur.next;
index--;
}
return cur;
}
然后利用cur附近的结点来直接插入即可
这是cur附近各个指针域的图,我们只需要改变cur附近的结点指针域的指向即可,首先node指向cur的地址也就是cur.prev.next
然后cur.prev.next指向node的地址,然后再开始前端指针的指向
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
if(index < 0|| index > size()){
System.out.println("非法");
return;
}
//ListNode node = new ListNode(data);
if(index == 0){
addLast(data);
}else if(index == size()){
addLast(data);
}else{
ListNode node = new ListNode(data);
ListNode cur = searchIndex(index);
node.next = cur.prev.next;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
}
}
7、删除元素
删除元素的话,我们需要:
(1)、删除元素为头结点。
假如是头结点的话我们还需要判断这个链表是否只有一个结点,如果是那么last指针也会为空,head指针也会为空,否则,我们只移动头指针结点就可以。
(2)、删除元素为中间结点或者尾巴结点
当删除中间结点的时候我们可以先找到对于位置的结点cur,利用对应位置的cur.prev和cur.next确定附近两个结点,然后进行删除即可,这个删除与链表相似,只是多了一个删除头结点而已。
首先跳过后端结点cur.prev.next = cur.next
然后跳过前端结点cur.next.prev = cur.prev
但是假如我们遇到了删除结点为尾巴结点该怎么办呢?
很简单,只需让last = last.prev即可!
//删除第一次出现关键字为key的节点
public void remove(int key){
ListNode cur = this.head;
while(cur != null){
if(cur.val == key){
if(cur == head){//假如为头结点
head = head.next;//Next指针移动完毕接下来就是prev指针
if(head != null){
//判断head是否为空,为空说明这个链表就只有一个元素,不为空说明不止一个
//这是说明这个属于中间位置hah
head.prev = null;
}else{
last = null;
}
}else{
cur.prev.next = cur.next;
if(cur.next != null){
//中间位置
cur.next.prev = cur.prev;
}else{
last = last.prev;
//假如是最后一个结点,last结点始终指向双向链表最后一个结点,最后一个删除last变为其前驱即可
}
}
return;
}
cur = cur.next;
}
}
三、总结
双向链表算是对于链表进行了更加全面的优化,需要注意的是,与单链表不同,双链表创建过程中,每创建一个新节点,都要与其前驱节点建立两次联系,分别是:
- 将新节点的 prior 指针指向直接前驱节点;
- 将直接前驱节点的 next 指针指向新节点;
这样也更好的利用了前后指针,更加具体的防止了,当我们进行对于链表的增加与删除时,操作不当整个链表丢失的情况。