链表 Linked List
什么是链表?
真正的动态数据结构
最简单的动态数据结构
更深入的理解引用(或者指针)
更深入理解递归
辅助组成其他数据结构
!
1572438319545.png
最后一个节点一定是Null
链表的优缺点:
优点:真正的动态,不需要处理固定容量的问题
缺点:丧失了随机访问的能力
对于数组来说,数组在内存中开辟的空间是连续的,可以直接寻找索引的偏移,找到元素的地址,该操作时间复杂度是O(1)。
对于链表来说,链表依赖next来连接各个节点,节点的位置在内存中是随机的,必须依赖next来寻找各个元素。
随机访问:
随机访问是说你可以随意访问该数据结构中的任意一个节点,假设该数据结构有10个节点,你可以随意访问第1个到第10个节点。
对于链表而言,如果其存在10个节点,如果你要访问第5个节点,你只能从列表的头或者尾,依次遍历相邻的每一个节点;对于vector而言,你可以直接利用[]操作符,直接访问[4],不需要遍历其他的节点,这就是随机访问。
**比如first是第一个元素的地址,现在想访问第N个元素。 随机访问:直接first+N,便可以得到第N个元素的地址,因为这些相邻元素是按顺序连续存储的。 **
比如普通数组就是可随机访问的。而链表不支持随机访问,链表存储的元素,它们的存储地址也不是连续的,是随机的。
要想访问第N个元素,只能从second = first->next遍历第2个元素,然后再three = first->next遍历第3个元素… 这样一直到第N个元素。所以这样的访问速度就没有随机访问快。
1572439095562.png
链表类中嵌套节点类
public class LinkedList {
/**
* 组成链表内部的节点类
* 使用private,屏蔽底层实现细节,用户无需知道
*/
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();
}
}
public Node head;//链表头结点
private int size;//链表实际容量
public LinkedList(){
head = null;
size = 0;
}
//获取链表中的元素个数
public int getSize(){return size;}
//判断是否为空
public boolean isEmpty(){ return size == 0;}
}
在链表中添加元素
在链表头添加元素
创建一个新的节点node,并且存储要添加的元素
将node指向head
将node变成head
//在链表头添加元素
public void addFirst(E e){
/* Node node = new Node(e);
node.next = head;
head = node;*/
//以上三行代码的优化写法
//1、新创建的Node中元素为e,指向节点是head
//2、将新创建的Node赋值给head,head指向头结点
head = new Node(e,head);
size ++;
}
在链表头添加节点.gif
根据索引在链表中添加元素
仅供练习使用,链表一般不会有索引
创建要插入的元素node
创建插入位置的前一个节点prev
通过index找到prev
将node指向的下一个元素改为prev指向的下一个元素
将prev指向的下一个元素指向node
注意:==第4 第5步顺序不能换==
//在链表的(0-based)位置添加新的元素e
//在链表中不是一个常规的操作,仅练习用
public void add(E e,int index){
if(index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Illegal index.");
if(index == 0)
addFirst(e);
else{
Node prev = head;
for (int i = 0; i < index-1; i++) {
prev = prev.next;
}
/*Node node = new Node(e);
node.next = prev.next;
prev.next = node;*/
//创建一个新节点,节点中元素为e,指向的下一个元素是prev.next(prev指向的下一个节点)
//将prev.next指向新创建的节点,该节点成功插入prev和prev.next之间
prev.next = new Node(e,prev.next);
size++;
}
}
根据索引在链表中添加元素.gif
在链表末尾添加元素(直接使用add(E e,size))
为链表设立虚拟头节点
在添加元素中遇到的问题:
在链表任意位置添加元素,在链表索引位置添加与链表头处添加,逻辑不同
产生原因:链表头节点之前无节点
解决办法:为表设立虚拟头节点(dummyHead)
虚拟头结点为空,无意义,只是为了编写逻辑方便
==链表的第一个节点是dummyHead.next==
类似循环队列中有意浪费一个节点
![1573011628788.png](https://upload-images.jianshu.io/upload_images/19955166-2df7d3f297c67338.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
public Node dummyHead;
private int size;
public LinkedList(){
dummyHead = new Node(null,null);
size = 0;
}
//在链表的(0-based)位置添加新的元素e
//在链表中不是一个常规的操作,仅练习用
public void add(E e,int index){
if(index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Illegal index.");
Node prev = dummyHead;
//利用dummyHead是在链表第一个有效节点前添加了一个元素
//所以for的界限要+1,prev需多走一步
for (int i = 0; i < index; i++) {
prev = prev.next;
}
/*Node node = new Node(e);
node.next = prev.next;
prev.next = node;*/
//创建一个新节点,节点中元素为e,指向的下一个元素是prev.next(prev指向的下一个节点)
//将prev.next指向新创建的节点,该节点成功插入prev和prev.next之间
prev.next = new Node(e,prev.next);
size++;
}
//在链表头添加元素
public void addFirst(E e){
add(e,0);
}
//在链表末尾添加元素
public void addLast(E e){
add(e,size);
}
链表的遍历,查询和修改
查找的是当前元素,所以命名为cur
//在链表的(0-based)位置获取新的元素e
//在链表中不是一个常规的操作,仅练习用
public E get(int index){
//Node prev = dummyHead;
//直接获取第index位的元素,不是index位置的前一个元素
Node cur = dummyHead.next;
for (int i = 0; i < index; i++)
cur = cur.next;
return cur.e;
}
//获取链表第一个元素
public E getFirst(){
return get(0);
}
//获取链表最后一个元素
public E getLast(){
return get(size-1);
}
//修改链表的(0-based)位置的元素e
//在链表中不是一个常规的操作,仅练习用
public void set(int index, E e){
Node cur = dummyHead.next;
for (int i = 0; i < index; i++)
cur = cur.next;
cur.e=e;
}
//查找链表中是否有元素e
public boolean constains(E e){
Node cur = dummyHead.next;
while(cur != null){
if(cur.e.equals(e))
return true;
cur = cur.next;
}
return false;
}
链表元素的删除
删除索引元素.gif
常见错误
1573011628788.png
实际只是将cur指向了下一个元素,需要发生变化的是要删除节点的前一个节点
//删除链表的(0-based)位置的元素e
//在链表中不是一个常规的操作,仅练习用
public E remove(int index){
Node prev = dummyHead;
for (int i = 0; i < index ; i++) {
prev = prev.next;
}
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size --;
return delNode.e;
}
public E removeFirst(){
return remove(0);
}
public E removeLast(){
return remove(size-1);
}
链表的时间复杂度分析
添加操作 O(n)
方法名
时间复杂度
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)
总结
1573021820979.png
使用链表实现栈
public class LinkedListStack implements Stack {
private LinkedList list;
public LinkedListStack(){list = new LinkedList<>();}
@Override
public int getSize() { return list.getSize();}
@Override
public boolean isEmpty() {return list.isEmpty(); }
//入栈
@Override
public void push(E e) {list.addFirst(e);}
//出栈
@Override
public E pop() {return list.removeFirst();}
@Override
public E peek() { return list.getFirst();}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append("Stack: top ");
res.append(list);
return res.toString();
}
}
使用数组实现栈和链表实现栈的比较
public class MainTest {
//测试使用stack运行opCount个push和pop操作所需要的时间,单位:秒
private static double testStack(Stack stack, int opCount) {
long startTime = System.nanoTime();//获取当前时间
Random random = new Random();
for (int i = 0; i < opCount; i++)
stack.push(random.nextInt(Integer.MAX_VALUE));
for (int j = 0; j < opCount; j++)
stack.pop();
long endTime = System.nanoTime();//获取完成操作后的当前时间
return (endTime - startTime) / 1000000000.0;
}
public static void main(String[] args){
int opCount = 1000000;
//使用数组完成栈的操作,消耗时间点:扩容
ArrayStack arrayStack = new ArrayStack<>();
double time1 = testStack(arrayStack,opCount);
System.out.println("ArrayStack, time: " + time1+"s");
//使用链表完成栈的操作,消耗时间点:new操作
LinkedListStack LinkedListStack = new LinkedListStack<>();
double time2 = testStack(LinkedListStack,opCount);
System.out.println("LinkedListStack, time: " + time2+"s");
/*结果:
opCpount:100000
ArrayStack, time: 0.0779552s
LinkedListStack, time: 0.045245s
opCpount:1000000
ArrayStack, time: 0.2513879s
LinkedListStack, time: 0.8988562s*/
}
}
使用链表实现队列
1573031846652.png
由于没有dummyHead,要注意链表为空的情况
public class LinkedLIstQueue implements Queue {
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);}
public String toString(){ return e.toString();}
}
private Node head,tail;
private int size;
public LinkedLIstQueue(){
head = null;
tail = null;
size = 0;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public void enqueue(E e) {
if(tail==null){
tail=new Node(e);
head = tail;
}else{
tail.next=new Node(e);
tail=tail.next;
}
size ++;
}
@Override
public E dequeue() {
if(isEmpty())
throw new IllegalArgumentException("链表为空");
Node retNode = head;
head = head.next;
retNode.next = null;
if(head == null)
tail = null;
size --;
return retNode.e;
}
@Override
public E getFornt() {
if(isEmpty())
throw new IllegalArgumentException("链表为空");
return head.e;
}
@Override
public String toString(){
StringBuilder sb = new StringBuilder();
sb.append("Queue: front ");
for (Node cur = head; cur != null; cur=cur.next)
sb.append(cur + "->");
sb.append("NULL tail");
return sb.toString();
}
}
对100000个元素进行操作,数组队列,循环队列和链表完成的队列的对比
1573046387080.png