双向链表模拟实现
双向链表概述
不同于单链表,双链表可以通过某一结点找到它的前驱,是双向的。并且双链表不仅有头指针head,还有尾指针last
而单链表只能找后继,是单向的
- 双链表图示
1) 构建结点
private static class LindNode {
int val;
LindNode prev;
LindNode next;
LindNode(int val) {
this.val = val;
}
}
private LindNode head; // 链表头部
private LindNode last; // 链表尾部
无头链表但有头结点head 解释:
无头单链表的模拟#头结点注释
2) 头插法
- 思路:
- 将新结点node与头结点双向连接:即 node 后继更改为原头结点head,head前驱更改为新结点node,然后头结点更换为新结点
- 注意事项: 如果链表一开始为空,在head引用其前驱时,就会发生空指针异常! 所以需要添加一个判定条件:当链表为空时,新结点即为头结点和尾结点
//头插法
public void addFirst(int data) {
LindNode node = new LindNode(data);
if (head == null) {
head = node;
last = node;
}else {
node.next = head;
head.prev = node;
head = node;
}
}
- 图解:
3) 尾插法
- 思路:与头插法相差无几
- 将新结点与尾结点 双向连接,尾结点更改为新结点
- 同理,当链表为空,会发生空指针异常。所以依旧是第一个结点手动创建
//尾插法
public void addLast(int data) {
LindNode node = new LindNode(data);
if (head == null) {
head = node;
last = node;
} else {
last.next = node;
node.prev = last;
last = node;
}
}
- 图解:
4) 插入指定位置(第一个数据节点为0号下标)
- 思路:与单向链表大同小异,只不过是多了前驱相连接
- 判断输入位置 index是否合法
- 找到 index 下标结点,新结点与 index的前驱和 index本身 双向连接 即可
- 图解:假设插入的位置是1下标
-
单向链表进行插入,是需要定位到 1 下标的前驱结点即 0 结点。然后基于 0结点来进行插入操作
-
在双向链表,1 结点本身就已经知道它的前驱结点,所以我们直接找到 index 下标结点即可,不用定位到其前驱结点。
-
private LindNode findIndexPos(int index) {
LindNode cur = head;
while (index != 0) {
cur = cur.next;
index--;
}
return cur;
}
- 找到之后,将新结点与插入前后结点 双向连接
//任意位置插入,第一个数据节点为0号下标
public boolean addIndex(int index, int data) {
if (index < 0 || index > size()) {
return false;
}
// 插入位置为头位置,头插法即可
if (index == 0) {
addFirst(data);
return true;
}
// 插入位置为最后,尾插法即可
if (index == size()) {
addLast(data);
return true;
}
// 插入位置为中间
LindNode node = new LindNode(data);
// 找到index下标位置
LindNode cur = findIndexPos(index);
//插入操作
node.next = cur;
node.prev = cur.prev;
cur.prev.next = node;
cur.prev = node;
return true;
}
- 图解:
5) 查找关键字key是否在链表当中
- 思路:与单链表查找一模一样 —— 单链表查找
6) 删除第一次出现关键字为key的节点
- 思路:单指针
- 定义一个单指针 cur 遍历链表,寻找key
- 找到后,将 cur 的 前驱和后继双向连接,即删除成功,直接return
// 前驱与后继双向连接
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
- 但如果是删除头结点和尾结点或者链表只有一个结点,此时就会发生空指针异常。
- 基于第2点,延伸拓展,考虑当 单指针cur 指向 头head与尾last时,增加判断条件,防止出现空指针异常。一步一步完善代码
//删除第一次出现关键字为key的节点
public void remove(int key) {
if (head == null) {
return;
}
LindNode 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 {
if (cur == last) {
// 删除尾结点
last = cur.prev;
last.next = null;
}else {
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
}
// 删除成功,后面的就不删了,直接返回
return;
}
cur = cur.next;
}
}
7) 删除所有值为key的节点
- 将第六点删一次就return语句去掉即可
// 删除所有值为key的节点
public void remove(int key) {
if (head == null) {
return;
}
LindNode 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 {
if (cur == last) {
// 删除尾结点
last = cur.prev;
last.next = null;
}else {
cur.prev.next = cur.next;
cur.next.prev = cur.prev;
}
}
// 删掉return语句,cur一直寻找key,找到一个删一个
}
cur = cur.next;
}
}
8) 得到单链表的长度
- 与单链表求长度一模一样——单链表模拟
9) 清空链表
- 思路:单指针
- 单指针cur 遍历链表,将每一个结点的 prev域和 next 域置空,最后再将头结点与尾结点置空即可
- 光有一个 cur 遍历,cur 前驱后继均置空后,就拿不到后续结点的地址。此时需要再添加一个定位指针 curNext 用于拿到后续结点地址
// 清空链表
public void clear() {
LindNode cur = head;
while (cur != null) {
LindNode curNext = cur.next;
cur.next = null;
cur.prev = null;
cur = curNext;
}
head = last = null;
}