前言❤️❤️
hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
这篇博客是链表博客的最后一篇,内容比较少,就是模拟双链表的实现,进一步的锻炼代码能力,初学的铁汁建议先模拟实现单链表(链表博客中有提到),再来模拟实现双链表,因为很多方法是在单链表的基础上改了一点
这个专栏的数据结构是代码都是用Java来写的,JavaSE专栏现在已经全部更新完成,铁汁们复习基础知识时非常推荐使用,可以试一下💪💪💪
🎇个人主页:洋不写bug的博客
🎇所属专栏:数据结构专栏
🎇复习Java基础知识:Java学习之旅,从入门到进阶
🎇铁汁们对于数据结构基础的各种核心知识(不太常用的也有😆),都可以在上面的数据结构专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~
1,双链表简介
-
单链表就是节点中只存了 next 引用,只能找到下一个节点。
-
双链表中存储了 prev 引用和 next 引用,也可以找到上一个节点,这样在使用时自然是更方便的
-
双链表的缺点就是空间利用率低,在Java中,链表的一个引用占8个字节,双向链表的引用就占16个字节,如果每个节点中保存数据的部分,占用空间很小(例如只保存了一个int类型的数字),那链表的空间利用率就是很低的
-

2,双链表的模拟实现
-
接下来就模拟实现个自己的双链表,里面存入的数据类型固定为String
-
先建个Node类表示节点,相比单链表要多定义一个属性prev
java
public class Node {
public String val;
public Node next = null;
public Node prev = null;
public Node(String val) {
this.val = val;
}
}
- 在MyDLinkedList类中,搞一个尾节点,方便后续进行尾插
- 对于addFirst方法,要判断下,如果head为null,也就是链表为空,那就让head和tail指向null;如果链表不为空,那就先让newNode指向head,把head的perv改为newNode,再更新下head
java
public class MyDLinkedList {
private Node head = null;
private Node tail = null;
public void addFirst(String val){
Node newNode = new Node(val);
if(head == null){
head = newNode;
tail = newNode;
}else{
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}
}
- 接着再写个toString方法,打印时使用
java
public String toString() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("[");
Node cur = head;
while(cur != null){
stringBuilder.append(cur.val);
if(cur.next != null){
stringBuilder.append(",");
}
cur = cur.next;
}
stringBuilder.append("]");
return stringBuilder.toString();
}
- 每写完一个方法,都建议在main方法中测试一下(后面每个方法的测试代码博主就不粘了)
java
public class Test {
public static void main(String[] args) {
MyDLinkedList myDLinkedList = new MyDLinkedList();
myDLinkedList.addFirst("aaa");
myDLinkedList.addFirst("bbb");
myDLinkedList.addFirst("ccc");
myDLinkedList.addFirst("ddd");
System.out.println(myDLinkedList);
}
}

