3.链表 Linked List
《玩转数据结构》-liuyubobobo 课程笔记
链表 Linked List
之前我们介绍了三种数据结构
- 动态数组
- 栈
- 队列
其底层都是依托静态数组,靠resize解决固定容量问题
而链表则是一种真正的动态数据结构,是一种最简单的动态数据结构,能够辅助组成其他数据结构
学习链表,能够更深入的理解引用(或者指针),更加深入的理解递归
节点
链表将数据存储在一种单独的结构中,叫做节点(Node)
Class Node{
/**
* 存储的数据
*/
E e;
/**
* 指向下一个节点
*/
Node next;
}
链表就像一个火车,每一个节点就像一节车厢,在车厢中存储数据,并且车厢和车厢直接,要进行连接,使得这些数据是整合在一起的,方便用户在这些数据中进行操作。数据和数据之间的连接,就是使用next
来完成的
最后一个节点中,next
存储的是一个null
优点:真正的动态,不需要处理固定容量的问题
缺点:丧失了随机访问的能力。也就是说不能像数组一样,通过索引来访问特定的元素 。因为在底层中,数组是直接开辟的一个连续的存储空间,通过索引计算偏移就可以找到元素存储的地址。但是链表不同,链表是通过Node
对象进行存储的,在底层中每个Node
对象存储的位置是不同的,需要通过next
一点一点地去找到我们想找的元素。
所以链表不适合用于索引有语义的情况。
数组最好用于索引有语意的情况。
节点实现
我们先来实现节点类
/**
* 链表
* @author 肖晟鹏
* @email 727901974@qq.com
* @date 2021/3/29
*/
public class LinkedList<E> {
/**
* 节点
* 用户不需要知道节点类,用户只需要使用链表类就行了,所以节点作为内部类
* 我们需要对用户屏蔽数据结构中的实现细节
*/
private class Node{
public E e;
public Node next;
public Node(E e,Node next){
this.e = e;
this.next = next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString() {
return e.toString();
}
}
}
在链表中添加元素
上文中,我们知道链表是通过节点存储元素,然后节点和节点之间连接起来这种数据结构。
对于链表来说,如果我们想要访问链表中的所有元素,相应的,我们必须把链表的头给存储起来,通常来说,链表的头叫做head
也就是说,在LinkedList
中,应该有一个叫做head
变量,指向链表的第一个节点,我们首先把LinkedList
中基本的成员变量给声明出来
/**
* 链表
* @author 肖晟鹏
* @email 727901974@qq.com
* @date 2021/3/29
*/
public class LinkedList<E> {
private class Node{
...
}
/**
* 指向链表中的第一个节点
*/
private Node head;
/**
* 元素个数
*/
private int size;
public LinkedList(){
this.head = null;
this.size = 0;
}
/**
* 获取链表中的元素个数
* @return 链表中的元素个数
*/
public int getSize() {
return size;
}
/**
* 判断链表是否为空
* @return true/false
*/
public boolean isEmpty(){
return this.size == 0;
}
}
接下来我们看看这一节的重头戏:添加元素
在链表头增加元素
现在有一个元素666
需要增加到一个链表中,也就是说需要将节点666
挂接到元素中,那么就需要将其next
属性指向链表的head
节点
此时节点666
就成了新的链表头,然后我们维护head
属性,将head
指向新的666
节点
/**
* 在链表头添加元素
* @param e 元素
*/
public void addFirst(E e){
/*
注意这里的逻辑
首先将新节点的next指向旧的链表头,此时node应该就为新的链表头
node.next = this.head;
这个时候维护head属性,将其指向新的链表头node
this.head = node;
*/
//上面的逻辑等于下面的一行代码
head = new Node(e,head);
this.size ++;
}
在链表的中间添加元素
现在我们要在链表索引为2的地方添加元素666
,注意,链表中是没有索引这个概念的,我这里只是类比,便于理解
思考:我们需要将节点666
插入到节点1
后面,然后节点666
后面再连接节点2
,也就是说,节点1
为prev
节点,即节点666
之前的节点。
怎么做呢?
第一步,我们需要将新节点666
的next
属性指向节点2
,即node.next = prev.next
第二步,再将prev
节点的next
属性,指向新节点666
,即prev.next = node
注意,以上两步不能改变顺序,这个顺序很重要,如果顺序颠倒了,得到的结果会是错误的,即新节点的next
指向自己,可以体会一下。
这个过程的关键,就是需要找到添加节点的前一个节点,即prev
节点。
找prev
节点的方式也很简单,从head
节点开始,不断向后面移动,直到index - 1 的位置,这样就找到了待插入节点前面一个节点的位置。
Node prev = head;
for(int i = 0; i < index - 1 ; i++){
prev = prev.next;
}
我们如果想要插入元素到链表的头部,而链表的头部是没有前一个节点的,所以需要特殊考虑
我们如果想要插入元素到链表的尾部,这就很简单了,直接在链表的size
位置增加一个节点即可。
/**
* 在链表的index位置添加新的元素e
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
*/
public void add(int index,E e){
//检查index合法性
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed.Require index >= 0 and index <= size.");
}
//特殊处理添加链表头
if(index == 0){
addFirst(e);
}
else {
//先将prev找到
Node prev = head;
for(int i = 0; i < index - 1 ; i++){
prev = prev.next;
}
/*插入新节点
Node node = new Node(e);
第一步,我们需要将新节点的next属性指向前节点原先的后面一个节点
node.next = prev.next;
第二步,再将前节点的next属性,指向新节点
prev.next = node;
注意,以上两步顺序不能乱,不然会发生错误。即node节点的next指向自己*/
//上面的逻辑等于下面的一行代码
prev.next = new Node(e,prev.next);
this.size ++;
}
}
/**
* 向链表的末尾添加一个新的元素e
* @param e 元素
*/
public void addLast(E e){
add(this.size,e);
}
使用链表的虚拟头节点
我们在实现添加元素的时候,发现在链表的头添加元素和链表的其他位置添加元素,会有逻辑上的差别,原因在于我们在添加元素的时候,会去寻找待添加节点的前一个节点,而链表头没有前一个节点,所以逻辑上会有差别。
所以这里就有一个小技巧,可以把链表头的特殊操作和其他位置的操作统一起来,这种技巧就叫做虚拟头节点。
我们的核心问题在于:链表头没有一个前节点,那么我们直接造一个虚拟前节点不就好了吗?虚拟节点不存储任何元素,只是为了编写的逻辑方便而存储的一个虚拟节点。对用户是屏蔽的,用户是不知道这个虚拟节点的存在的。这里有点像之前实现循环队列时,我们为了编写逻辑的方便,有意识地去浪费的一个空间。
创造一个dummyHead(虚拟头节点)
,其不存储任何的数据,其next
属性指向真正的头节点,即e = null dummyHead.next = head
这个时候,链表中所有的节点(包括head
)都有了一个前节点,可以统一其操作,这个时候,就可以修改一下方法addFirst(E e)
了
/**
* 链表
* @author 肖晟鹏
* @email 727901974@qq.com
* @date 2021/3/29
*/
public class LinkedList<E> {
/**
* 节点
* 用户不需要知道节点类,用户只需要使用链表类就行了,所以节点作为内部类
* 我们需要对用户屏蔽数据结构中的实现细节
*/
private class Node{
...
}
/**
* 虚拟节点,链表中的真正头节点的前一个节点
* 存在意义:便于编写逻辑
* 对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
*/
private Node dummyHead;
...
public LinkedList(){
//初始化的时候,创建虚拟节点,使其指向为空
this.dummyHead = new Node(null,null);
this.size = 0;
}
...
/**
* 在链表的index位置添加新的元素e
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
*/
public void add(int index, E e) {
//检查index合法性
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed.Require index >= 0 and index <= size.");
}
//先将prev找到
//虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node prev = this.dummyHead;
//因为是从虚拟头结点开始遍历的,索引遍历次数会+1,即这里寻找prev节点的遍历次数应该是index次,而不是index - 1次
for (int i = 0; i < index; i++) {
prev = prev.next;
}
/*
插入新节点
Node node = new Node(e);
第一步,我们需要将新节点的next属性指向前节点原先的后面一个节点
node.next = prev.next;
第二步,再将前节点的next属性,指向新节点
prev.next = node;
注意,以上两步顺序不能乱,不然会发生错误。即node节点的next指向自己
上面的逻辑等于下面的一行代码
*/
prev.next = new Node(e, prev.next);
this.size++;
}
/**
* 向链表的头添加一个新的元素e
* @param e 元素
*/
public void addFirst(E e){
add(0,e);
}
...
}
链表的遍历,查询和修改
遍历和查询
遍历与寻找前节点不同,如果我们需要寻找index节点,那么就需要遍历index次,而不是(index-1)次
/**
* 遍历,获取在链表的第index位置的节点
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
* @return 元素
*/
public Node traverse(int index){
//合法性判断
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Index is illegal.Require index >= 0 and index < size.");
}
//从真正的头节点开始遍历,虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node cur = this.dummyHead.next;
//与寻找前节点不同,我们就是需要寻找index节点,所以遍历index次
for(int i = 0 ; i < index ; i++){
cur = cur.next;
}
return cur;
}
/**
* 获取在链表的第index位置的元素
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
* @return 第index位置的元素
*/
public E get(int index){
return traverse(index).e;
}
/**
* 获得第一个元素
* @return 第一个元素
*/
public E getFirst(){
return get(0);
}
/**
* 获得最后一个元素
* @return 最后一个元素
*/
public E getLast(){
return get(this.size -1);
}
/**
* 判断链表中是否有元素e
* @param e 元素e
* @return true/false
*/
public boolean contains(E e){
//从真正的头节点开始遍历,虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node cur = this.dummyHead.next;
//另一种遍历方式,cur 只要不为空,则视为有效节点
while (cur != null){
if(cur.e.equals(e)){
//元素相等,返回true
return true;
}
else {
//元素不相等,向后移动,继续遍历
cur = cur.next;
}
}
//最后所有的节点都遍历了,还是没有相等的元素,则返回false
return false;
}
修改
我们既然能遍历和查询元素了,那么修改元素也响应地非常简单了。
遍历查询到我们需要修改的元素的节点,然后set值就可以了
/**
* 修改在链表的第index位置的元素e
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
* @param e 元素
*/
public void set(int index,E e){
//遍历到index位置的节点
Node cur = traverse(index);
//修改元素
cur.e = e;
}
链表元素的删除
删除元素就很简单了,比如我们想删除索引为2位置的元素。和增加元素的逻辑一样,先找到待删除元素(delNode节点)之前的那个元素,然后prev.next =delNode.next
即可
这样delNode
节点就等同于从链表中删除了,为了方便java去回收空间,我们还需要将delNode
节点和链表整个脱离开来,即delNode.next = null
/**
* 删除在链表的第index位置的元素e
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
*/
public E remove(int index){
//合法性判断
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed.Require index >= 0 and index < size.");
}
//先将prev找到
//虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node prev = this.dummyHead;
//因为是从虚拟头结点开始遍历的,索引遍历次数会+1,即这里寻找prev节点的遍历次数应该是index次,而不是index - 1次
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
this.size --;
return delNode.e;
}
/**
* 从链表中删除元素e
* @param e 元素e
*/
public void removeElement(E e){
//先将prev找到
//虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node prev = this.dummyHead;
while (prev.next != null){
if(prev.next.e.equals(e)){
break;
}
prev = prev.next;
}
if(prev.next != null){
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
this.size --;
}
}
/**
* 删除第一个元素
* @return
*/
public E removeFirst(){
return remove(0);
}
/**
* 删除最后一个元素
* @return
*/
public E removeLast(){
return remove(this.size -1);
}
链表整体实现
/**
* 链表
* @author 肖晟鹏
* @email 727901974@qq.com
* @date 2021/3/29
*/
public class LinkedList<E> {
/**
* 节点
* 用户不需要知道节点类,用户只需要使用链表类就行了,所以节点作为内部类
* 我们需要对用户屏蔽数据结构中的实现细节
*/
private class Node{
public E e;
public Node next;
public Node(E e,Node next){
this.e = e;
this.next = next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString() {
return e.toString();
}
}
/**
* 虚拟节点,链表中的真正头节点的前一个节点
* 存在意义:便于编写逻辑
* 对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
*/
private Node dummyHead;
/**
* 元素个数
*/
private int size;
public LinkedList(){
//初始化的时候,创建虚拟节点,使其指向为空
this.dummyHead = new Node(null,null);
this.size = 0;
}
/**
* 获取链表中的元素个数
* @return 链表中的元素个数
*/
public int getSize() {
return size;
}
/**
* 判断链表是否为空
* @return true/false
*/
public boolean isEmpty(){
return this.size == 0;
}
/**
* 在链表的index位置添加新的元素e
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
*/
public void add(int index, E e) {
//检查index合法性
if (index < 0 || index > size) {
throw new IllegalArgumentException("Add failed.Require index >= 0 and index <= size.");
}
//先将prev找到
//虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node prev = this.dummyHead;
//因为是从虚拟头结点开始遍历的,索引遍历次数会+1,即这里寻找prev节点的遍历次数应该是index次,而不是index - 1次
for (int i = 0; i < index; i++) {
prev = prev.next;
}
/*
插入新节点
Node node = new Node(e);
第一步,我们需要将新节点的next属性指向前节点原先的后面一个节点
node.next = prev.next;
第二步,再将前节点的next属性,指向新节点
prev.next = node;
注意,以上两步顺序不能乱,不然会发生错误。即node节点的next指向自己
上面的逻辑等于下面的一行代码
*/
prev.next = new Node(e, prev.next);
this.size++;
}
/**
* 向链表的末尾添加一个新的元素e
* @param e 元素
*/
public void addLast(E e){
add(this.size,e);
}
/**
* 向链表的头添加一个新的元素e
* @param e 元素
*/
public void addFirst(E e){
add(0,e);
}
/**
* index要大于0并且小于size
* @param index 索引
*/
/*private void checkIndex(int index){
}*/
/**
* 遍历,获取在链表的第index位置的节点
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
* @return 元素
*/
public Node traverse(int index){
//合法性判断
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Index is illegal.Require index >= 0 and index < size.");
}
//从真正的头节点开始遍历,虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node cur = this.dummyHead.next;
//与寻找前节点不同,我们就是需要寻找index节点,所以遍历index次
for(int i = 0 ; i < index ; i++){
cur = cur.next;
}
return cur;
}
/**
* 获取在链表的第index位置的元素
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
* @return 第index位置的元素
*/
public E get(int index){
return traverse(index).e;
}
/**
* 获得第一个元素
* @return 第一个元素
*/
public E getFirst(){
return get(0);
}
/**
* 获得最后一个元素
* @return 最后一个元素
*/
public E getLast(){
return get(this.size -1);
}
/**
* 判断链表中是否有元素e
* @param e 元素e
* @return true/false
*/
public boolean contains(E e){
//从真正的头节点开始遍历,虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node cur = this.dummyHead.next;
//另一种遍历方式,cur 只要不为空,则视为有效节点
while (cur != null){
if(cur.e.equals(e)){
//元素相等,返回true
return true;
}
else {
//元素不相等,向后移动,继续遍历
cur = cur.next;
}
}
//最后所有的节点都遍历了,还是没有相等的元素,则返回false
return false;
}
/**
* 修改在链表的第index位置的元素e
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
* @param e 元素
*/
public void set(int index,E e){
//遍历到index位置的节点
Node cur = traverse(index);
//修改元素
cur.e = e;
}
/**
* 删除在链表的第index位置的元素e
* 注意,这不是一个常用的操作,只作为练习用
* @param index 从0开始计数
*/
public E remove(int index){
//合法性判断
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Remove failed.Require index >= 0 and index < size.");
}
//先将prev找到
//虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node prev = this.dummyHead;
//因为是从虚拟头结点开始遍历的,索引遍历次数会+1,即这里寻找prev节点的遍历次数应该是index次,而不是index - 1次
for (int i = 0; i < index; i++) {
prev = prev.next;
}
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
this.size --;
return delNode.e;
}
/**
* 从链表中删除元素e
* @param e 元素e
*/
public void removeElement(E e){
//先将prev找到
//虚拟头节点对用户是屏蔽的,用户是不知道这个虚拟节点的存在的
Node prev = this.dummyHead;
while (prev.next != null){
if(prev.next.e.equals(e)){
break;
}
prev = prev.next;
}
if(prev.next != null){
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
this.size --;
}
}
/**
* 删除第一个元素
* @return
*/
public E removeFirst(){
return remove(0);
}
/**
* 删除最后一个元素
* @return
*/
public E removeLast(){
return remove(this.size -1);
}
@Override
public String toString() {
StringBuilder res=new StringBuilder();
Node cur = this.dummyHead.next;
while (cur != null){
res.append(cur + "->");
cur = cur.next;
}
res.append(cur);
return res.toString();
}
}
测试
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
System.out.println("===================================================");
System.out.println("Add Head");
for (int i = 0; i < 5; i++) {
linkedList.addFirst(i);
System.out.println(linkedList);
}
//在索引为2的位置,增加元素666
System.out.println("===================================================");
System.out.println("Add Node 666 to LinkedList where index is 2.");
linkedList.add(2,666);
System.out.println(linkedList);
System.out.println("LinkedList contains node '666' ? " + linkedList.contains(666));
System.out.println("LinkedList contains node '5' ? " + linkedList.contains(5));
System.out.println("Index 2 is :" + linkedList.get(2));
//删除索引为2的元素,即删除666
System.out.println("===================================================");
System.out.println("Remove index 2 Node.");
linkedList.remove(2);
System.out.println(linkedList);
//删除头元素
System.out.println("===================================================");
System.out.println("Remove head");
linkedList.removeFirst();
System.out.println(linkedList);
System.out.println("Head is dummyHead.next :" + linkedList.getFirst());
System.out.println("Tail is :" + linkedList.getLast());
System.out.println("===================================================");
//删除尾元素
System.out.println("Remove tail");
linkedList.removeLast();
System.out.println(linkedList);
System.out.println("Head is dummyHead.next :" + linkedList.getFirst());
System.out.println("Tail is :" + linkedList.getLast());
System.out.println("===================================================");
System.out.println("Remove tail");
for (int i = 0; i < 3; i++) {
linkedList.removeLast();
System.out.println(linkedList);
}
System.out.println("===================================================");
System.out.println("LinkedList is empty? " + linkedList.isEmpty());
System.out.println("===================================================");
}
>>
===================================================
Add Head
0->null
1->0->null
2->1->0->null
3->2->1->0->null
4->3->2->1->0->null
===================================================
Add Node 666 to LinkedList where index is 2.
4->3->666->2->1->0->null
LinkedList contains node '666' ? true
LinkedList contains node '5' ? false
Index 2 is :666
===================================================
Remove index 2 Node.
4->3->2->1->0->null
===================================================
Remove head
3->2->1->0->null
Head is dummyHead.next :3
Tail is :0
===================================================
Remove tail
3->2->1->null
Head is dummyHead.next :3
Tail is :1
===================================================
Remove tail
3->2->null
3->null
null
===================================================
LinkedList is empty? true
===================================================
时间复杂度分析
添加操作
- addLast(e) O(n)
- addFirst(e) O(1)
- add(index,e) O(n/2)=O(n)
综上,增加操作的时间复杂度为O(n)
删除操作
- removeLast(e) O(n)
- removeFirst(e) O(1)
- remove(index,e) O(n/2)=O(n)
综上,删除操作的时间复杂度为O(n)
修改操作
- set(index,e) O(n)
综上,修改操作的时间复杂度为O(n)
查找操作
- get(index) O(n)
- contains(e) O(n)
综上,查找操作的时间复杂度为O(n)
总结
- 增: O(n)
- 删: O(n)
- 改: O(n)
- 查: O(n)
链表的整体的时间复杂度要比数组要差一些,这也是数组的优势,可以通过索引去访问元素
对于链表来说,如果只会对链表头进行操作,辣么它的时间复杂度其实是O(1)级别的。
这就提示我们了,对于链表来说,最好是对链表头进行操作,并且也不去修改,查也是只查链表头,在这种时候,其时间复杂度就和数组是一样的,并且它是动态的,就不会大量地浪费内存空间,这也就体现了它的优势
使用链表实现栈
/**
* 链表实现栈
* @author 肖晟鹏
* @email 727901974@qq.com
* @date 2021/4/6
*/
public class LinkedListStack<E> implements Stack<E>{
private LinkedList<E> list;
public LinkedListStack(){
list = new LinkedList<>();
}
/**
* 入栈
* 对应增加链表头
* @param e 元素
*/
@Override
public void push(E e) {
list.addFirst(e);
}
/**
* 出栈
* 对应删除栈尾
* @return
*/
@Override
public E pop() {
return list.removeFirst();
}
/**
* 获取栈顶元素
* 对应获取链表头元素
* @return
*/
@Override
public E peek() {
return list.getFirst();
}
@Override
public int getSize() {
return list.getSize();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public String toString() {
StringBuilder res=new StringBuilder();
res.append("Stack:top ");
res.append("[");
res.append(list);
res.append("] ");
return res.toString();
}
}
测试
public static void main(String[] args) {
LinkedListStack<Integer> stack = new LinkedListStack<>();
for (int i = 0; i < 5 ; i++) {
stack.push(i);
System.out.println(stack);
}
stack.pop();
System.out.println(stack);
}
>>
Stack:top [0->null]
Stack:top [1->0->null]
Stack:top [2->1->0->null]
Stack:top [3->2->1->0->null]
Stack:top [4->3->2->1->0->null]
Stack:top [3->2->1->0->null]
比较
比较数组栈和链表栈
/**
* 比较栈
* @author 肖晟鹏
* @email 727901974@qq.com
* @date 2021/4/6
*/
public class Main {
/**
* 测试运行入栈和出栈操作所需要的时间
* @param stack 栈
* @param opCount 操作数
* @return 入栈和出栈操作所需要的时间,单位ms
*/
private static double testStack(Stack<Integer> stack,int opCount){
long startTime = System.currentTimeMillis();
for(int i = 0; i < opCount; i++ ){
stack.push(i);
}
for(int i = 0; i < opCount; i++ ){
stack.pop();
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public static void main(String[] args) {
//操作次数100w
int opCount = 1000000;
ArrayStack<Integer> arrayStack = new ArrayStack<>();
LinkedListStack<Integer> linkedListStack = new LinkedListStack<>();
System.out.println("LinkedListStack:" + testStack(linkedListStack,opCount) + "ms");
System.out.println("ArrayStack:" + testStack(arrayStack,opCount) + "ms");
}
}
>>
LinkedListStack:58.0ms
ArrayStack:84.0ms
可以看到,链表栈要快一些。
这也是意料之中的,因为我们的数组栈还需要时不时得进行扩容缩容,而链表栈本身就是动态的,不需要这些操作。
当然这个比较很粗,在不同的电脑,操作系统和JVM上面,可能有不同的结果,因为LinkedListStack
包含了很多new
操作,new
操作会让操作系统(或者JVM)不停地去寻找可以开辟空间的地方
这个比较其实是为了说明,数组栈和链表栈都是同一复杂度的,在不同的情况下,谁比谁快一点,是很正常的,因为它们在时间复杂度上面是没有巨大的差异的(对比循环队列和数组队列,他们有巨大的差异)。
使用链表实现队列
和实现循环队列一样,我们在使用链表实现队列的时候,不能直接使用我们已经实现的链表去实现队列,需要对链表进行改进。
怎么改进呢?
我们在链表头的位置插入和删除一个元素很简单,原因就是因为我们有个链表头的指针。现在我们希望在链表的尾部进行操作,辣么我们就应该添加一个尾指针tail
,指向链表的尾部。
这样我们在tail
端添加一个元素非常容易了
但是从tail
位置删除一个节点还是很麻烦,因为节点之间只有next
指向下一个节点。而我们在执行删除操作时,需要找到待删除节点的前一个节点prev
,也就是说我们任然需要从head
开始遍历,直到找到tail
节点的前一个节点prev
。
也就是说从tail
端删除一个元素并不容易
但是经过优化,这样的链表已经能够满足实现队列需求了。
因为tail
端删除一个元素并不容易,所以,我们规定从head
端删除元素,从tail
端插入元素,即head
端为队首,tail
端为队尾。
因为我们的操作都是直接操作head
端或者tail
端,不牵扯到中间的元素,所以虚拟头节点就不需要了。这个时候需要注意,如果没有dummmyhead
,链表为空时,head
和tail
都指向NULL
/**
* 链表实现队列
* @author 肖晟鹏
* @email 727901974@qq.com
* @date 2021/4/7
*/
public class LinkedListQueue<E> implements Queue<E>{
/**
* 节点
* 用户不需要知道节点类,用户只需要使用链表类就行了,所以节点作为内部类
* 我们需要对用户屏蔽数据结构中的实现细节
*/
private class Node{
public E e;
public Node next;
public Node(E e,Node next){
this.e = e;
this.next = next;
}
public Node(E e){
this(e,null);
}
public Node(){
this(null,null);
}
@Override
public String toString() {
return e.toString();
}
}
/**
* 头结点
*/
private Node head;
/**
* 尾节点
*/
private Node tail;
/**
* 元素个数
*/
private int size;
/**
* 入队
* @param e 元素
*/
@Override
public void enqueue(E e) {
//如果tail节点为空,则说明链表为空,需要维护头结点
if(this.tail == null){
this.tail = new Node(e);
this.head = this.tail;
}else {
//链表不为空,则直接在尾部插入一个节点即可
this.tail.next = new Node(e);
this.tail = this.tail.next;
}
this.size ++;
}
/**
* 出队
* @return 队首元素
*/
@Override
public E dequeue() {
//如果队列为空,则不能出队,抛出异常
if(this.isEmpty()){
throw new IllegalArgumentException("Cannot dequeque from an empty queue");
}
Node delNode = this.head;
this.head = this.head.next;
//判空,如果此时head为空,说明链表空了,需要维护尾节点
if(this.head == null){
this.tail = null;
}
delNode.next = null;
this.size -- ;
return delNode.e;
}
/**
* 获取队首元素
* @return 队首元素
*/
@Override
public E getFront() {
//如果队列为空,抛出异常
if(this.isEmpty()){
throw new IllegalArgumentException("Queue is empty");
}
return this.head.e;
}
@Override
public int getSize() {
return this.size;
}
@Override
public boolean isEmpty() {
return this.size == 0;
}
@Override
public String toString() {
StringBuilder res=new StringBuilder();
res.append("Queue: front ");
Node cur = this.head;
while (cur != null){
res.append(cur + "->");
cur = cur.next;
}
res.append(cur);
res.append(" tail");
return res.toString();
}
}
测试
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
System.out.println("===================================================");
System.out.println("Remove Head");
for (int i = 0; i < 5; i++) {
linkedList.addFirst(i);
System.out.println(linkedList);
}
//在索引为2的位置,增加元素666
System.out.println("===================================================");
System.out.println("Add Node 666 to LinkedList where index is 2.");
linkedList.add(2,666);
System.out.println(linkedList);
System.out.println("LinkedList contains node '666' ? " + linkedList.contains(666));
System.out.println("LinkedList contains node '5' ? " + linkedList.contains(5));
System.out.println("Index 2 is :" + linkedList.get(2));
//删除索引为2的元素,即删除666
System.out.println("===================================================");
System.out.println("Remove index 2 Node.");
linkedList.remove(2);
System.out.println(linkedList);
//删除头元素
System.out.println("===================================================");
System.out.println("Remove head");
linkedList.removeFirst();
System.out.println(linkedList);
System.out.println("Head is dummyHead.next :" + linkedList.getFirst());
System.out.println("Tail is dummyHead.prev :" + linkedList.getLast());
System.out.println("===================================================");
//删除尾元素
System.out.println("Remove tail");
linkedList.removeLast();
System.out.println(linkedList);
System.out.println("Head is dummyHead.next :" + linkedList.getFirst());
System.out.println("Tail is dummyHead.prev :" + linkedList.getLast());
System.out.println("===================================================");
System.out.println("Remove tail");
for (int i = 0; i < 3; i++) {
linkedList.removeLast();
System.out.println(linkedList);
}
System.out.println("===================================================");
System.out.println("LinkedList is empty? " + linkedList.isEmpty());
System.out.println("===================================================");
}
>>
===================================================
Remove Head
0->null
1->0->null
2->1->0->null
3->2->1->0->null
4->3->2->1->0->null
===================================================
Add Node 666 to LinkedList where index is 2.
4->3->666->2->1->0->null
LinkedList contains node '666' ? true
LinkedList contains node '5' ? false
Index 2 is :666
===================================================
Remove index 2 Node.
4->3->2->1->0->null
===================================================
Remove head
3->2->1->0->null
Head is dummyHead.next :3
Tail is dummyHead.prev :0
===================================================
Remove tail
3->2->1->null
Head is dummyHead.next :3
Tail is dummyHead.prev :1
===================================================
Remove tail
3->2->null
3->null
null
===================================================
LinkedList is empty? true
===================================================
比较
我们可以将链表实现的队列与之前我们实现的数组队列和循环队列进行比较
package com.cupricnitrate.datastructure.queue;
/**
* 数组队列、循环队列和链表队列比较
* @author 肖晟鹏
* @email 727901974@qq.com
* @date 2021/3/25
*/
public class Main {
/**
* 测试运行入队和出队操作所需要的时间
* @param q 队列
* @param opCount 操作数
* @return 入队和出队的时间,单位ms
*/
private static long testQueue(Queue<Integer> q,int opCount){
long startTime = System.currentTimeMillis();
for(int i = 0; i < opCount; i++ ){
q.enqueue(i);
}
for(int i = 0; i < opCount; i++ ){
q.dequeue();
}
long endTime = System.currentTimeMillis();
return endTime - startTime;
}
public static void main(String[] args) {
int opCount = 100000;
ArrayQueue<Integer> arrayQueue = new ArrayQueue<>();
LoopQueue<Integer> loopQueue = new LoopQueue<>();
LinkedListQueue<Integer> linkedListQueue = new LinkedListQueue<>();
System.out.println("LinkedListQueue:" + testQueue(linkedListQueue,opCount) + "ms");
System.out.println("LoopQueue:" + testQueue(loopQueue,opCount) + "ms");
System.out.println("ArrayQueue:" + testQueue(arrayQueue,opCount) + "ms");
}
}
>>
LinkedListQueue:14ms
LoopQueue:18ms
ArrayQueue:4511ms
同样的,这个对比是很粗的,LinkedListQueue和LoopQueue谁比谁快一点是不确定的,但是我们能确定的是,LinkedListQueue和LoopQueue的时间复杂度是相同的,为O(n),没有太大的差异。