赞
踩
文章链接之前所介绍的是单向链表,查找元素只能从头节点开始寻找,判断出符合条件的元素,时间复杂度为O(n)。当链表节点数目过多时,查询性能下降。而有了双向链表后,我们可以从两个方向查询元素,提升查询效率。
双向链表是一种数据结构,由若干个节点构成,其中每个节点均由三部分构成,分别是前驱节点,元素,后继节点。双向链表中的节点在内存中是游离状态存在的,如下图所示:
1.1 Java集合框架中LinkedList底层就是通过双向链表实现的,我们可以通过查看,阅读源码进行分析。
1.2 MySQL的Innodb存储引擎管理的数据页可以组成一个双向链表,而每个数据页中的记录会按照主键值从小到大的顺序组成一个 单向链表。如下图所示:
查询方式:对半查找,若查找的位置小于链表长度的一半,则从头结点开始顺序查找;否则,从尾结点开始逆序查找,这样做可以提高查询效率。
//根据索引找到节点(查找目标节点位置小于链表长度的一半就从前往后找,大于的话从后往前找话) private Node<E> node(int index) { rangeCheck(index); Node<E> 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; }
之前单项链表新增节点操纵需要获取到 index 前面的节点,现在双向链表不需要,可以通过 prev 获取到前驱节点,next 获取到后继节点。
add(E) -- 在链表尾部添加元素,将元素封装到节点中,创建新节点,让新节点和前一个节点建立双向链表的关系。
add(int index,E e) -- 在指定位置插入元素,其过程实际上就是断开链,重新构建链的过程。
双向链表只有一个节点的时候,如下图所示:
@Override public void add(int index, E element) { rangeCheckForAdd(index); //一开始index == 0 size() == 0 则 old.next == null if (size == index){//往最后面添加元素 //获取到最后一个节点 Node<E> old = last; //创建新的尾节点,last指向新的尾节点 last = new Node<>(last, element, null); if (old == null){ first = last; }else { //之前的尾节点的next指向新的尾节点 old.next = last; } }else { //获取指定index的节点 Node<E> next = node(index); //获取指定index的节点前一个节点 Node<E> prev = next.prev; //创建新节点并指定他的prev和next Node<E> node = new Node<>(prev, element, next); next.prev = node; if (prev == null){ first = node; }else{ prev.next = node; } } size++; }
remove(int index) – 删除指定位置的元素,其过程实际上依然是断开链,重新构建链的过程。
@Override public E remove(int index) { rangeCheck(index); //获取index位置的节点 Node<E> node = node(index); //获取index位置的上一个节点 Node<E> prev = node.prev; //获取index位置的下一个节点 Node<E> next = node.next; //prev == null 则说明删除的是第一个节点 0位置节点 if (prev == null){ first = next; }else { prev.next = next; } //next == null 则说明删除的是最后一个节点 size()位置节点 if (next == null){ last = prev; }else{ next.prev = prev; } size--; return node.element; }
package com.hbx.linkedList.bidirection; import com.hbx.module.AbstractList; public class LinkedList<E> extends AbstractList<E> { private Node<E> first; private Node<E> last; // 链表中的节点 private static class Node<E> { E element; // 节点元素 Node<E> prev; // 节点指向下一个节点 Node<E> next; // 节点指向下一个节点 public Node(Node<E> prev, E element, Node<E> next) { this.prev = prev; this.element = element; this.next = next; } @Override public String toString(){ StringBuilder sb = new StringBuilder(); if(prev != null){ sb.append(prev.element); }else{ sb.append("null"); } sb.append("_").append(element).append("_"); if(next != null){ sb.append(next.element); }else{ sb.append("null"); } return sb.toString(); } } @Override public void clear() { size = 0; //jvm的gcRoots(可达性分析) first = null; last = null; } @Override public E get(int index) { return node(index).element; } /** * 根据索引找到节点 */ private Node<E> node(int index) { rangeCheck(index); if (index < (size >> 1)) { // 索引小于一半从前往后找 Node<E> node = first; for (int i = 0; i < index; i++) { node = node.next; } return node; } else { // 索引大于一半从后往前找 Node<E> node = last; for (int i = size - 1; i > index; i--) { node = node.prev; } return node; } } @Override public E set(int index, E element) { /* * 最好:O(1) * 最坏:O(n) * 平均:O(n) */ E old = node(index).element; node(index).element = element; return old; } @Override public void add(int index, E element) { rangeCheckForAdd(index); //一开始index == 0 size() == 0 old.next == null if (size == index){//往最后面添加元素 Node<E> old = last; last = new Node<>(old, element, null); if (old == null){ first = last; }else { old.next = last; } }else { Node<E> next = node(index); Node<E> prev = next.prev; Node<E> node = new Node<>(prev, element, next); next.prev = node; if (prev == null){ first = node; }else{ prev.next = node; } } size++; } @Override public E remove(int index) { rangeCheck(index); Node<E> node = node(index); Node<E> prev = node.prev; Node<E> next = node.next; if (prev == null){ first = next; }else { prev.next = next; } if (next == null){ last = prev; }else{ next.prev = prev; } size--; return node.element; } @Override public int indexOf(E element) { // 有个注意点, 如果传入元素为null, 则不能调用equals方法, 否则会空指针 // 因此需要对元素是否为null做分别处理 Node<E> node = first; if (element == null) { for (int i = 0; i < size; i++) { if (node.element == null) return i; node = node.next; } } else { for (int i = 0; i < size; i++) { if (node.element.equals(element)) return i; node = node.next; } } return ELEMENT_NOT_FOUND; } @Override public String toString() { StringBuilder string = new StringBuilder(); string.append("[size=").append(size).append(", "); Node<E> node = first; for (int i = 0; i < size; i++) { if (i != 0) { string.append(", "); } string.append(node); node = node.next; } string.append("]"); return string.toString(); } }
@Test public void test(){ List<Integer> list = new LinkedList<>(); list.add(11); list.add(22); list.add(33); list.add(44); list.add(0,55);//55, 11,22, 33, 44 list.add(2,66);//55, 11, 66, 22, 33, 44 list.add(list.size(),77);//55, 11, 66, 22, 33, 44, 77 list.remove(0); list.remove(2); list.remove(list.size()-1); System.out.println(list); }
粗略对比一下删除的操作数量:操作数量缩减了近一半
动态数组:开辟、销毁内存空间的次数相对较少,但可能造成内存空间浪费(可以通过缩容解决)
双向链表:开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。