⭐作者介绍:大二本科网络工程专业在读,持续学习Java,努力输出优质文章
⭐作者主页:@逐梦苍穹
⭐所属专栏:数据结构
双链表
- 1、简介
- 2、常见操作
- 3、时间复杂度
- 4、代码实现思路总览
- 5、Node
- 6、DoubleLinkedList
- 6.1、添加节点
- 6.1.1、addNode
- 6.1.2、addNodeByOrder
- 6.2、删除节点
- 6.3、修改节点
- 6.4、显示链表内容
1、简介
双向链表(Doubly linked list)是一种常见的线性数据结构,它与单链表类似,但每个节点不仅包含一个指向下一个节点的指针,还包含一个指向前一个节点的指针。
这使得双向链表可以在需要正向和反向遍历的场景中提高效率。
2、常见操作
下面是双向链表的一些基本操作:
- 链表的创建:与单链表类似,在创建链表时需要创建头节点,并将其前驱指针和后继指针都设置为NULL。然后逐个创建后续节点,将前一个节点的后继指针指向当前节点,将当前节点的前驱指针指向前一个节点,直到创建完整个链表。
- 链表的遍历:与单链表类似,从头节点开始,逐个遍历链表中的节点,直到遍历到尾节点为止。
- 链表的插入:可以在链表的任意位置插入一个新节点。具体操作是先将新节点的指针指向其下一个节点,然后将前一个节点的后继指针指向新节点,将下一个节点的前驱指针指向新节点。
- 链表的删除:可以删除链表中的任意节点。具体操作是将待删除节点的前一个节点的后继指针指向待删除节点的下一个节点,将下一个节点的前驱指针指向待删除节点的前一个节点,然后释放待删除节点的内存空间。
3、时间复杂度
双向链表的时间复杂度如下:
- 链表的创建时间复杂度为O(n),其中n为链表中节点的个数。
- 链表的遍历时间复杂度为O(n),其中n为链表中节点的个数。
- 链表的插入时间复杂度为O(1)。
- 链表的删除时间复杂度为O(1)。
需要注意的是,与单链表相比,双向链表需要占用更多的内存空间,因为每个节点需要同时保存前驱指针和后继指针。此外,双向链表在需要正向和反向遍历的场景中表现更好,但在只需要正向遍历的场景中,其效率与单链表相当。
4、代码实现思路总览
双链表与单链表在实现过程中最大的不同是:在实现的方法中,不再需要遍历到 目标节点的前一个节点,而是直接遍历到目标节点。
回顾一下单链表:遍历到目标节点的前一个节点的原因:单链表是单向的,如果直接遍历到目标节点,则无法修改目标节点前一个节点的指针指向,会破坏链表结构。
而双链表则没有这个问题,因为多了一个前驱指针pre
实现过程简要分析如下:
首先创建节点对象代码Node类,创建成员变量:唯一标识符id,节点的值value,节点Node类型的next、节点Node类型的pre(重点)以及重写toString方法。
然后创建双链表类DoubleLinkedList,先初始化头节点,然后依次编写各种方法:获取头节点、添加节点(两种方式)、删除节点、修改节点、显示链表内容。[总体实现思路如下图所示]:
5、Node
重写的toString方法,next和pre不直接打印对象内容,而是打印十六进制的地址值:
这里还运用了三元表达式.
下面是Node的代码:
package linkedList.doubleLinkedList;
/**
* @author 逐梦苍穹
* @date 2023/5/12 10:00
*/
public class Node {
public int id;
private int value;
private Node next;
private Node pre;
public Node(int id, int value) {
this.id = id;
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"id=" + id +
", value=" + value +
", next=" + (next == null ? null
: ("0x" + Integer.toHexString(next.hashCode()))) +
", pre=0x" + Integer.toHexString(pre.hashCode()) +
'}';
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
public Node getPre() {
return pre;
}
public void setPre(Node pre) {
this.pre = pre;
}
}
6、DoubleLinkedList
双链表的全部代码如下,后面对其详细拆分解析:
package linkedList.doubleLinkedList;
/**
* @author 逐梦苍穹
* @date 2023/5/12 10:00
*/
public class DoubleLinkedList {
private final Node headNode = new Node(0, 0);
/**
* 获取头节点
*/
public Node getHeadNode() {
return headNode;
}
/**
* 添加到尾部
*/
public void addNode(Node node) {
Node temp = this.headNode;
while (true) {
if (temp.getNext() == null) {
break;
} else {
//辅助节点后移
temp = temp.getNext();
}
}
//跳出循环说明到达链表尾部,可以添加
temp.setNext(node); //把最后一个节点的next指针置为下一个要添加的节点的地址值
node.setPre(temp); //把添加到末尾的节点的pre前置指针置为上一个节点的地址值
}
/**
* 根据id查找位置插入
*/
public void addNodeByOrder(Node node) {
Node temp = this.headNode;
while (true) {
if (temp.getNext() == null) { // 如果temp是尾节点
temp.setNext(node);
node.setPre(temp); // 设置新节点的前驱节点为temp
break;
}
if (node.id < temp.getNext().id) { // 如果新节点应该插入到temp之后
node.setNext(temp.getNext());
node.setPre(temp); // 设置新节点的前驱节点为temp
temp.getNext().setPre(node); // 设置新节点之后的节点的前驱节点为node
temp.setNext(node);
break;
}
temp = temp.getNext();
}
}
/**
* 删除节点
*/
public void deleteNode(int id) {
Node temp = this.headNode;
if (temp.getNext() == null) {
System.out.println("链表空无法删除");
return;
}
while (true) {
if (temp.getNext() == null) break;
if (temp.id == id) {
temp.getPre().setNext(temp.getNext());
temp.getNext().setPre(temp.getPre());
System.out.println("Node{id=" + id + "}:删除成功");
return;
}
temp = temp.getNext();
}
System.out.println("无此节点");
}
/**
* 修改节点
*/
public void updateNode(int id, int newValue) {
Node temp = this.headNode;
if (temp.getNext() == null) {
System.out.println("链表空");
}
while (true) {
if (temp.getNext() == null) break;
if (temp.id == id) {
temp.setValue(newValue);
System.out.println("Node{id=" + id + ", newValue=" + newValue + "}");
return;
}
temp = temp.getNext();
}
System.out.println("无此节点");
}
/**
* 打印整个双链表的内容
*/
public void outDoubleLinkedList() {
Node temp = this.headNode.getNext();
if (temp == null) {
System.out.println("链表空");
return;
}
while (true) {
System.out.println(temp);
if (temp.getNext() == null) break;
temp = temp.getNext();
}
}
}
6.1、添加节点
添加节点有两种方式:直接添加到链表尾部、按唯一标识id按顺序添加
6.1.1、addNode
直接添加到链表尾部:先判断当前节点的next域是不是null,如果是,说明到达链表尾部,可以添加;如果不是,则链表指针需要后移,直到抵达尾部:
6.1.2、addNodeByOrder
按唯一标识id按顺序添加:
这里在while中有两个if判断,一开始是指向头节点的,这时候链表如果为空,则直接添加数据即可;
当链表不为空的时候,进入的是第二个if分支,由于每一次插入的时候,都是保持有序的,所以在插入节点的时候只需要判断:想要插入节点的位置的下一个节点的id大于想要插入节点的id即可。
6.2、删除节点
删除节点的操作:找到待删除节点,把 该节点的pre的next指针
指向 该节点的next
,然后把 该节点的next指针的pre
指向 该节点的pre
。
被删除的节点,是不需要手动释放资源的,JVM有自动回收机制。
6.3、修改节点
6.4、显示链表内容
显示链表内容的思路,其实无非也就是遍历。这里采用的是while进行遍历,因为并不知道链表到底有多长,所以采用while遍历更合适。
遍历终止条件:链表为空 或者 到达最后一个节点(即在判断不为空的情况下出现next为null)
写在最后:
⭐如果觉得文章写的不错,欢迎点个关注一键三连。您的三连支持,是我创作的最大动力
⭐有写的不好的地方也欢迎指正,一同进步😁