文章目录
1. 双向链表
前面提到了单向链表,单链表的特点是只能从前向后遍历,但是如果我们已知一个节点在链表靠后的位置,这时如果使用单链表效率就会很低,因此引入了双向链表就可以快速的找到靠后的节点。双向链表与单向链表的区别就是它不仅有后继节点,还有前驱节点。这样就既存储了下一个节点的地址,也存储了前一个节点的地址。
2. 双向链表的定义
节点类:
class DoubleNode {
// 指向前驱节点
DoubleNode prev;
// 具体存储的数据
int val;
// 指向后继节点
DoubleNode next;
public DoubleNode(int val) {
this.val = val;
}
}
链表类:
public class DoubleLinkedList {
// 实际存储元素个数
int size;
// 双向链表的头节点
private DoubleNode first;
// 双向链表的尾节点
private DoubleNode last;
}
3. 增加操作
3.1 头插法
// 头插法
public void addFirst(int val) {
// 暂存头节点
DoubleNode head = first;
// 1.先 new 一个节点
DoubleNode node = new DoubleNode(val);
// 2.再将头节点指向新插入的节点
first = node;
// 注意判空
if (head == null) {
// 如果链表为空,尾节点就是头节点
last = node;
} else { // 3.将新节点的后继与原来的第一个元素相连,再将原来第一个节点的前驱与新节点相连
node.next = head;
head.prev = node;
}
size++;
}
断点调试:
图解链表不为空时:
3.2 尾插法
// 尾插法
public void addLast(int val) {
DoubleNode l = last;
DoubleNode node = new DoubleNode(val);
last = node;
// 注意判空
if (l == null) {
// 头节点就是尾节点
first = node;
} else {
node.prev = l;
l.next = node;
}
size++;
}
断点调试:
图解链表不为空:
3.3 任意位置插入
由于双向链表的特性,它既可以从前向后遍历,也可以从后向前遍历,所以可以从中间位置开始。这是就要考虑两部分,也就是分治思想,先处理一半的情况在处理另一半情况。
分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的子问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并才能得到原问题的解,因此整个分治过程经常用递归来实现。
先考虑 index 是处于链表的那一半,前一半还是后一半,因为这个方法不只是插入时使用,查找,删除,修改时都需要判断 index 的位置,可以将此方法提出为公共方法。
// 根据 index 与 size 的大小关系定位 node 节点
private DoubleNode node(int index) {
// 不用从头开始,可以从头也可以从尾
// 1.如果 index 在链表前半部分
if (index < size >> 1) { // size >> 1 一半
// 从前向后走
DoubleNode node = first;
for (int i = 0; i < index; i++) {
node = node.next;
}
return node;
} else { // 2.如果 index 在链表后半部分
// 从后向前走
DoubleNode node = last;
for (int i = size - 1; i > index; i--) {
node = node.prev;
}
return node;
}
}
在任意位置插入元素:
注意:使用索引作为参数都要考虑索引是否合法!
// 判断索引是否合法
private boolean rangeCheck(int index) {
if(index < 0 || index >= size) {
System.err.println("索引非法!");
return false;
}
return true;
}
// 在任意位置插入
public void addIndex(int index, int val) {
if (rangeCheck(index)) {
if (index == 0) {
addFirst(val);
} else if(index == size - 1) { //已更正
addLast(val);
} else {
// 找到插入的位置
DoubleNode node = node(index);
// 指向当前位置的前驱节点
DoubleNode prev = node.prev;
// 要插入的新节点
DoubleNode newNode = new DoubleNode(val);
// 先处理新节点与插入位置后面的节点之间的链接
newNode.next = node;
node.prev = newNode;
// 再处理新节点与插入位置前面的节点之间的链接
prev.next = newNode;
newNode.prev = prev;
size++;
}
}else{
System.out.println("不能添加!");
}
}
图解:
4. 删除操作
双向链表与单向链表删除操作的不同点在于,双向链表不需要先找到待删除节点的前驱节点,双向链表只要找到待删除节点,就可以知道它的前驱和后继节点。所以双向链表的重点在于传入一个节点如何将它从链表中删除。可以使用分治思想,先处理待删除节点如何和链表前面链接,再处理待删除节点如何和链表后面链接。
// 传入一个双向链表节点将该节点删除
// 分治思想:先处理待删除节点的前半部分链表的链接,再处理后半部分链表的链接
private void deleteNode(DoubleNode node) {
// 待删除节点的前驱节点
DoubleNode prevNode = node.prev;
// 待删除节点的后继节点
DoubleNode nextNode = node.next;
// 1.先处理前半部分
// 边界条件:删除的是个头节点
if (prevNode == null) {
first = nextNode;
} else {
// 前驱节点不为空,删除的不是头节点
prevNode.next = nextNode;
node.prev = null;
}
// 2.处理后半部分
// 边界条件:删除的是最后一个节点
if (nextNode == null) {
last = prevNode;
} else {
//后继节点不为空
nextNode.prev = prevNode;
node.next = null;
}
}
图解删除的是头节点:
图解删除的是尾节点:
图解删除中间位置节点:
4.1 删除双向链表中第一个出现的元素
先遍历循环找到指定元素所在的节点,然后使用上述方法删除该节点,就结束了。
// 删除双向链表中第一个出现的元素
public void removeValOnce(int val) {
for(DoubleNode node = first; node!= null; node = node.next) {
if (node.val == val) {
// node 就是待删除的节点
deleteNode(node);
return;
}
}
}
4.2 删除双向链表中指定的所有元素
和上一个方法一样也是先找到待删除的元素节点,不同的是这个方法中要将第一个待删除节点的 next 暂存,防止删除多个元素的剩下元素时找不到第一个删除的节点位置。除此之外,还要注意 node = next ,防止有两个连续的待删除节点在头节点位置而漏掉第二个待删除的节点。
// 删除双向链表中指定的所有元素节点
public void removeAllVal(int val) {
// 先找到待删除节点
for(DoubleNode node = first; node != null;) {
if (node.val == val) {
// 暂存 next 节点地址,防止继续删除找不到之前删除的位置
DoubleNode next = node.next;
deleteNode(node);
node = next;
} else {
node = node.next;
}
}
}
图解:
4.3 删除指定索引节点
// 删除指定索引节点
public void removeIndexVal(int index) {
if(rangeCheck(index)) {
// 1.通过索引找到该节点的位置
DoubleNode node = node(index);
// 2.删除该节点
deleteNode(node);
size--;
} else {
System.out.println("不能删除!");
}
}
5. 查找操作
5.1 判断链表中是否包含指定元素
// 判断链表中是否包含指定值
public boolean contain(int val) {
DoubleNode node = new DoubleNode(val);
node = first;
while(node != null) {
if (node.val == val) {
return true;
}
node = node.next;
}
return false;
}
5.2 根据索引查找所在节点元素
// 根据索引 index 取得节点值
public int get(int index) {
if (rangeCheck(index)) {
DoubleNode node = node(index);
return node.val;
} else {
return -1;
}
}
6. 修改操作
// 修改
public void set(int index, int newVal) {
if (rangeCheck(index)) {
DoubleNode node = node(index);
node.val = newVal;
} else {
System.err.println("不能修改!");
}
}
总结
- 双链表与单链表的区别:双链表可以即从前向后遍历,也可以从后向前遍历;但是单链表只能从前向后遍历。
- 添加元素重要的是首先找到要添加元素的节点位置,然后根据分治思想,先处理指定节点和前面节点的链接,然后再处理指定节点和后面节点的链接。
- 删除元素重要的是弄清除在任意位置的节点如何删除,思想方法也是分治。
- 查找,修改操作主要是找到指定元素节点的位置,剩下的就很简单。
- 涉及到索引的方法都需要判断索引是否合法。
- 想不到就画图,画图很重要!