前面两节内容咱们详细介绍了ArrayList,一是手写实现ArrayList数据结构,而是经过分析ArrayList源码看看内置实现,关于集合内容一如既往,本节课咱们继续学习集合LinkedList,咱们首先入门LinkedList数据结构,而后再去看看LinkedList源码是如何实现的,咱们开始吧。数据结构
LinkedList内置是经过双链表数据结构来存储数据,和ArrayList不一样的是,ArrayList属于真正意义物理意义上的线性结构,而LinkedList也属于线性链表,只不过须要经过咱们手动来关联先后节点数据,同时呢,双链表和单链表只是在结构上有所不一样而已,只是双链表多了一个前驱节点,其余无差别,那么到底何为双链表呢?在咱们平常生活中处处都是这样的例子,好比咱们音乐播放器应该算比较形象了,以下:app
接下来咱们来实现单链表,而后对单链表进行改形成双链表,咱们看到如上播放器,单链表只是少了前驱节点,可是有后继节点(如上写错了),因此咱们须要定义一个节点,而后在此节点上有链接下一节点的引用(在C或C++中为指针),和当前节点所存储的数据,因此咱们定义以下泛型节点类:ide
public class Node<T> { //当前节点值 public T data; //后继节点 public Node next; public Node(T data) { this.data = data; } }
接下来则是定义链表来操做上述节点类并存储数据了, 这里咱们稍微作的简单点,在链表中会存在头节点和尾节点,这里呢咱们经过来头节点来操做,等咱们升级到双链表时再来定义尾节点,因此在单链表中有头节点和链表长度两个变量,以下:性能
public class MyLinkedList<T> { //头节点 private Node head; //链表元素长度 private int length; }
舒适提示:这里我就不给你们画图演示了,自行脑补,实在感受绕的话本身在画板或纸上画一下就明白了,我也是在纸上画了一番才动手写代码的。首先咱们须要考虑头节点和尾节点即播放器中第一首歌和最后一首歌,而后针对指定位置添加歌曲经过next串联就造成了歌曲列表,更为形象的例子当属咱们吃过的串串了。那么接下来咱们完成往播放器列表中添加第一个首歌,此时咱们应该想,头节点是否添加了第一首歌,若不存在则直接实例化头节点便可,若已存在第一首歌,咱们则将从新实例化一首歌,而后将其已添加的第一首歌的引用赋值给新添加的歌曲的next,因此就有了以下方法:学习
//添加至头结点 public void addToHead(T data) { if (head == null) { head = new Node(data); } else { Node temp = head; head = new Node(data); head.next = temp; } length++; }
好了,将新添加的歌曲放在第一首咱们已经彻底搞定了,而后咱们再来往歌曲列表中最后添加一首歌曲,这个时候咱们肿么知道是最后一首呢,只要next为空,说明就是最后一首歌曲,这就是判断依据,这点就不用我再过多解释了,那么就有了以下方法:测试
//添加至尾节点 public void addToTail(T data) { Node temp = head; while (temp.next != null) { temp = temp.next; } temp.next = new Node(data); length++; }
单链表的肯定就在这里,咱们只能循环遍历才能找到最后一首,而后添加对应歌曲,因此当数据量足够大时,可想其性能。接下来则是最重要的一块了,咱们想要在指定歌曲下添加歌曲,这个时候就涉及到找到对应歌曲索引而后添加数据,ui
//添加到指定索引元素 public void add(int index, T data) { if (index < 0) { throw new RuntimeException("非法索引"); } if (index > length) { throw new RuntimeException("超出索引边界"); } if (head == null || index == 0) { addToHead(data); return; } //头节点 Node temp = head; //指定索引下一节点 Node holder; for (int i = 0; i < index - 1 && temp.next != null; i++) { temp = temp.next; } //未插入节点时指定索引下一节点 holder = temp.next; //指定索引节点下一节点即待插入的节点 temp.next = new Node(data); //将列表中指定索引节点下一节点引用指向指定待插入节点(此时指定索引下节点即为待插入节点,而后再下一节点即为待插入节点) temp.next.next = holder; length++; }
接下来则是根据指定索引查找元素,我就不解释了,直接上代码,以下this
//根据索引查找元素 public T find(int index) { if (index < 0) { throw new RuntimeException("非法索引"); } if (length == 0 || index > length) { throw new RuntimeException("超出索引边界"); } Node temp = head; for (int i = 0; i < index; i++) { temp = temp.next; } return (T) temp.data; }
最后老规矩重写toString方法,打印链表数据,以下:spa
//链表元素大小 public int size() { return length; } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node temp = head; while (temp != null) { sb.append(temp.data); sb.append(","); temp = temp.next; } if (sb.charAt(sb.length() - 1) == ',') { sb.delete(sb.length() - 1, sb.length()); } return sb.toString(); }
最后咱们来往播放器列表中添加歌曲作个测试吧,走你,以下:3d
public class Main { public static void main(String[] args) { MyLinkedList<Integer> list = new MyLinkedList<>(); //添加元素11到头节点 list.addToHead(11); System.out.println(list); //添加元素15到尾节点 list.addToTail(15); System.out.println(list); //添加元素12到头节点 list.addToHead(12); System.out.println(list); //添加元素13到头节点 list.addToHead(13); System.out.println(list); //添加元素8到尾节点 list.addToTail(8); //添加元素7到尾节点 list.addToTail(7); list.add(2, 9); System.out.println(list); //在索引2位置添加元素9 list.add(2, 9); System.out.println(list); //删除索引为4的元素 list.delete(4); System.out.println(list); } }
有了如上单链表的铺垫,接下来咱们再来实现双链表则是垂手可得了,只不过添加了前驱节点和链表中的尾结点而已,走你,咱们往节点类中添加前驱节点,以下:
public class Node<T> { //当前节点值 public T data; //前驱节点 public Node previous; //后继节点 public Node next; public Node(T data) { this.data = data; } }
同理,咱们在链表类中添加尾节点字段,以下:
public class MyLinkedList<T> { //头节点 private Node head; //尾节点 private Node tail; //链表元素长度 private int length; }
一样,当添加歌曲至首位时,此时咱们也需初始化头节点,只不过这时多了个尾节点,不要紧,这个时候头节点就是尾节点,咱们封装一个初始化头节点和尾节点的方法,以下:
//初始化头接点和尾节点 void initHead(T data) { //初始化头节点 head = new Node(data); //此时尾节点即头节点 tail = head; }
而后添加歌曲至头节点时,只不过多了个前驱节点,也就相应多了一行代码而已,就是将已添加首位歌曲的前驱节点赋给待添加的首位歌曲,以下:
//添加元素至头结点 public void addToHead(T data) { if (head == null) { initHead(data); } else { Node temp = head; head = new Node(data); head.next = temp; temp.previous = head; } length++; }
而添加歌曲至末位时就和上述单链表就有些不一样了,单链表中是直接循环遍历,这里咱们定义了尾节点,因此直接操做尾节点便可,以下:
//添加至尾节点 public void addToTail(T data) { if (size() == 0) { initHead(data); } else { Node temp = tail; tail = new Node(data); temp.next = tail; tail.previous = temp; } length++; }
接下来又是添加指定索引元素的核心方法了,其实也很是简单,我都将注释给你写好了,仍是看不懂,建议到纸上画画哈。
//添加指定索引元素 public void add(int index, T data) { if (index < 0) { throw new RuntimeException("非法索引"); } if (index > length) { throw new RuntimeException("超出索引边界"); } if (head == null || index == 0) { initHead(data); return; } //头节点 Node temp = head; //定义获取指定索引节点下一节点 Node holder; for (int i = 0; i < index - 1 && temp.next != null; i++) { temp = temp.next; } //当前节点的下一节点 holder = temp.next; //要添加的下一节点 temp.next = new Node(data); //插入节点的后继节点为当前节点下一节点 temp.next.next = holder; //当前节点下一前驱节点为插入节点 temp.next.next.previous = temp.next; length++; }
不管是添加仍是删除最重要的是咱们须要想清楚,添加时和删除后前驱节点和后继节点分别指向谁,把这个问题想明白了,那也就没什么了,走你,删除方法:
//删除指定索引元素 public void delete(int index) { if (index < 0) { throw new RuntimeException("非法索引"); } if (length == 0 || index > length) { throw new RuntimeException("超出索引边界"); } Node temp = head; for (int i = 0; i < index - 1 && temp.next != null; i++) { temp = temp.next; } temp.next.next.previous = temp; temp.next = temp.next.next; length--; }
为了验证咱们所写代码,咱们打印出对应节点的前驱和后继节点,以下:
public int size() { return length; } @Override public String toString() { StringBuilder sb = new StringBuilder(); Node temp = head; while (temp != null) { sb.append(temp.data); sb.append(","); if (temp.previous != null && temp.next != null) { System.out.println(temp.previous.data + "<-(" + temp.data + ")->" + temp.next.data); } temp = temp.next; } if (sb.charAt(sb.length() - 1) == ',') { sb.delete(sb.length() - 1, sb.length()); } return sb.toString(); }
控制台测试数据和单链表中同样,结果数据以下(固然咱们能够分开打印对应节点前驱和后继节点去验证也是阔以的,这里我也验证过来,么有任何问题):
本节咱们经过手写代码实现了单链表和双链表,仍是很是简单,下一节咱们详细分析LinkedList源码,感谢您的阅读,咱们下节见