双向链表的定义
双向链表也叫双面链表,它的每个节点由三部分组成:prev 指针指向前置节点,此节点的数据和 next 指针指向后置节点。
双向链表的数据结构
具体如下图所示:
双向链表有如下特点:
1)可以使用一个head和一个tail分别指向头部和尾部的节点;
2)每个节点都由三部分组成:前一个节点的指针prev、保存的元素item、后一个节点的指针next;
3)双向链表的第一个节点的prev是null
4)双向链表的最后节点的next是null
为什么需要双向链表?
单向链表的遍历方向单一,只能从链头一直遍历到链尾。所以有一个比较大的缺点:当要查询某一个节点的前一个节点时,只能再次从头进行遍历查询,因此效率比较低。
除此之外,还有两个缺点:
1)单向链表要查询某一个节点的前一个节点时,只能再次从头进行遍历查询,因此效率比较低;
2)单线链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除。
所以,这个时候双向链表就派上用场了,改进了单向链表的缺点。
双向链表的应用场景?
单向链表适用于解决一条道走到黑的访问场景,比如给正在排队测温的人测量,只需要测试一次就下一位,无需重复测试。
循环链表适用于具有环状数据结构的场景,比如丢手绢游戏,双向链表则适合需要提供多个方向访问功能的数据结构,比如老师在考场监考,可以来回走动:假设这个考场座位是顺序的。
双向链表的应用举例
双向链表的实现思路主要包含如下几个核心方法:
1)遍历:和单链表不一样的地方,在于不仅可以向前,还可以向后查找;
2)添加:先找到双向链表的最后这个节点,把数据添加到双向链表的最后;
3)修改:通过遍历找到该节点,然后直接吧数据替换掉就可以了,思路与单链表是一样的;
4)删除
下面是具体的代码示例:
public class DoubleLinkedList<E> { // 链表元素的数量 private int size; // 声明头结点 private Node first; // 声明尾节点 private Node last; // 创建Node节点 private class Node{ // 存放内容 public E data; // 指向链表的上一个节点 public Node prev; // 指向下一个节点 public Node next; // 构造方法 public Node() { } public Node(Node prev, E data, Node next) { this.prev = prev; this.data = data; this.next = next; } @Override public String toString(){ return data.toString(); } } // 初始化头结点 public DoubleLinkedList(){ first = null; last = null; size = 0; } /*** * 获取链表中的元素个数 * @return */ public int getSize(){ return size; } /*** * 返回链表是否为空 * @return */ public boolean isEmpty(){ return size == 0; } /*** * 根据链表的index位置添加新的元素e * @param index * @param data */ public void add(int index, E data){ // 调用方法 rangeCheckAdd(index); if (index == size) { // 往最后面添加元素 addLast(data); } else if(index == 0){ addLast(data); } else { // 新添加节点下一个元素 Node nextNode = node(index); // 新添加节点的上一个元素 Node prevNode = nextNode.prev; // 新添加节点 Node newNode = new Node (prevNode, data, nextNode); // next节点的上一个prev指向新节点 nextNode.prev = newNode; // prevNode节点的下一个next指向新节点 prevNode.next = newNode; } size++; } /*** * 在链表头添加新的元素e * @param data */ public void addFirst(E data){ // 创建一个新节点 Node newNode = new Node(null, data, null); if(isEmpty()){ last = newNode; // last -> newNode }else { first.prev = newNode; // first.prev->newNode } newNode.next = first; // newNode.next -> first; first = newNode; size++; } /*** * 在链表末尾添加新的元素e * @param data */ public void addLast(E data){ // 创建一个新节点 Node newNode = new Node(null, data, null); if(isEmpty()){ first = newNode; // first->newNode }else{ last.next = newNode; // last指向的节点指向新节点 newNode.prev = last; // 新节点的前驱指向last指针 } last = newNode; // last指针指向新节点 size++; } /** * 获取index位置对应的节点对象 * @param index * @return */ private Node node(int index) { rangeCheck(index); Node node; if (index < (size >> 1)) { node = first; for (int i = 0; i < index; i++) { node = node.next; } } else { node = last; for (int i = size - 1; i > index; i--) { node = node.prev; } } return node; } @Override public String toString(){ StringBuilder res = new StringBuilder(); res.append("size=").append(size).append(", ["); // 定义一个指针变量 Node cur = first; while(cur != null){ res.append(cur + "->"); cur = cur.next; } res.append("NULL"); res.append("]"); return res.toString(); } // 索引值检查范围方法 private void rangeCheck(int index){ if(index < 0 || index >=size){ // 调用越界处理方法 outOfBounds(index); } } // 添加方法索引检查范围 private void rangeCheckAdd(int index){ if(index < 0 || index >size){ // 调用越界处理方法 outOfBounds(index); } } // 数组索引越界处理 private void outOfBounds(int index){ throw new IndexOutOfBoundsException("index:" + index + ", Size:" + size); } }
陈睿mikechen
10年+大厂架构经验,资深技术专家,就职于阿里巴巴、淘宝、百度等一线互联网大厂。
关注「mikechen」公众号,获取更多技术干货!
后台回复【面试】即可获取《史上最全阿里Java面试题总结》,后台回复【架构】,即可获取《阿里架构师进阶专题全部合集》