链表
链表是使用一段任意的存储单元存储线性表(元素之间是一对一的关系,除了第一个和最后一个元素之外)元素的数据结构,链表有两个基本单元组成,由于是不刻意使用连续的存储空间存储元素,所以无法使用物理上的相邻来表示线性表的对应关系,所以就需要创建类结构Node,其中除了表示元素数据的字段之外还需要维护一个指向相邻元素的引用next。定义了Node之后,线性表的各元素可以通过next字段指向组成一条链,链表只需要一个提供一个指向这条链的链首或链尾的引用就可以访问这个线性表。所以单链表的实现首先需要定义一个Node类,然后在链表类list中提供创建链表的方法,提供指向链表的引用即可。以指向链表头为例,可以提供直接指向链表首节点的引用head,也可以添加一个附加头节点,头节点的next域指向首节点,提供一个指向头节点的引用head。
包含头节点和不含头节点
无论链表是否包含头节点,都需要一个引用head,添加了头节点的链表只是实现了操作的统一性,首先给出两种方式下的头插法实现。
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* 2 包含头节点的链表 3 */ 4 5 class list{ 6 Node head=new Node();//创建头节点,head是头节点的引用。head.next指向首节点 7 //头插法 8 public void addAtHead(Object obj){ 9 Node node=new Node(obj,head.next); 10 head.next=node; 11 } 12 } 13 14 15 /* 16 不包含头节点的链表 17 */ 18 19 class list2{ 20 Node head;//head是首节点的引用,head指向首节点 21 //头插法 22 public void addAtHead(Object obj){ 23 Node node=new Node(obj,head); 24 head=node; 25 } 26 }
在第一个节点之前插入元素时,是否包含头节点影响并不大,但是如果是在某节点s之前添加节点q的话(s前驱为p),不包含头节点的话,,且s不是首节点,就要进行如下操作:
q.next=p.next;
p.next=q;
但是如果s是在首节点的话,操作代码就是另一种形式:
q.next=head;
head=q;
如果是包含头节点的话,无论s是否是首节点,只要一种操作形式:
q.next=p.next;
p.next=q;
所以说,包含头节点的链表实现了操作的统一性(参考)。
插入和删除
单链表的插入和删除操作有两种方法:头结点插入(删除)和末节点插入(删除)。无论是头结点插入还是末节点插入,需要考虑的特殊情况是如果链表是空的话,方法也适合。同时,如果链表是空的话,需要更新tail的值为指向插入的节点。无论是头结点删除还是末节点删除,首先检测链表大小,如果为空的话,输出提示信息,不做其他操作。另外还需要考虑链表只有一个节点的情况。总而言之,头结点的插入和删除操作,时间复杂度均是O(1),而末节点的插入操作是O(1),删除操作是O(n)。相比而言,头结点插入和删除效率较高。前插法、前删法、末插法的示意图如下:
前插法
前删法
末插法
链表的插入时,首先根据传入的参数创建对象Node,然后通过修改引用的指向将这些Node对象串在一条链中,就构成了链表。
具体实现代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /** 2 * Created by hfz on 2016/8/2. 3 * 单链表的实现需要定义两个类,一个是Node类,存储了节点的数据值以及后继节点的引用。 4 * 另外一个类就是list,用于存储这些Node节点。 5 */ 6 public class list { 7 private Node head=new Node();//指向链表首节点引用,虽然head中各元素为null,不过head不为null,是一个对象了 8 private Node tail=new Node();//指向链表末节点引用 9 private int size=0; 10 /* 11 无论是头结点插入还是末节点插入,需要考虑的特殊情况是如果链表是空的话,方法适合也适合。同时,如果链表是空的话,需要更新 12 tail的值为指向插入的节点。 13 无论是头结点删除还是末节点删除,首先检测链表大小,如果为空的话,输出提示信息,不做其他操作。另外还需要考虑链表只有一个节点 14 的情况。 15 总而言之,头结点的插入和删除操作,时间复杂度均是O(1),而末节点的插入操作是O(1),删除操作是O(n)。 16 相比而言,头结点插入和删除效率较高。 17 */ 18 19 /* 20 21 头节点插入/删除的时候,需要特殊考虑的情况是tail的设置 22 23 */ 24 25 26 /* 27 头结点插入: 28 1.将首节点引用赋值给插入节点的next域。 29 2.将插入节点的引用赋值给首节点。 30 这种方法在链表为空是依然适用,只不过需要设置tail值为插入节点的引用。 31 操作在O(1)时间内完成 32 */ 33 //头结点插入 34 public void addAtHead(Object obj){ 35 Node node=new Node(obj,head); 36 head=node;//链表的head指向新插入的节点 37 if(size++==0){//如果当前是空链表,更新末节点引用。 38 tail=node; 39 } 40 } 41 /* 42 头结点删除:只需要将头指针指向的节点的next域赋值给头指针即可。 43 即使单链表只有一个节点,这种方法依然正确,只不过,这种情况下需要设置tail为null 44 操作在O(1)时间内完成 45 */ 46 //头结点删除 47 public void deleteAtHead(){ 48 if(size>0) 49 { 50 head = head.getNext(); 51 if (size-- == 1) { 52 tail = new Node(); 53 } 54 } 55 else{ 56 System.out.println("链表为空,不能删除节点"); 57 } 58 } 59 /* 60 61 末节点插入/删除的时候,需要特殊考虑的情况是head的设置 62 63 */ 64 /* 65 末节点插入 66 */ 67 public void addAtTail(Object obj){ 68 Node node=new Node(obj,null); 69 tail.setNext(node);// Node tail=new Node();必须使用这种形式给tail赋初值,虽然tail中各内容是null,不过tai已经是对象 70 //可以调用setNext()方法。如果是以Node tail=null,就会抛出空指针异常“.NullPointerException”,因为tail并不是对象, 71 // 不能调用方法 72 tail=node; 73 if(size++==0){ 74 head=node; 75 } 76 } 77 /* 78 末节点删除 79 */ 80 public void deleteAtTail(){ 81 Node tempNode=head; 82 Node preNode=new Node(); 83 if(size>0){ 84 while (!tempNode.equals(tail)){ 85 preNode=tempNode; 86 tempNode=tempNode.getNext(); 87 } 88 preNode.setNext(null); 89 tail=preNode; 90 if(size--==1){ 91 head=new Node(); 92 } 93 } 94 else { 95 System.out.println("链表为空,不能删除节点"); 96 } 97 } 98 //测试代码 99 public static void main(String[] args){ 100 list ls=new list(); 101 ls.addAtTail("D1"); 102 ls.addAtHead("A"); 103 ls.addAtHead("B"); 104 ls.addAtHead("C"); 105 ls.deleteAtHead(); 106 ls.deleteAtHead(); 107 ls.deleteAtHead(); 108 ls.deleteAtHead(); 109 ls.deleteAtHead(); 110 ls.addAtTail("D1"); 111 ls.addAtTail("D2"); 112 ls.addAtTail("D3"); 113 ls.addAtTail("D4"); 114 ls.deleteAtTail(); 115 ls.deleteAtTail(); 116 System.out.println(); 117 } 118 } 119 class Node { 120 private Object ele; 121 private Node next; 122 public Node(){ 123 this(null,null); 124 } 125 public Node(Object ele,Node next){ 126 this.ele=ele; 127 this.next=next; 128 } 129 130 public Object getEle(){ 131 return ele; 132 } 133 public Object setEle(Object ele){ 134 Object oldEle=this.ele; 135 this.ele=ele; 136 return oldEle; 137 } 138 139 public Node getNext(){ 140 return next; 141 } 142 143 public void setNext(Node newNext){ 144 next=newNext; 145 } 146 }
2)链表反转:如果只是需要逆序输出单链表的节点而不改变链表的结构,可以顺序遍历链表,将遍历内容存入栈,最后出栈。如果要求反转链表,则可以使用以下方法,反转链表就是从首节点开始,依次使其指向前驱节点,最终结果就是使头节点指向原链表尾节点。算法需要定义三个节点,分别是前驱节点、当前节点和后继结点,对应代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /* 2 反转链表,其中head就是链表的首节点(链表第一个元素),而非头节点 3 */ 4 5 public static Node reverse(Node head){ 6 if(head==null){ 7 return head; 8 } 9 Node pre=null;//当前节点的前驱节点,首节点没有前驱节点,所以是null 10 Node cur=head; 11 Node next=null; 12 while (cur!=null){ 13 next=cur.getNext(); 14 cur.setNext(pre); 15 pre=cur; 16 cur=next; 17 } 18 head=pre; 19 return head; 20 }
基于链表的栈
3)有了上面单链表的实现,我们就可以实现基于单链表的栈,基于数组的栈存在的一个问题就是长度固定,存在溢出的问题。利用数组还有一个top指针定义了了基于数组的栈,同样的,基于链表的栈,使用定义的Node类和头指针定义一个基于单链表的栈。并且,基于头结点的插入和删除操作,完全符合栈的特性,并且其时间复杂度均是O(1),达到了数组栈的速度,栈的规模又可以动态增加,简直完美。
同样的,入栈其实就是执行单链表的插入操作,也是需要根据传入的参数创建对象Node,然后通过修改引用的指向将这些Node对象串在一条链中,就构成了栈,同时栈大小加1。出栈时只需要修改引用,同时栈大小减1。
基于单链表的栈实现如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /** 2 * Created by hfz on 2016/8/2. 3 */ 4 public class Stack_list { 5 private Node head=null;//指向链表首节点引用,虽然head中各元素为null,不过head不为null,是一个对象了 6 private int size=0; 7 public void push(Object obj){ 8 Node node=new Node(obj,head); 9 head=node;//链表的head指向新插入的节点 10 size++; 11 System.out.println(String.format("%s入栈",(String)obj)); 12 13 } 14 public Object pop(){ 15 if(size>0) 16 { 17 Object obj = head.getEle(); 18 head = head.getNext(); 19 size--; 20 System.out.println(String.format("%s出栈",(String)obj)); 21 return obj; 22 } 23 else{ 24 System.out.println("栈为空,不能出栈"); 25 return null; 26 } 27 } 28 public Object top(){ 29 if(size>0) 30 { 31 return head; 32 33 } 34 else{ 35 System.out.println("栈为空,没有栈顶元素"); 36 return null; 37 } 38 39 } 40 public int getSize(){ 41 return size; 42 } 43 //test 44 public static void main(String[] args){ 45 Stack_list stack=new Stack_list(); 46 stack.push("A1"); 47 stack.push("A2"); 48 stack.push("A3"); 49 stack.push("A4"); 50 stack.push("A5"); 51 stack.pop(); 52 stack.pop(); 53 stack.pop(); 54 stack.pop(); 55 stack.pop(); 56 stack.pop(); 57 stack.pop(); 58 } 59 }
基于链表的队列
4)使用单链表的前插法实现了栈,我们还可以利用单链表的后插法实现队列。定义指向队首front指针和指向队尾的rear指针,入队就对应着后插法中的插入,不过出队时与后插法的删除并不相同,出队时,删除front指向的元素,时间复杂度为O(1)。而后插法的删除操作是删除rear指向的元素,时间复杂度为O(n)。也就是说,基于上述方法实现的队列,出队、入队时间复杂度均是O(1)。
同样的,入队其实就是执行单链表的插入操作,也是需要根据传入的参数创建对象Node,然后通过修改引用的指向将这些Node对象串在一条链中,就构成了队列,同时队大小加1。出队只需要修改引用指向,同时队大小减1。
对应的代码如下:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
1 /** 2 * Created by hfz on 2016/8/2. 3 */ 4 public class Queue_list { 5 private Node front=null; 6 private Node rear=new Node(); 7 private int size=0; 8 public void enqueue(Object obj){ 9 Node node=new Node(obj,null); 10 rear.setNext(node); 11 rear=node; 12 if(size++==0){//后插法考虑链表为空,对head做特殊处理 13 front=node; 14 } 15 System.out.println(String.format("入队元素是:%s",(String)obj)); 16 } 17 public Object dequeue(){ 18 if(size>0){ 19 Object obj=front.getEle(); 20 front=front.getNext(); 21 if(size--==1){ 22 rear=new Node();//删除时,考虑只有一个元素时的特殊情况 23 } 24 System.out.println(String.format("出队元素是:%s",(String)obj)); 25 return obj; 26 } 27 else { 28 System.out.println("队列为空,无元素可出队!"); 29 return null; 30 } 31 } 32 public Object front(){ 33 if(size>0){ 34 Object obj=front.getEle(); 35 System.out.println(String.format("队元首素是:%s",(String)obj)); 36 return obj; 37 } 38 else { 39 System.out.println("队列为空,无队首元素!"); 40 return null; 41 } 42 } 43 public int getSize(){ 44 return size; 45 } 46 public static void main(String[] args){ 47 Queue_list queue_list=new Queue_list(); 48 queue_list.enqueue("A1"); 49 queue_list.enqueue("A2"); 50 queue_list.enqueue("A3"); 51 queue_list.enqueue("A4"); 52 queue_list.enqueue("A5"); 53 queue_list.enqueue("A6"); 54 queue_list.enqueue("A7"); 55 queue_list.dequeue(); 56 queue_list.dequeue(); 57 queue_list.dequeue(); 58 queue_list.dequeue(); 59 queue_list.dequeue(); 60 queue_list.dequeue(); 61 queue_list.dequeue(); 62 queue_list.front(); 63 } 64 }
最后,做个总结,单链表前插法/后插法的插入操作需要考虑链表为空的特殊情况,对其做特殊处理。删除操作当链表有对尾节点的引用时,需要考虑元素数量为1的情况,对其做特殊处理。基于单链表实现的栈只有对首节点的引用top,所以只需要处理入栈时,栈为空时的特殊情况,不用处理出栈时栈中只有一个元素的情形。而基于链表实现的队列有对首节点的引用front和队尾节点的rear引用,所以入队时要对队空做特殊处理,出对时要对队列只有1个元素的情况做特殊处理。