目录
🐧今日良言:生如逆旅单行道,哪有岁月可回头
⛄一、介绍双向链表
🐟1.双向链表的相关定义
🐎2.双向链表的几种类型
⛄二、实现双向链表
🐄1.思路分析
🐉2.双向链表中的相关操作
⛄三、完整代码
🐧今日良言:生如逆旅单行道,哪有岁月可回头
⛄一、介绍双向链表
🐎1.双向链表的相关定义
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两 指针 ,分别指向直接后继节点和直接前驱节点。
🐟2.双向链表的几种类型
带头循环双向链表
带头不循环双向链表
不带头循环双向链表
不带头不循环双向链表
实现的是不带头不循环双向链表
⛄二、实现双向链表
🐉1.思路分析
经过单链表的学习,对于链表我们有了一定的了解,双向链表只是在单链表的基础之上多了一个prev(指向直接前驱节点)的指针域,先创建一个主类MyLinkedList,我们将对双向链表进行的相关操作全部放到这个类中,然后创建一个节点类ListNode ,其中的成员属性如下:
public int val;// 数据域 public ListNode prev; // 指向前驱节点的指针域 public ListNode next; // 指向后继节点的指针域
提供一个构造方法,进行初始化
public ListNode (int val) { this.val = val; }
我们还需要在类MyLinkedList中定义一个指向头结点的ListNode 类型 的head和指向尾结点的ListNode类型的tail
public ListNode head;// 头节点 public ListNode tail;// 尾节点
🐄2.双向链表中的相关操作
1.打印双向链表
双向链表的打印和单链表相同,定义一个辅助节点cur,从头节点head处开始打印,当cur为空时结束循环,代码如下:
// 打印双向链表
public void display() {
ListNode cur = this.head;
if (cur == null) {
System.out.println("双向链表为空");
return;
}
while (cur != null) {
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
2.得到链表结点个数
与单链表求节点个数方法相同,定义一个辅助节点从head开始,再定义一个count用来计数,每次count++,cur为空结束循环,代码如下:
// 求链表长度
public int size() {
ListNode cur = this.head;
int count = 0;
while (cur != null) {
count++;
cur = cur.next;
}
return count;
}
3.头插法
先判断当前头节点是否为空,为空说明是第一次插入数据,直接将要插入的这个节点当做头节点和尾节点,如果不为空说明不是第一次插入数据,此时先让这个节点的next指针域指向头节点,再让头节点的prev指针域指向该节点,最后将该节点当做新的头节点
图解如下:
代码如下:
// 头插法
public void addFirst(int val) {
ListNode node = new ListNode(val);
if (this.head == null) {
this.head = node;
this.tail = node;
} else {
node.next = this.head;
this.head.prev = node;
this.head = node;
}
}
4.尾插法
首先也是判断head是否为空,如果为空就将待插入节点当做新的head和tail,如果不为空,说明不是第一次插入数据,与单链表不同,单链表是需要遍历找到最后一个节点,然后插入节点,而双向链表有一个尾结点tail,此时直接将节点插入到tail节点后面即可,最后将待插入节点当做新的tail,图解如下:
代码如下:
// 尾插法
public void addLast(int val) {
ListNode node = new ListNode(val);
if (this.head == null) {
this.head = node;
this.tail = node;
} else {
this.tail.next = node;
node.prev = this.tail;
this.tail = node;
}
}
5.在任意位置插入节点,第一个节点为0下标
5.1.需要对双向链表进行判空,如果为空,抛出一个异常
if (isEmpty()) {
throw new RuntimeException("双向链表为空");
}
5.2我们需要检查要插入位置pos的合法性,当index < 0 || index > size() ,此时index不合法可以抛出一个异常
// 1.判断index合法性
if (index < 0 || index > size()) {
throw new IndexException("index位置不合法");
}
5.3此时需要判断是不是尾插法(当index == size())和头插法(index == 0)
// 头插尾插
if (index == 0) {
addFirst(val);
return;
}
if (index == size()) {
addLast(val);
return;
}
5.4创建一个方法getIndex,参数为index,与单链表的插入不同的是,可以定义一个辅助节点cur,让cur走index步,最后返回cur
private ListNode getIndex(int index) {
ListNode cur = this.head;
while (index > 0) {
cur = cur.next;
index--;
}
return cur;
}
5.5这里需要注意连接顺序,连接顺序出错,可能会导致空指针异常
// 找到index下标开始插入
ListNode cur = getIndex(index);
ListNode node = new ListNode(val);
node.next = cur;
node.prev = cur.prev;
cur.prev.next = node;
cur.prev = node;
完整代码:
// 在index位置插入数据
public void addIndex(int index,int val) {
if (isEmpty()) {
throw new RuntimeException("双向链表为空");
}
// 判断index合法性
if (index < 0 || index > size()) {
throw new IndexException("index位置不合法");
}
// 头插尾插
if (index == 0) {
addFirst(val);
return;
}
if (index == size()) {
addLast(val);
return;
}
// 找到index下标开始插入
ListNode cur = getIndex(index);
ListNode node = new ListNode(val);
node.next = cur;
node.prev = cur.prev;
cur.prev.next = node;
cur.prev = node;
}
private ListNode getIndex(int index) {
ListNode cur = this.head;
while (index > 0) {
cur = cur.next;
index--;
}
return cur;
}
6.查找是否包含关键字key的节点在链表中
定义一个辅助节点cur,从头节点开始查找,找到就返回true,否则返回false
public boolean contains(int val) {
if (isEmpty()) {
throw new RuntimeException("双向链表为空");
}
// 遍历查找
ListNode cur = this.head;
while (cur != null) {
if (cur.val == val) {
return true;
}
cur = cur.next;
}
return false;
}
7.删除第一次出现的key值节点
1.先判断双向链表是否为空
2.定义一个辅助节点cur,从头结点开始,当cur为空结束循环,进入循环后,先判断当前cur.val是否等于key值,如果等于就执行删除操作
3.执行删除操作时,需要对当前cur节点进行讨论
3.1 如果当前cur节点是头结点,让头节点指向头节点的下一个节点,接下来的操作,是将新头节点的prev指针域置空,但是,这里不能忽略一种情况,如果我的链表只有一个节点,那么当前新头节点就是空指针,执行空指针的prev操作会发生空指针异常,所以说,这里需要判断新头节点如果为空,就将尾节点tail也置空,如果不为空,就将新头节点的prev指针置空
this.head = this.head.next; // 判断是不是只有一个节点的情况 if (this.head == null) { this.tail = null; } else { this.head.prev = null; }
3.2如果当前cur节点不是头节点,此时,判断当前节点是不是尾结点
如果是尾结点的话,直接修改tail的指向,让tail指向前一个节点,然后再将修改后的tail的next指针域置空
this.tail = this.tail.prev; this.tail.next = null;
如果不是尾结点,让当前cur的前一个节点的next指针域指向cur的下一个节点,再让cur的下一个节点的prev指针域指向cur的前一个节点
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
删除节点后直接return结束操作即可。
完整代码:
// 删除第一次出现key值的节点
public void remove(int key) {
if (this.head == null) {
System.out.println("链表为空,无法删除");
return;
}
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
// 如果是删除头节点
if (cur == this.head) {
this.head = this.head.next;
// 此时判断是不是只有一个节点的情况,避免空指针异常
if (this.head != null) {
this.head.prev = null;
} else {
// 只有一个节点
this.tail = null;
}
} else {
// 不是头节点
// 判断是不是尾节点
if (cur == this.tail) {
this.tail = cur.prev;
this.tail.next = null;
} else {
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
}
return;
}
cur = cur.next;
}
System.out.println("没有你要删除的数据");
}
8.删除所有的key值节点
该操作与删除第一次出现key值节点的操作思路是一样的,只是删除第一次出现的key值操作删除节点后,我们直接return 结束了删除操作,而删除所有的key值节点删除return操作,让cur等于null结束循环,即可删除所有key节点
代码如下:
// 删除第一次出现key值的节点
public void remove(int key) {
if (this.head == null) {
System.out.println("链表为空,无法删除");
return;
}
ListNode cur = this.head;
while (cur != null) {
if (cur.val == key) {
// 如果是删除头节点
if (cur == this.head) {
this.head = this.head.next;
// 此时判断是不是只有一个节点的情况,避免空指针异常
if (this.head != null) {
this.head.prev = null;
} else {
// 只有一个节点
this.tail = null;
}
} else {
// 不是头节点
// 判断是不是尾节点
if (cur == this.tail) {
this.tail = cur.prev;
this.tail.next = null;
} else {
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
}
}
cur = cur.next;
}
System.out.println("没有你要删除的数据");
}
9.清空双向链表
定义一个辅助结点cur,从头节点开始,直到cur为空结束操作,在循环体中,每次记录当前cur节点的下一个后继节点curNext,然后将当前cur的next域和prev域置空,最后修改cur为curNext。
当循环结束后,将head和tail置空,即可完成清空双向链表操作
代码如下:
// 清空链表
public void clear() {
ListNode cur = this.head;
while (cur != null) {
ListNode curNext = cur.next;
cur.prev = null;
cur.next = null;
cur = curNext;
}
this.tail = null;
this.head = null;
}