上期回顾:上次我们学习数组,感觉也还好,并没有多少难度~(哈)本期我们学习链表,首先带着几个小问题来学习。
1、链表和数组有啥子区别
2、链表在内存中存储结构是啥样子的
3、它的增删改查的如何做的
1、链表
1.1概念:
链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
2、链表的基本结构
2.1 单向链表
由图可见:单向链表每个节点都有两个属性,一个本身value,一个是指向下一个节点的指针。
private static class Node {
private Object data;
private Node next;
}
链表的第一个节点被称为头结点,最后一个加点被称为尾节点,尾结点的next指针指向null。
与数组按照下标来随机寻找元素不同,对于链表的其中一个节点A,我们只能根据节点A的next指针来找到该节点的下一个节点B,依次往下寻找。
那么问题来了如何快速找到他的前一个节点你呢?不是下一个节点。
2.2双向链
双向链表比单向链表稍微复杂一些,他的每一个节点除了拥有data和next指针,还拥有指向前置节点的prev指针。
2.3链表环问题
判断是否有环
定义一个快指针和一个慢指针,快指针一次走两步,慢指针一次走两步,会出现两种情况,情况一指针走到了空的位置,那就说明这个链表不带环。情况二两个指针相遇,说明这个链表带环。(后面讲到算法的时候会再学习具体实现的)
2.4链表的基本存储
接下来我们看看链表的存储方式。
如果说数组在内存中的存储方式是顺序存储,那么在链表在内存中存储方式就是随机存储。
啥子叫随机存储呢?
链表是采用“见缝插针”的方式,链表的每一个节点分布在内存的不同位置。依靠next指针关联起来。这样可以灵活有效的利用零碎的碎片空间。让我们看一看下面的两张图,对比一下数组和链表在内存中分配方式的不同。
图中的箭头代表链表节点的next指针。
3、链表的基本操作
3.1查找节点
在查找元素时,链表不像数组那样可以通过下标快速进行定位,只能从头结点开始向后一个一个节点逐一查找。
例如:给出一个链表,需要查找从头开始的第三个节点
1)、将查找的指针定位到头结点
2)、根据头节点的next的指针,定位到第2个节点。
3)、根据第二个 节点next指针,定位到第三个节点,查找完毕
一个小问题,链表节点的时间复杂度是多少呢?
很简单是吧,链表的数据只能按照顺序进行访问,最坏的时间复杂度是O(n)
3.2 更新节点
如果不考虑查找节点的过程,链表的更新速度还是可观的,因为他和数组一样直接把旧数据换成新数据即可。
如:我们要把一个链表的第二个节点数据更新。
3.3 插入节点
与数组类似,链表插入节点时,同样分为三种情况:
- 尾部插入
- 头部插入
- 中间插入
1)、尾部插入,是最简单的情况,把最后欧一个节点的next指针指向新插入的节点即可。
2)、头部插入,可以分为两个步骤
第一步,把新的节点的next指针指向原先的节点。
第二步,把新的节点变为链表的头节点。
3)、中间插入,同样分为两个步骤
第一步,新节点的next指针,指向插入位置的节点。
第二本,插入位置的前置节点的next指针,指向新节点
如果内存允许,链表的长度可以是无限大的。
3.4 删除节点
同样和插入一样分为三种方式
- 尾部删除
- 头部删除
- 中间删除
1)、尾部删除,是最简单的情况,把倒数第二个节点的next指针指向空即可
2)、头部删除也很简单,把链表的头结点设为原先头结点的next指针即可
3)、中间删除,同样很简单,把删除节点的前置节点的next指针,指向要删除元素的下一个节点即可。
这里有个小点:java有自动化回收机制,删除的节点会被回收。
链表的插入和删除不考虑查找元素的过程,只考虑纯粹的插入和删除操作,时间复杂度都是O(1);
完整代码如下:
public class linkTest {
//头节点指针
private Node head;
//尾节点指针
private Node last;
//链表的实际长度
private int size;
/**
* 链表插入元素
* @Pram data 插入元素
* @Pram index 插入位置
*/
public void insert(Object data,int index) throws Exception {
if (index<0||index>size){
throw new IndexOutOfBoundsException("超出链表节点范围!");
}
Node insertNode = new Node(data);
if (size==0){
//空链表
head=insertNode;
last=insertNode;
}else if (index==0){
//插入头部
insertNode.next=head;
head=insertNode;
}else if (size==index){
//插入尾部
last.next=insertNode;
last=insertNode;
}else {
//插入中间
//获取到第index-1的节点对象
Node prevNode = get(index - 1);
//将第index-1的node.next数据赋给insert.next
insertNode.next=prevNode.next;
//再将insertNode的信息赋给prevNode.next
prevNode.next=insertNode;
}
size++;
}
/**
* 删除元素
* @Pram index 删除位置
*/
public Node remove(int index) throws Exception{
if (index<0||index>size){
throw new IndexOutOfBoundsException("超出链表节点范围!");
}
Node removeNode=null;
if (index==0){
//删除头节点
removeNode=head;
head=head.next;
}else if (index==size-1){
//删除尾节点
Node prevNode = get(index - 1);
removeNode=prevNode.next;
prevNode.next=null;
last=prevNode;
}else {
//删除中间节点
//获取到删除节点的前一个节点
Node prevNode = get(index - 1);
//获取到next.next目的将删除节点的前一个节点next指向删除节点的下一个节点
Node nextNode=prevNode.next.next;
//将要删除的节点赋给removeNode
removeNode=prevNode.next;
//删除节点的前一个节点的next指向删除节点的下一个节点
prevNode.next=nextNode;
}
size--;
return removeNode;
}
/**
* 链表查询元素
* @Pram index 查找元素的位置
*/
public Node get(int index)throws Exception{
if (index<0||index>size){
throw new IndexOutOfBoundsException("超出链表节点范围!");
}
Node temp = this.head;
for (int i = 0; i <index ; i++) {
temp=temp.next;
}
return temp;
}
/**
* 链表节点
*/
private static class Node{
Object data;
Node next;
Node(Object data){
this.data=data;
}
}
/**
*输出链表
*/
public void output(){
Node temp=head;
while (temp!=null){
System.out.println(temp.data);
temp=temp.next;
}
}
public static void main(String[] args) throws Exception {
linkTest linkTest = new linkTest();
linkTest.insert("小明",0);
linkTest.insert("小花",1);
linkTest.insert("小红",2);
linkTest.insert("小狗蛋",3);
linkTest.insert("小王八",4);
//执行删除
linkTest.remove(3);
linkTest.insert("小猫咪",3);
linkTest.output();
}
}
以上是对单链表相关操作的代码实现。为了尾部插入的方便,代码中额外增加了指定向链表尾节点的指针last。
到这里链表介绍结束,是不是也很简单,biu~
那么我们总结一下:
1.数据结构没有绝对的好与坏,数组和链表各有千秋。数组和链表的相关操作的性能对比如下图:
从表格可以看出,数组的优势在于快速定位元素,适用于读多写少的场景
反之,链表优势在于灵活的插入与删除。
下期预告:栈和队列的前世今生。。。。