前言
上次,我们介绍了顺序表和单链表的相关概念,以及实现了它们的增加、删除、查找等操作,今天我们介绍另一种链表——双向链表。
一、双向链表
1.1 双向链表的结构:
每个结点存在两个指针域,分别存储该结点的前继结点的引用和后继结点的引用,从任意一个结点出发,都能通过前驱引用以及后继引用完成整个链表结点的遍历。
1.2 双向链表和单链表的区别:
和单链表改造的循环链表比起来,共同点是这两个结构都能够从任意结点通过不同的操作访问到链表的所有结点。但是双向链表为单链表扩展为在链表中可以进行双向遍历, 换句话说,我们可从链表中第一个节点遍历到到最后一个节点;也可以从最后一个节点遍历到第一个节点。
1.3 实用的场景:
应用场景:单链表适用于解决一条道走到黑的访问场景,比如给正在排队测温的人测量,只需要测试一次就下一位,无需重复测试;循环链表适用于具有环状数据结构的场景,比如丢手绢游戏;双向链表则适合需要提供多个方向访问功能的数据结构,比如老师在考场监考,可以来回走动(假设这个考场座位是顺序的)
二、双向链表的构造和相关操作
2.1 双向链表节点的构造
参考代码:
static class ListNode{
public int val;
public ListNode next;
public ListNode prev;
public ListNode(int val) {
this.val = val;
}
}
定义一个头节点和尾节点
public ListNode head;
public ListNode last;
2.2 节点的插入_前插法
参考代码:
public void addFirst(int data) {
ListNode node = new ListNode(data);
if (head == null){
head = node;
last = node;
}else {
node.next = head; //先绑定
head.prev = node; //绑定原来头节点的前驱
head = node; //重新定义头节点
}
}
2.2 节点的插入_尾插法
参考代码:
public void addLast(int data) {
ListNode node = new ListNode(data);
if (last == null){
head = node;
last = node;
}else {
last.next = node; //先绑定
node.prev = last;
last =node; //重新定义尾节点
}
}
2.4 返回索引下标的值
参考代码:
/**
* 找到index处的节点
* @param index
* @return
*/
private ListNode findIndex(int index){
ListNode cur = head;
while (index != 0){
index--;
cur = cur.next;
}
return cur;
}
2.5 指定位置插入节点
插入过程:
- node.next = cur;
- cur.prev.next = node; //先修改next
- node.prev = cur.prev;
- cur.prev = node;
参考代码:
/**
* 在指定位置插入数据
* @param index
* @param data
*/
@Override
public void addIndex(int index, int data) {
//检查index是否合法
int len = size();
if(index <0 || index >len){
//判断下标是否合法
System.out.println("index不合法 "+ index);
}
if (index == 0){
addFirst(data);
return;
}if (index == len){
addLast(data);
return;
}
ListNode cur = findIndex(index); // 接收index处的节点
ListNode node = new ListNode(data); //要插入的节点
node.next = cur;
cur.prev.next = node; //先修改next
node.prev = cur.prev;
cur.prev = node;
}
2.6 是否包含目标值
public boolean contains(int key) {
ListNode cur = head;
while (cur != null){
if (cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
2.7 删除节点
主要过程:
- cur.prev.next = cur.next;
- cur.next.prev = cur.prev;
参考代码:
/**
* 注意删除key的关键点:
* 1、删除头结点和尾节点
* 2、只有一个节点
* @param key
*/
@Override
public void remove(int key) {
ListNode cur = head;
while (cur != null){
if (cur.val == key){
if (cur == head){ //删除头结点
head = head.next;
if (head == null){
//如果是头结点,且不是只有一个节点
last = null;
}else {
head.prev = null; //正常删除头节点
}
}else {
cur.prev.next = cur.next;
if (cur.next == null){
//删除的是尾节点
last = last.prev;
}else {
//删除的是中间节点
cur.next.prev = cur.prev;
}
}
return;
}
else {
cur = cur.next;
}
}
}
2.8 删除所有关键值得节点
参考代码:
public void removeAllKey(int key) {
ListNode cur = head;
while (cur != null){
if (cur.val == key){
if (cur == head){ //删除头结点
head = head.next;
if (head == null){
//如果是头结点,且不是只有一个节点
last = null;
}else {
head.prev = null; //正常删除头节点
}
}else {
cur.prev.next = cur.next;
if (cur.next == null){
//删除的是尾节点
last = last.prev;
}else {
//删除的是中间节点
cur.next.prev = cur.prev;
}
}
//删除return即可
}
else {
cur = cur.next;
}
}
}
2.8 链表的大小
参考代码:
public int size() {
ListNode cur = head;
int count = 0;
while (cur != null){
count++;
cur = cur.next;
}
return count;
}
2.9 链表的打印
public void display() {
ListNode cur = head;
while (cur != null){
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
总结
-
双向链表在于Link类中,多了一个Pre前驱链接,相对于单链表只有next链接,需要弄清楚前驱和后驱的链接关系
-
可以选择固定headt和last节点,插入或者删除first或者last节点,只需要修改2个链接
-
在链表中间插入链接,需要修改4个链接(第一步一定是把要插入的节点先连接上);
-
在链表中间删除,只需要修改2个链接,一定要通过画图来更好的理解链接的关系。