- 接着是尾插,因为链表中存储了最后一个节点的引用,就无需再去遍历找尾节点了
java
public void addLast(String val){
Node newNode = new Node(val);
if(head == null){
head = newNode;
tail = newNode;
}else{
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
}
- size方法如下,写法跟单链表相同
java
public int size(){
Node cur = head;
int size = 0;
while(cur != null){
size++;
cur = cur.next;
}
return size;
}
- 中间位置插入方法add,就是传入下标和元素值,在中间位置插入元素,条件判断的时候,是可以等于size()的,相当于尾插;也可以等于0,相当于头插
- 下面这两种写法,单从代码上来看,第二种写法效率是比较高的,因为第一种写法size()是可能执行两次的(判断时和打印错误信息时),而size()方法是遍历整个链表,还是有一定开销的
- 但是如果测试的话,这两种写法的执行效率可能也差不多,这是因为Java的编译器在编译代码的时候,具有优化功能,可能会对代码的顺序,写法之类的进行调整,以降低程序员之间的代码差距(所以铁汁们用哪种写法根据自己的喜好来即可🐵)
java
if(index < 0 || index > size()){
throw new IndexOutOfBoundsException("size:" + size() + ",index:" + index);
}
java
int size = size();
if(index < 0 || index > size){
throw new IndexOutOfBoundsException("size:" + size + ",index:" + index);
}
-
接着分析中间位置插入的逻辑,首先把新节点的prev指向前面的节点,next指向后面的节点,接着再调下前面节点的next,和后面节点的prev即可
-

-
对于首插和尾插,直接调用前面的方法即可
java
public void add(int index,String val){
int size = size();
if(index < 0 || index > size){
throw new IndexOutOfBoundsException("size:" + size + ",index:" + index);
}
if(index == 0){
addFirst(val);
return;
}
if(index == size){
addLast(val);
return;
}
Node newNode = new Node(val);
Node prev = head;
for(int i = 0;i < index - 1;i++){
prev = prev.next;
}
Node next = prev.next;
newNode.prev = prev;
newNode.next = next;
prev.next = newNode;
next.prev = newNode;
}
- contains和indexOf方法都是遍历下链表即可
java
public boolean contains(String val){
if(head == null){
return false;
}
Node cur = head;
while(cur != null){
if(cur.val.equals(val)){
return true;
}
cur = cur.next;
}
return false;
}
java
public int indexOf(String val){
int index = 0;
if(head == null){
return -1;
}
Node cur = head;
while(cur != null){
if(cur.val.equals(val)){
return index;
}
index++;
cur = cur.next;
}
return -1;
}
- removeFirst方法,删除头部元素,需要分为空链表,只有一个节点的链表,还有多个节点的链表
java
public void removeFirst(){
if(head == null){
return;
}
if(head.next == null){
head = null;
tail = null;
return;
}
Node cur = head.next;
cur.prev = null;
head = head.next;
}
- removeLast方法,和removeFirst的写法类似
java
public void removeLast(){
if(head == null){
return;
}
if(head.next == null){
head = null;
tail = null;
return;
}
Node cur = tail.prev;
cur.next = null;
tail = cur;
}
-
指定位置删除,remove方法,里面传入index,这里index就不能等于size()了,因为链表的下标是从0到index - 1的,需要找到要删除元素的上个位置和下个位置
-

-
如果要删除的是头节点或者尾节点,直接调用前面已经写过的removeFirst和RemoveLast方法即可,排除这两种情况后,prev和next就不可能是null,我们就可以放心的去写代码
java
public void remove(int index){
int size = size();
if(index < 0 || index >= size){
throw new IndexOutOfBoundsException("size:" + size + ",index:" + index);
}
if(index == 0){
removeFirst();
return;
}
if(index == size - 1){
removeLast();
return;
}
Node prev = head;
for(int i = 0;i < index - 1;i++){
prev = prev.next;
}
Node next = prev.next.next;
prev.next = next;
next.prev = prev;
}
- reomve方法传入元素的版本,就遍历一下链表,找到toRemove节点,接获取它的上个节点和下个节点,分别修改下next和prev的指向即可
java
public void remove(String val){
Node toRemove = head;
while(toRemove != null){
if(toRemove.val.equals(val)){
break;
}
toRemove = toRemove.next;
}
if(toRemove == null){
return;
}
if(toRemove == head){
removeFirst();
return;
}
if(toRemove == tail){
removeLast();
return;
}
Node prev = toRemove.prev;
Node next = toRemove.next;
prev.next = next;
next.prev = prev;
}
结语
- 双链表的模拟实现在日常开发中上不会用到,这里模拟实现同样是锻炼我们的代码能力,包括画图分析,对于特殊情况和边界的处理
- 在写代码时,可以先考虑正常的情况,先写出代码,再结合正常情况的代码来分析,加上处理特殊情况的逻辑(remove方法的逻辑就是这样)
- 初学数据结构的铁汁,如果能把这三篇博客中的代码都自己敲一遍,你做链表题目的代码能力一定会有个很大的提升💪💪💪

