java实现双向链表
持久对象
定义:程序通常是会在程序运行的时候 根据某些条件来创建新对象,在编译的时候并不知道要创建对象的类型 数量,数组只能解决java中基本数据类型的存储,而java的实用类库提供了一套相当完整的容器类来解决这个问题,四种基本的类型 list set map queue,这些容器类都提供了自动调整自己的长度的特性,又为了避免将错误类型对象的引用 放进容器中,又引进了泛型来创建某类型的容器类对象,
Collection:为了我们创建的代码更加通用。通过接口接收参数的时候的向上转型特性 我们可以传入他子类的具体实现 它的常用子类有list set。所以他们的所有具体实现都可以向上转型看作为Collection对象,
list的特性:必须按照插入的顺序来保存元素,
第一种实现:最常用的arrylist:结构类似于数组,所以访问元素的时候可以直接通过索引来访问任何位置的数据,但是当插入元素的时候默认是追加到数组的最后,那么数组当中原有的元素的位置不变 只是申请开辟了一块内存空间和新增加了一个索引,其他都没有变化,但是当向其他位置插入元素的时候,会先申请开辟一块内存空间和一个新索引 但是这个索引不是给新插入元素使用的,而是给数组当中最后一个元素使用的,新元素会插入到指定索引位置 代替原索引处的元素 并将该元素以及其后面的所有元素全部向后移动,所以这个是浪费时间的,而删除呢就是一样的原理,队尾删除很简单,其他位置删除。位于被删除元素后面的元素的位置全部向前移动。所以一样很浪费时间。所以该实现只是适用于随机访问元素或者遍历元素,因为他的底层是由数组来实现的。
第二种常用的实现是:linkedlist,它是基于链表实现的,链表有很多种,
链表是一系列的节点组成的,这些点不需要在内存中相连,
单链表是由一个头结点开始。然后依次插入新的节点。每个节点包含两个部分一个是数据的引用或者基本类型的数据 和下一个节点的存储地址。这样一个链表向外暴露的只是第一个头结点。所以只要知道头结点就可以直接找到剩下其余的节点。
单链表的结构如下
增加
删除节点
单链表的内存结构如下图:
头结点不存储数据 其他节点存储的结构看下图 是数据加上下一个节点的地址
对比一下 arryList与链表实现的linkedlist之间的优劣势
LinkedList集合中的元素也是有序的,有索引,为什么和ArrayList相比查找比较慢,增删快呢?
我们打个比方:LinkedList和ArrayList中都装了10个人。
在ArrayList集合中的10个人是这样的:Arraylis由于是类似于数组它本身就有标号0,,,,2,3,,4........。每个人都站在一个标号上,比如我要找4号,我说4号出来,站在4号的人,一看自己的位置标号,就知道叫的是他,他就直接出来了,所以比较快。如果我要在4号位置加个人,那么从4号开始后面的的人都要往后移,把4号位置腾出来,所以比较麻烦。
而在LinkedList集合中的每个人都是随便站的。但是他们中的每个人都认识一个人。并且每两个认识的人之间都有一个链子把他们连接起来。所有人都连接完之后,顺着链子看,就也有一个顺序,每个人就也有一个序号,但是这个序号并没有标出来。也就是说相当于有一个隐式的序号。所以:比如我要叫4号出来的时候,他们并不知道自己到底谁是4号,所以就要顺着链子从头开始查一下,查到4号是谁了,4号就出来了。也就是说,每次要找n号位置的人时,都要从头查一遍,看谁是n号。所以查找比较麻烦。至于增删效率高,就很容易理解了。
其实就是一点,就是相当于说:ArrayList集合中的索引是标示出来的,而LinkedList集合中的索引是隐式的只能一个接着一个的找,这样就很容易理解,为什么LinkedList集合中也有索引为什么查找效率就比Arraylist低的原因了。
如果我们在表的第一项处插入或者删除元素该操作花费的时间为常数,但是如果我们需要在最后一项插入或者删除元素这是一个花费时间为O(N)的操作。
我们可以用双向链表来解决这个问题。双向链表的每一个结点都有一条指向其后继结点的next链和一条指向其前结点的pre链。双向链表既可以从第一项开始遍历也可以从最后一项开始往前遍历,双向链表可以用下图表示:
增加节点的时候
删除节点
java的linkedlist实现了双向链表 看下linkedlist的用法
package test; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Stack; import org.junit.Before; public class Test { public static LinkedList<String> dl; public static ArrayList<String> l; @Before public void init() { dl = new LinkedList<String>(); dl.add("N1"); dl.add("N2"); dl.add("N3"); dl.add("N4"); dl.add("N5"); l = new ArrayList<String>(); l.add("N1"); l.add("N2"); l.add("N3"); l.add("N4"); l.add("N5"); } @org.junit.Test public void test() { for (String str : dl) { System.out.println("双向链表:" + str); } for (String str : l) { System.out.println("普通arrylist:" + str); } } @org.junit.Test public void add() { // 双向链表独有的向链表头部添加元素 dl.addFirst("N6"); dl.addLast("N7"); } @org.junit.Test public void del() { dl.remove("N1"); for (String str : dl) { System.out.println("双向链表:" + str); } l.remove("N1"); for (String str : l) { System.out.println("普通arrylist:" + str); } } /** * 模仿栈的后进后出的pop * * @return */ public static String pop() { return dl.removeLast(); } /** * */ @org.junit.Test public void stack() { System.out.println(Test.pop()); for (String str : dl) { System.out.println("双向链表:" + str); } } @org.junit.Test public void t() { Stack<String> statck = new Stack<String>(); statck.push("N1"); statck.push("N2"); statck.push("N3"); statck.push("N4"); statck.push("N5"); System.out.println("pop:" + statck.pop()); for (String str : statck) { System.out.println(str); } } /** * 实现队列 * 队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除, * 所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。[1] * 队列队首删除 队尾插入 */ @org.junit.Test public void quene(){ LinkedList<String> quene = new LinkedList<String>(); quene = (LinkedList<String>)Collections.synchronizedCollection(quene); Test.addQuene(quene, "n1"); Test.addQuene(quene, "n2"); Test.addQuene(quene, "n3"); Test.frecah(quene); System.out.println("---------------------------------"); Test.delQuene(quene); Test.frecah(quene); } public static void frecah(List<String> list) { for(String str:list){ System.out.println(str); } } /** * 每次插入都会插入队尾 * @param quene * @param o */ public static void addQuene(LinkedList<String> quene,String o){ quene.add(o); } /** * 删除 从队首删除 */ public static void delQuene(LinkedList<String> quene){ quene.removeFirst(); } }
下面展示一下使用java实现双向链表的代码
1 package test.link; 2 3 4 5 public class DoubleLink<T> { 6 7 private class Node<T> { 8 /** 9 * 节点值 10 */ 11 12 private T vlaue; 13 /** 14 * 前一个节点 15 */ 16 17 private Node<T> prev; 18 /** 19 * 后一个节点 20 */ 21 22 private Node<T> prex; 23 24 public Node(T value,Node<T> prev,Node<T> prex){ 25 this.vlaue = value; 26 this.prev = prev; 27 this.prex = prex; 28 } 29 } 30 /** 31 * 链表长度 32 */ 33 private int size ; 34 /** 35 * 头节点 36 */ 37 private Node<T> head; 38 public DoubleLink(){ 39 /** 40 * 头结点不存储值 并且头结点初始化时 就一个头结点。 41 * 所以头结点的前后节点都是自己 42 * 并且这个链表的长度为0; 43 */ 44 head = new Node<>(null, null, null); 45 head.prev = head.prex ; 46 head = head.prex; 47 size = 0; 48 } 49 public int getSize(){ 50 return this.size; 51 } 52 /** 53 * 判断链表的长度是否为空 54 */ 55 public boolean isEmplty(){ 56 return size == 0; 57 } 58 /** 59 * 判断索引是否超出范围 60 */ 61 public void checkIndex(int index){ 62 if(index<0||index>=size){ 63 throw new IndexOutOfBoundsException(); 64 } 65 return; 66 } 67 /** 68 * 通过索引获取链表当中的节点 69 * 70 */ 71 public Node<T> getNode(int index){ 72 /** 73 * 检查该索引是否超出范围 74 */ 75 checkIndex(index); 76 /** 77 * 当索引的值小于该链表长度的一半时,那么从链表的头结点开始向后找是最快的 78 */ 79 if(index<size/2){ 80 Node<T> cur = head.prex; 81 for(int i=0;i<index;i++){ 82 cur = cur.prex; 83 } 84 return cur; 85 } 86 /** 87 * 当索引值位于链表的后半段时,则从链表的另端开始找是最快的 88 */ 89 /** 90 * 此 91 */ 92 Node<T> cur = head.prev; 93 int newIndex = size - (index+1); 94 for(int i=0;i<newIndex;i++){ 95 cur = cur.prev; 96 } 97 return cur; 98 } 99 /** 100 * 获取节点当中的值 101 */ 102 public T getValue(Node<T> cur){ 103 return cur.vlaue; 104 } 105 /** 106 * 获取第一个节点的值 107 */ 108 public T getFirst(){ 109 return getValue(getNode(0)); 110 } 111 /** 112 * 获取最后一个节点的值 113 */ 114 public T getLast(){ 115 return getValue(getNode(size-1)); 116 } 117 /** 118 * 插入节点 119 */ 120 public void inesert(int index,T value){ 121 //如果这次插入时 链表是空的 122 if(index==0){ 123 //这个节点的 124 Node<T> cur = new Node<T>(value, head, head.prex); 125 head.prex.prev = cur; 126 head.prex = cur; 127 size++; 128 return; 129 } 130 /** 131 * 先根据给出的插入位置 找到该链表原来在此位置的节点 132 */ 133 Node<T> node = getNode(index); 134 /** 135 *放置的位置的前一个节点就是原节点的前置节点 而后节点就是原节点 136 */ 137 Node<T> cur = new Node<T>(value,node.prev,node); 138 /** 139 * 现将该位置也就是 原节点的前节点的后节点 赋值成为新节点 140 * 然后将新节点的后置节点的值赋值成为原节点 141 */ 142 node.prev.prex = cur; 143 node.prev = cur; 144 size++; 145 } 146 /** 147 * 向表头插入数据 148 */ 149 public void insertTo(T Value) 150 { 151 inesert(0,Value); 152 } 153 /** 154 * 将元素插入到链表的尾部 155 */ 156 public void insertTotatil(T vlaue){ 157 Node<T> cur = new Node<>(vlaue,head.prev, head); 158 //head.prev 代表原来的尾部节点 159 //遵循两个原则 一 新插入节点的前一个节点的后一个节点为新节点。新节点的后一个节点的前一个节点是新节点 160 head.prev.prex = cur; 161 head.prev = cur; 162 size++; 163 } 164 /** 165 * 删除节点的方法 166 */ 167 public void del(int index){ 168 checkIndex(index); 169 Node<T> cur = getNode(index); 170 //记住此时的指针还没断开 赋值以后才相当于断开 171 cur.prev.prex = cur.prex; 172 cur.prex.prev = cur.prev; 173 size--; 174 cur = null; 175 return; 176 } 177 /** 178 * 删除第一个节点 179 */ 180 public void delFirst(){ 181 del(0); 182 } 183 /** 184 * 删除最后一个节点 185 */ 186 public void delLast(){ 187 del(size-1); 188 } 189 }