🐵本篇文章将对双向链表进行讲解,模拟实现双向链表的常用方法
一、什么是双向链表
双向链表在指针域上相较于单链表,每一个节点多了一个指向前驱节点的引用prev以及多了指向最后一个节点的引用last:
二、双向链表的模拟实现
首先将要模拟实现的方法写到IList接口中:
public interface IList {
//头插法插入节点
public void addFirst(int data);
//尾插法插入节点
public void addLast(int data);
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data);
//查找是否包含关键字key是否在链表当中
public boolean contains(int key);
//删除第一次出现关键字为key的节点
public void remove(int key);
//删除所有值为key的节点
public void removeAllKey(int key);
//得到链表的长度
public int size();
//清除链表
public void clear();
//显示链表
public void display();
}
之后再创建一个MyLinkedList类实现上述接口并重写接口中所有的方法
public class MySingleList implements IList{
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; //指向最后一个节点
/*以下是要重写IList接口中的方法*/
...
}
2.1 模拟实现
public void addFirst(int data);
双向链表的头插法和单链表基本一致,只不过当链表为空时不仅要让head指向新节点还要让last也指向新节点
public void addFirst(int data) {
ListNode newNode = new ListNode(data);
if (head == null) {
head = newNode;
last = newNode;
return;
}
newNode.next = head;
head.prev = newNode;
head = newNode;
}
public void addLast(int data);
当链表为空时要让head和last都指向新节点,当链表部不为空时要让最后一个节点的next指向新节点,之后让新节点的prev指向原来的最后一个节点
public void addLast(int data) {
ListNode newNode = new ListNode(data);
if (head == null) {
head = newNode;
last = newNode;
return;
}
last.next = newNode;
newNode.prev = last;
last = newNode;
}
public void addLast(int data);
在任意位置处插入一个节点,第一个节点的索引为0
首先要判断一下index是否合法:
public class IndexException extends RuntimeException{
public IndexException(String message) {
super(message);
}
}
=======================================================
public void addIndex(int index, int data) {
if (index < 0 || index > size()) { //size()为链表长度
throw new IndexException("下标错误");
}
...
}
在任意位置插入节点,所以如果index为0或等于链表长度,就可以直接使用刚刚实现过的头插和尾插方法
public void addIndex(int index, int data) {
if (index < 0 || index > size()) {
throw new IndexException("下标错误");
}
if (index == 0) {
addFirst(data);
}
if (index == size()) {
addLast(data);
}
...
}
一般情况下(在中间插入),单链表中要通过循环找到插入位置的前一个节点,但在双向链表中,直接循环到插入位置(插入位置记为cur)即可
public void addIndex(int index, int data) {
if (index < 0 || index > size()) {
throw new IndexException("下标错误");
}
if (index == 0) {
addFirst(data);
return;
}
if (index == size()) {
addLast(data);
return;
}
ListNode newNode = new ListNode(data);
ListNode cur = head;
for (int i = 0; i < index; i++) {
cur = cur.next;
}
/*一般情况下*/
newNode.next = cur;
cur.prev.next = newNode;
newNode.prev = cur.prev;
cur.prev = newNode;
}
public void remove(int key);
删除第一次出现val = key的节点
先考虑常规情况,即通过遍历找到要删除的节点,这里记为cur
让cur的前驱节点的next指向cur的后继节点,cur的后继节点的prev指向cur的前驱节点
public void remove(int key) {
ListNode cur = head;
while(cur != null) {
if (cur.val == key) {
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
return;
}
cur = cur.next;
}
}
之后有两种特殊情况需要考虑:1.cur为第一个节点;2.cur为最后一个节点;当cur为这两种情况时如果使用上述代码,会引发空指针异常,所以这两种情况要单独考虑
1.cur为第一个节点:此时需要让cur的后继节点prev指向空(cur.prev = null),并让head = head.next,但是这样还有一个小问题:当链表中只有一个节点时也会引发空指针异常,这个问题也要单独处理,只需要直接让head = null即可
if (cur == head) {
head = head.next;
if (head == null) {
last = null;
} else {
head.prev = null;
}
return;
}
2.cur为最后一个节点:只需让cur的前驱节点的next指向空,并让last = last.prev;即可
if (cur.next == null) {
cur.prev.next = null;
last = last.prev;
return;
}
remove的最终代码如下:
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;
}
return;
}
if (cur.next == null) {
cur.prev.next = null;
last = last.prev;
return;
}
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
return;
}
cur = cur.next;
}
}
void removeAllKey(int key);
删除所有val = key的节点,这里只需要将remove方法修改以下即可
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;
}
cur = head;
continue;
}
if (cur.next == null) {
cur.prev.next = null;
last = last.prev;
break;
}
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
cur = cur.next;
}
}
剩下的contains() 、size()、clear()、display()方法和上篇文章的单链表实现方法一致
三、LinkedList类讲解
LinkedList类是Java提供的类,底层是一个双向链表,包含我们刚刚实现过的方法,LinkedList也实现了List接口
3.1 LinkedList构造方法
LinkedList有两个构造方法:
1.无参构造方法
public LinkedList()
2. 带参构造方法
public LinkedList(Collection<? extends E> c)
该构造方法可以将c构造为双向链表,前提是c实现了Collection接口并且其泛型必须是E或是E的子类,例如:
ArrayList<Integer> list1 = new ArrayList<>();
list1.add(1);
list1.add(2);
LinkedList<Integer> list = new LinkedList<>(list1); //list1属于ArrayList类,实现了Collection接口,泛型和list1一样都是Integer,此时顺序表list1就被构造为了双向链表list
3.2 LinkedList类常用方法
boolean add(E e) //尾插 e
void add(int index, E element) //将 e 插入到 index 位置
boolean addAll(Collection<? extends E> c) //尾插 c 中的元素
E remove(int index) //删除 index 位置元素
boolean remove(Object o) //删除遇到的第一个 o
E get(int index) //获取下标 index 位置元素
E set(int index, E element) //将下标 index 位置元素设置为 element
void clear() //清空链表
boolean contains(Object o) //判断 o 是否在线性表中
int indexOf(Object o) //返回第一个 o 所在下标
int lastIndexOf(Object o) //返回最后一个 o 的下标
List<E> subList(int fromIndex, int toIndex) //截取链表,按左闭右开的区间截取[fromIndex, toIndex)
这些方法的底层实现方式和我们上述模拟实现的方法的实现方式相同
3.3 LinkedList遍历
1. for循环
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) +" ");
}
2. for-each
for (int x : list) {
System.out.print(x +" ");
}
3.迭代器
顺向遍历
ListIterator<Integer> it = list.listIterator();
while(it.hasNext()) {
System.out.print(it.next() +" ");
}
逆向遍历
ListIterator<Integer> it1 = list.listIterator(list.size());
while(it1.hasPrevious()) {
System.out.print(it1.previous() +" ");
}
🙉本篇文章到此结束,下篇文章会对栈相关知识进行讲解