双向链表结构分析
双向链表描述
双向链表也叫双链表,它的每个数据结点都有两个指针,分别指向前驱结点和后继节点,同时有一个数据域来保存数据,双向链表的图示如下:
从图片可以看出,双链表的头结点的前驱结点和尾结点的后继结点为空,这一点要注意,对双链表的操作要检查这两种情况。
双向链表结构
每个数据结点都有两个指针,分别指向前驱结点和后继节点,同时有一个数据域来保存数据,我们先来定义一个数据结点的结构:
/** * 内部Node,用于存储链表的结点 * @author mingshan * */ private class Node { // 存储节点的值 E item; // 指向节点的前驱结点 Node next; // 指向节点的后继结点 Node prev; Node(Node prev, E element, Node next) { this.item = element; this.next = next; this.prev = prev; } }
从Node类我们可以看出,item代表结点存储的元素,next指向链表的后继结点,prev指向前驱结点,由于我们在写单链表时定义了LinkedList接口,所以我们直接实现这个接口好了。
下面是对双向链表的功能的具体分析。
add方法
我们现在定义三个成员变量:size, first,last,用来表示链表结点的个数以及指向链表头结点和尾节点,代码与解释如下:
// 链表结点数量 private int size = 0; // 指向头结点 private Node first; // 指向尾结点 private Node last;
add(E data)
首先我们来实现add(E data)方法,代码如下:
@Override public boolean add(E data) { if (data == null) throw new NullPointerException(); // 将当前结点作为尾结点 linkLast(data); return true; }
在这个方法中我们调用了linkLast(data)这个方法来将当前节点作为尾结点,代码如下:
/** * 将当前结点作为尾结点 * @param e */ private void linkLast(E data) { final Node l = last; final Node newNode = new Node(l, data, null); last = newNode; if (l == null) { first = newNode; } else { // 原来的尾结点指向新结点 l.next = newNode; } size++; }
在linkLast方法中,先获取尾结点,再构造新节点,让last指向新节点,然后开始判断原来的尾节点是否为空,为空代表链表为空,让first指向新结点即可;如果不为空,那么原来的尾结点的next指向新结点。
add(int index, E data)
我们再来add(int index, E data)这个方法怎么实现,代码如下:
@Override public void add(int index, E data) { if (data == null) throw new NullPointerException(); checkPositionIndex(index); // 判断在该索引的结点是不是尾结点 if (size == index) { // 将当前结点作为尾结点 linkLast(data); } else { // 将结点插入到指定位置index(原来的结点之前) linkBefore(index, data); } }
这个方法的作用是向索引位置index处添加结点,这个时候我们就需要检测index是否有效,checkPositionIndex(index)相关的方法如下:
/** * 检测索引位置是否合法 * @param index */ private void checkPositionIndex(int index) { if (!isPositionIndex(index)) throw new IllegalArgumentException("参数不合法"); } private boolean isPositionIndex(int index) { return index >= 0 && index <= size; }
如果index符合以上要求,那么就要判断在该索引位置的结点是不是尾结点,如果是,直接调用linkLast(data) 将当前节点作为尾结点;如果不是,将结点插入到指定位置index(原来的结点之前),此时调用linkBefore(index, data)方法,代码如下:
/** * 将结点插入到指定位置index(原来的结点之前) * @param index * @param data */ private void linkBefore(int index, E data) { Node curr = node(index); Node pred = curr.prev; Node newNode = new Node(pred, data, curr); curr.prev = newNode; if (pred == null) { first = newNode; } else { pred.next = newNode; } size++; }
在该方法中,我们需要获取在该索引位置的节点curr,curr的前驱结点pred,以及构造新节点newNode,同时还要将curr的前驱结点指向新节点,然后判断pred是否为空,如果pred为空,说明curr为头结点,那么此时就让新节点作为头结点;如果不为空,说明此时属于一般情况,在链表的中间的某个位置插入元素,那么就让prev的后继结点指向新节点就行了。
remove方法
remove(int index)
根据索引位置来删除元素,代码如下:
@Override public E remove(int index) { checkElementIndex(index); // 获取在该索引位置上的结点 Node c = node(index); E element = c.item; Node prev = c.prev; Node next = c.next; // 代表头结点 if (prev == null) { // 将下一个结点置为头结点 first = next; // 将下一个结点的前驱结点置为null next.prev = null; // 将原来头结点的后继结点置为null c.next = null; } else if (next == null) { // 移除尾结点 last = prev; // 前一个结点的后继结点置为null prev.next = null; // 将原来尾结点的前驱结点置为null c.prev = null; } else { // 属于一般情况 // 将前一个结点的后继结点置为原结点的后继结点 prev.next = next; // 将后一个结点的前驱结点置为原结点的前驱结点 next.prev = prev; // 切断当前删除的结点的前驱和后继结点 c.prev = null; c.next = null; } c.item = null; size--; return element; }
在该方法中,还是要先检测索引是否合法,这里是checkElementIndex(index)方法,代码如下:
/** * 检测元素位置是否合法 * @param index */ private void checkElementIndex(int index) { if (!isElementIndex(index)) throw new IndexOutOfBoundsException("查找元素位置不合法"); } private boolean isElementIndex(int index) { return index >= 0 && index < size; }
检测通过后就要获取在该索引处的结点信息,包括结点的数据,前驱节点和后继节点,此时有三种情况需要考虑:
- 如果prev为空,代表该结点为头结点,那么就将下一个结点置为头结点,然后将下一个结点的前驱结点置为null,最后将原来头结点的后继结点置为null。说白了就是讲头结点移除,同时解除头结点与后面一个节点的关系。
- 如果next为空,代表该结点为尾节点,那么就将尾节点的前驱节点作为尾节点,前一个结点的后继结点置为null,将原来尾结点的前驱结点置为null。
- 如果既不是头结点,又不是尾节点,那么就属于一般情况了,此时将前一个结点的后继结点置为原结点的后继结点,将后一个结点的前驱结点置为原结点的前驱结点,最后切断当前删除的结点的前驱和后继结点。
以上代码可以简化,具体可以参考JDK源码中java.util.LinkedList中的unlink方法,简化后的代码如下:
// 代表头结点 if (prev == null) { first = next; } else { prev.next = next; c.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; c.next = null; }
老铁们整明白了吗( ̄▽ ̄)/,其实和我上面的代码效果是一样的,只是把一般情况合并了,也很好理解。
set方法
set方法将索引位置的结点的值替换成新的值,代码如下:
@Override public E set(int index, E data) { if (data == null) throw new NullPointerException(); checkPositionIndex(index); // 获取原来在该索引位置上的结点 Node oldNode = node(index); // 获取原来结点的值 E oldValue = oldNode.item; // 更新值 oldNode.item = data; return oldValue; }
此时获取当前索引位置的结点用到了node(index)方法,代码如下:
/** * 根据索引获取结点 * @param index * @return */ private Node node(int index) { // 如果当前索引值小于当前链表长度的一半,那么从头结点开始遍历 if (index < size / 2) { Node temp = first; for (int i = 0; i < index; i++) { temp = temp.next; } return temp; } else { // 如果当前索引值大于当前链表长度的一半,那么从尾结点反向遍历 Node temp = last; for (int i = size - 1; i > index; i--) { temp = temp.prev; } return temp; } }
在node方法中,我们进行了折半查找,这样效率会高些吧,哈哈,简单就不说了。
get方法
获取传入索引的结点的值,也需要遍历双链表,就不说了,代码如下:
@Override public E get(int index) { checkElementIndex(index); // 获取其索引的结点 Node node = node(index); return node.item; }
反转双链表
反转双链表,这里我采用是遍历双链表,逐个链接点进行反转。原理是:使用p和q两个指针配合工作,使得两个节点间的指向反向,同时用r记录剩下的链表。
图示如下:
具体代码和步骤参考如下代码:
@Override public void reverse() { if (first != null) { // 代表指向当前进行反转的下一个结点 Node r; // p 代表进行结点指向反转的结点前一个结点 Node p = first; // q 代表进行结点指向反转的当前结点 Node q = first.next; // 首先将head指向的下一个结点置为null // 因为进行链表反转时头结点变成了尾结点,指向的下一个结点必然是null first.next = null; // 进行循环操作,p, q指向向前移动 while (q != null) { // 将当前正在反转的结点的下一个结点指向r r = q.next; // 将当前结点的下一个结点指向其前一个结点(由指向后一个结点改为指向前一个结点) q.next = p; // 将当前结点的prev改为指向下一个结点 p.prev = q; // p和q都向链表后面移一位 // 原来的q变成了p p = q; // 原来的r变成了q q = r; } // 将最后一个结点的prev指向为null p.prev = null; // 将原来的头结点置为尾结点 last = first; // 将最后一个结点置为头结点 first = p; } }
title: 双向链表结构分析
tags: [数据结构, 双向链表]
author: Mingshan
categories: [数据结构, 链表]
date: 2017-12-17
本文来自博客园,作者:mingshan,转载请注明原文链接:https://www.cnblogs.com/mingshan/p/17793531.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)