(希望我所描述的,给你带来收获!)——关于阅读笔者数据结构系列,建议先将代码粘至IDE,然后对照文字解释进行理解
开始抛出——什么是链表?
答:链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而顺序表相应的时间复杂度分别是O(logn)和O(1)——————(摘自维基百科)
我的补充:链表具有真正意义上的动态!而动态数组是通过扩容缩容的手段达到一定程度的动态!链表使用“节点”去存储数据,插入数据时就动态的申请节点!
我们首要思考的是,我们应该用什么样的“节点”去存储数据,在这里我采用的是私有内部类的设计——内部类相关知识传送门(待补充!):
节点代码如下:
1 public class LinkedList<E> { 2 private class Node { 3 public E e; //数据域 4 public Node next; //引用域(java中不存在指针说法,但也可以理解成指针) 5 6 public Node() { 7 this(null,null); 8 } 9 10 public Node(E e) { 11 this.e = e; 12 this.next = null; 13 } 14 15 public Node(E e,Node next) { 16 this.e = e; 17 this.next = next; 18 } 19 20 @Override 21 public String toString() { 22 return e.toString(); 23 } 24 }
}
整体代码的补充:
1 public class LinkedList<E> { 2 private class Node { 3 public E e; 4 public Node next; 5 6 public Node() { 7 this(null,null); 8 } 9 10 public Node(E e) { 11 this.e = e; 12 this.next = null; 13 } 14 15 public Node(E e,Node next) { 16 this.e = e; 17 this.next = next; 18 } 19 20 @Override 21 public String toString() { 22 return e.toString(); 23 } 24 } 25 private Node head; 26 private int size; 27 28 public int getSize() { 29 return size; 30 } 31 32 public boolean isEmpty() { 33 return size == 0; 34 } 35 36 public void addFirst(E e) { 37 head = new Node(e,head); 38 size ++; 39 } 40 41 public void add(int index, E e) { 42 if (index < 0 || index > size) 43 throw new IllegalArgumentException("index is illegal"); 44 if (index == 0) 45 addFirst(e); 46 else { 47 Node prev = head; 48 for (int i = 0; i < index - 1; i++) { 49 prev = prev.next; 50 } 51 prev.next = new Node(e,prev.next); 52 size ++; 53 } 54 } 55 56 public void addLast(E e) { 57 add(size,e); 58 } 59 60 //测试打印输出 61 @Override 62 public String toString() { 63 StringBuilder stringBuilder = new StringBuilder(); 64 stringBuilder.append(String.format("LinkedList size = %d\n",size)); 65 stringBuilder.append("head ["); 66 Node cur = head; 67 for (int i = 0; cur != null; cur = cur.next) { 68 stringBuilder.append(cur); 69 if (cur.next != null) 70 stringBuilder.append("->"); 71 } 72 stringBuilder.append("] tail"); 73 return String.valueOf(stringBuilder); 74 } 75 76 public static void main(String[] args) { 77 LinkedList<Integer> linkedList = new LinkedList<>(); 78 for (int i = 0; i < 10; i++) { 79 linkedList.addLast(i); 80 } 81 System.out.println(linkedList.toString()); 82 } 83 }
关于addFirst方法的阐述:
这里我们首先设立一个头节点(head)的概念,在这里!如果链表有数据,头节点其实就是链表头的那个节点(如果链表没有数据呢?那么null就是该链表的“表头”)
1、addFirst也就是说,在链表的第一个位置插入一个数据元素!
2、想要在表头插入一个元素e,必须先创建一个节点去承载 ---->>>Node node = new Node(e);
3、因为这个节点要当表头了,所以它要放在head的前面 ---->>> node.next = head;
4、这个节点成为新的表头了,所以head这个名字要交给这个新的节点 ---->>> head = node;
5、size是链表长度的变量,添加节点成功,则size ++
总结以上:三段代码可以直接浓缩为 head = new Node(e,head); ------->这和Node类的双参构造函数有关
关于add(int index, E e)方法的阐述:
要强调的是,这里的index是我们人为意识去强加给链表的(目的是我们在底层操作的时候,更便于存取数据)——数组具有索引(index),可以通过索引值直接取数据,但链表不可以!
很自然的,add方法是说:我们要在index位置上存储元素e
第一步:我们需要找到 index - 1的那个节点,因为每一个节点只有指向下一个节点的“引用域”(这是单向链表)
第二步:index - 1位置的节点我们称为 prev,待插入的节点称为 cur,我们需要操作的是----->>>cur.next = prev.next;
(prev是(index - 1)位置的节点,那 prev.next 就是index位置的节点了,cur 需要替换 prev.next 的位置得先像 prev 学习,cur.next 要成为index位置上的节点)
第三步:当cur.next在index位置上,要想明白的是prev.next也在index位置上 执行----->>> prev.next = cur;(prev.next 始终是index位置节点,我把cur放在index位置上,那自然的cur.next 就是index + 1上的位置了——插入了新节点cur!)
第四步:新增节点 我们要维护我们的size变量------->>>size ++
总结来说:三个步骤依然可以用-------->prev.next = new Node(e,prev.next);这样的形式来表达!
在阐述add方法的时候,我用了index - 1位置这样的描述,认真阅读了代码的朋友会发现add方法中for循环的条件语句是这样的:
for (int i = 0; i < index - 1; i++) { prev = prev.next; }
该循环表明的是,当 i == index - 2是for的最后一次循环;那index - 2才是待插入位置的前一个位置(prve)?
答:前面阐述的add方法中,笔者只是借用index、index - 1 来表明逻辑上的前后关系、因为实际上在不设立虚拟节点的前提下,我们默认表头的第一个节点就是head节点! 循环体中 prev = prev.next; 语句, 假使 i == 0,第一次循环就已经使得prev在第二个位置上了;假设index可以取-1(-1是0的前一个节点,要在-1 和 0之间插入数据),如果i == 0,prev实则在index为1的位置,要使得插入成功,prev必须往回走两步才能走到-1得位置,因此就是相差2,index - 2的逻辑来得自然(-1这种思考方式个人认为很奇特,读者可以换种思路——比如在index == 3位置上插入节点;可以画图理解)