开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情
前言
本文为 数据结构基础【链表】 相关知识介绍,具体将对链表概念
,单链表
,双链表
,循环链表
,Java中链表的使用
等进行详尽介绍~
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~ 👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~
一、链表介绍
上节我们介绍了数组,是一种线性结构,而链表也是一种线性结构,它们都可以称作线性表。不同的是数组在内存空间中是顺序存放的,而链表不需要连续存放。下面就首先来看看链表的定义与相关介绍。
- 在内存空间中,数组和链表都是基本的数据结构,都是【表】,或者叫【线性表】。
- 线性表是一个线性结构,它是一个含有n≥0个结点的有限序列,对于其中的结点,有且仅有一个开始结点没有前驱但有一个后继结点,有且仅有一个终端结点没有后继但有一个前驱结点,其它的结点都有且仅有一个前驱和一个后继结点,说人话,就是有头有尾一条线。
还不明白就再来一张图:
- 链表是一种动态数据结构,他的特点是用一组任意的存储单元(可以是连续的,也可以是不连续的)存放 数据元素。
- 链表中每一个元素成为 “结点” , 每一个结点都是由数据域和指针域组成的, 每个结点中的指针域指向下一 个结点。
- Head 是 “头指针” , 表示链表的开始, 用来指向第一个结点, 而最后一个指针的指针域为 NULL( 空地址 ) ,表示链表的结束。
- 可以看出链表结构必须利用指针才能实现,即一个结点中必须包含一个指针变量,用来存放下一个结点的 地址。
- 实际上,链表中的每个结点可以用若干个数据和若干个指针。
由以上介绍我们可以知道,链表它与数组一样是一种线性结构,区别在于它在内存空间中不是顺序存放的,也就是说物理结构不是线性的,而逻辑结构是线性的,要想保持这种逻辑上的线性结构,就需要将它们以某种方式能够连接起来形成线性结构,链表呢就是通过指针来实现的,每一个结点中除了数据元素外 还有一个用于指向 下一个结点的指针,通过这些指针,结点之间进行连接最终成一个链,逻辑上是线性的。
二、单链表
由以上介绍相信已经对于链表是什么已经有了一些认识,那么链表具有一些特殊的特点我们需要了解一下。
单链表特点:
- 长度可变,扩展性好
- 内存利用高(可以不连续)
- 时间性能:查找O(n)、插入和删除O(1)
- 空间性能:不需要分配存储空间,只要有就可以分配,元素个数不受限制
单链表的逻辑结构:
单向链表的节点的抽象:
其节点由两部分构成:
- data域:数据域,用来存储元素数据
- next域:用于指向下一节点
以下是链表的定义代码示例:
class Node<E> {
E item;
Node<E> next;
//构造函数
Node(E element) {
this.item = element;
this.next = null;
}
}
由以上对于链表的特点的介绍我们可以知道,链表具有长度可变,可扩展性好的特点,而数组我们知道它是在内存空间中是顺序存放的,我们在定义数组的时候就已经定义好了数组的大小,相当于在内存中已经给数组分配了固定大小的内存空间,在后续的使用过程中数组的大小是不可以改变的,相比于此,链表的大小可变的特性就方便我们对于线性表的使用。
三、双链表
我们上边介绍的链表 其实是一种单链表,单链表就是说每一个结点只含有一个指针,这个指针指向它的下一个结点,所以在对链表进行遍历查找的时候就只能从头往后进行查找。所以本节我们将分析单链表存在的 缺点,并针对单链表的这些缺点引入双链表的介绍。
单向链表的缺点分析:
- 单向链表,在查找的时候查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 对于单向链表的某一个结点不能自我删除,需要靠辅助节点,而双向链表的某一个结点则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp是待删除节点的前一个节点。
为了弥补单链表的缺点,使得链表能够实现双向查找的和自我删除的功能,引出了双向链表的概念。
双向链表的逻辑结构:
双向链表的节点的抽象:
双链表的节点由三部分构成:
- data域:数据域,用来存储元素数据
- next域:用于指向后一节点
- front域:用于指向前一节点
以下是双链表的定义代码示例:
class Node<E> {
E item;
Node<E> next;
Node<E> prev;
//构造函数
Node(E element) {
this.item = element;
this.next = null;
this.prev = null;
}
}
四、循环链表
还有一种比较特殊的链表结构叫做循环链表:链表的最后一个节点指向第一个节点,整体构成一个链环。
将单链表中的终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list),循环链表不一定需要头结点。
下图是一个循环链表的结构示意图:
由上图我们可以看到循环链表头尾相连在逻辑上形成一个环,这种特殊的数据结构会在一些特殊的地方有一些妙用。
五、Java中链表的使用
在最后一节将为大家介绍在Java中对于链表的一些使用,在Java中的ArrayList和LinkedList底层都是实现了链表。两者区别在于:
- ArrayList和LinkedList两者的区别在于与 ArrayList 相比,LinkedList的增加和删除的操作效率更高,而查找和修改的操作效率较低。
- 链表用LinkedList较多。
1️⃣Java中LinkedList的操作
首先我们来看看Java中对于LinkedList的一些操作与使用。
LinkedList类 是位于java.util 包中的,我们要想使用它,使用前需要引入这个包,引入的语法格式如下:
// 引入 LinkedList 类
import java.util.LinkedList;
LinkedList<E> list = new LinkedList<E>(); // 普通创建方法
// 或者
LinkedList<E> list = new LinkedList(Collection<? extends E> c); // 使用集合创建链表
创建一个简单的链表实例:
import java.util.LinkedList;
public class RunoobTest {
public static void main(String[] args) {
LinkedList<String> sites = new LinkedList<String>();
sites.add("Baidu");
sites.add("CSDN");
sites.add("Juejin");
sites.add("Yahoo");
System.out.println(sites);
}
}
以上链表实例的执行输出结果为:
// 打印结果:
[Baidu, CSDN, Juejin, Yahoo]
以下表格列出了LinkedList的其他操作方法:
方法 | 描述 |
---|---|
addFirst() | 在表头增加一个元素 |
addLast() | 在表尾增加一个元素 |
removeFirst() | 从表头移除一个元素 |
removeLast() | 从表尾移除一个元素 |
getFirst() | 获得表头的元素 |
getLast() | 获得表尾的元素 |
2️⃣Java实现链表
java中ListNode链表就是用Java自定义实现的链表结构。
基本结构:
class ListNode { //类名 :Java类就是一种自定义的数据结构
int val; //数据 :节点数据
ListNode next; //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
}
添加构造方法方便初始化:
class ListNode { //类名 :Java类就是一种自定义的数据结构
int val; //数据 :节点数据
ListNode next; //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
ListNode(int val){ //构造方法 :构造方法和类名相同
this.val=val; //把接收的参数赋值给当前类的val变量
}
}
范型写法,使用范型可以兼容不同的数据类型:
class ListNode<E>{ //类名 :Java类就是一种自定义的数据结构
E val; //数据 :节点数据
ListNode<E> next; //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
ListNode(E val){ //构造方法 :构造方法和类名相同
this.val=val; //把接收的参数赋值给当前类的val变量
}
}
创建链表及遍历链表:
class ListNode { //类名 :Java类就是一种自定义的数据结构
int val; //数据 :节点数据
ListNode next; //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
ListNode(int val){ //构造方法 :构造方法和类名相同
this.val=val; //把接收的参数赋值给当前类的val变量
}
}
class Test{
public static void main(String[] args){
ListNode nodeSta = new ListNode(0); //创建首节点
ListNode nextNode; //声明一个变量用来在移动过程中指向当前节点
nextNode=nodeSta; //指向首节点
//创建链表
for(int i=1;i<10;i++){
ListNode node = new ListNode(i); //生成新的节点
nextNode.next=node; //把心节点连起来
nextNode=nextNode.next; //当前节点往后移动
} //当for循环完成之后 nextNode指向最后一个节点,
nextNode=nodeSta; //重新赋值让它指向首节点
print(nextNode); //打印输出
}
//打印输出方法
static void print(ListNode listNoed){
//创建链表节点
while(listNoed!=null){
System.out.println("节点:"+listNoed.val);
listNoed=listNoed.next;
}
System.out.println();
}
}
插入节点:
class ListNode { //类名 :Java类就是一种自定义的数据结构
int val; //数据 :节点数据
ListNode next; //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
ListNode(int val){ //构造方法 :构造方法和类名相同
this.val=val; //把接收的参数赋值给当前类的val变量
}
}
class Test{
public static void main(String[] args){
ListNode nodeSta = new ListNode(0); //创建首节点
ListNode nextNode; //声明一个变量用来在移动过程中指向当前节点
nextNode=nodeSta; //指向首节点
//创建链表
for(int i=1;i<10;i++){
ListNode node = new ListNode(i); //生成新的节点
nextNode.next=node; //把心节点连起来
nextNode=nextNode.next; //当前节点往后移动
} //当for循环完成之后 nextNode指向最后一个节点,
nextNode=nodeSta; //重新赋值让它指向首节点
print(nextNode); //打印输出
//插入节点
while(nextNode!=null){
if(nextNode.val==5){
ListNode newnode = new ListNode(99); //生成新的节点
ListNode node=nextNode.next; //先保存下一个节点
nextNode.next=newnode; //插入新节点
newnode.next=node; //新节点的下一个节点指向 之前保存的节点
}
nextNode=nextNode.next;
}//循环完成之后 nextNode指向最后一个节点
nextNode=nodeSta; //重新赋值让它指向首节点
print(nextNode); //打印输出
}
static void print(ListNode listNoed){
//创建链表节点
while(listNoed!=null){
System.out.println("节点:"+listNoed.val);
listNoed=listNoed.next;
}
System.out.println();
}
}
替换节点:
class ListNode { //类名 :Java类就是一种自定义的数据结构
int val; //数据 :节点数据
ListNode next; //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
ListNode(int val){ //构造方法 :构造方法和类名相同
this.val=val; //把接收的参数赋值给当前类的val变量
}
}
class Test{
public static void main(String[] args){
ListNode nodeSta = new ListNode(0); //创建首节点
ListNode nextNode; //声明一个变量用来在移动过程中指向当前节点
nextNode=nodeSta; //指向首节点
//创建链表
for(int i=1;i<10;i++){
ListNode node = new ListNode(i); //生成新的节点
nextNode.next=node; //把心节点连起来
nextNode=nextNode.next; //当前节点往后移动
} //当for循环完成之后 nextNode指向最后一个节点,
nextNode=nodeSta; //重新赋值让它指向首节点
print(nextNode); //打印输出
//替换节点
while(nextNode!=null){
if(nextNode.val==4){
ListNode newnode = new ListNode(99); //生成新的节点
ListNode node=nextNode.next.next; //先保存要替换节点的下一个节点
nextNode.next.next=null; //被替换节点 指向为空 ,等待java垃圾回收
nextNode.next=newnode; //插入新节点
newnode.next=node; //新节点的下一个节点指向 之前保存的节点
}
nextNode=nextNode.next;
}//循环完成之后 nextNode指向最后一个节点
nextNode=nodeSta; //重新赋值让它指向首节点
print(nextNode); //打印输出
}
//打印输出方法
static void print(ListNode listNoed){
//创建链表节点
while(listNoed!=null){
System.out.println("节点:"+listNoed.val);
listNoed=listNoed.next;
}
System.out.println();
}
}
删除节点:
class ListNode { //类名 :Java类就是一种自定义的数据结构
int val; //数据 :节点数据
ListNode next; //对象 :引用下一个节点对象。在Java中没有指针的概念,Java中的引用和C语言的指针类似
ListNode(int val){ //构造方法 :构造方法和类名相同
this.val=val; //把接收的参数赋值给当前类的val变量
}
}
class Test{
public static void main(String[] args){
ListNode nodeSta = new ListNode(0); //创建首节点
ListNode nextNode; //声明一个变量用来在移动过程中指向当前节点
nextNode=nodeSta; //指向首节点
//创建链表
for(int i=1;i<10;i++){
ListNode node = new ListNode(i); //生成新的节点
nextNode.next=node; //把心节点连起来
nextNode=nextNode.next; //当前节点往后移动
} //当for循环完成之后 nextNode指向最后一个节点,
nextNode=nodeSta; //重新赋值让它指向首节点
print(nextNode); //打印输出
//删除节点
while(nextNode!=null){
if(nextNode.val==5){
ListNode listNode=nextNode.next.next; //保存要删除节点的下一个节点
nextNode.next.next=null; //被删除节点 指向为空 ,等待java垃圾回收
nextNode.next=listNode; //指向要删除节点的下一个节点
}
nextNode=nextNode.next;
}//循环完成之后 nextNode指向最后一个节点
nextNode=nodeSta; //重新赋值让它指向首节点
print(nextNode); //打印输出
}
//打印输出方法
static void print(ListNode listNoed){
//创建链表节点
while(listNoed!=null){
System.out.println("节点:"+listNoed.val);
listNoed=listNoed.next;
}
System.out.println();
}
}
在对节点进行替换或删除的时候,被替换或被删节点的next引用需不需要设置为null?
答案是不需要的,这是因为一个对象被回收的前提是这个对象的引用(引用计数器为0)不再被持有,也就是说它不在被引用,这个时候它将会被回收,这个时候对于它引用什么对象已经无关紧要,这是因为对于它所引用的对象来说依然是看它的引用计数器是否为0。
后记
本文对链表概念
,单链表
,双链表
,循环链表
,包括它们的定义与特点,以及Java中链表的使用
(包括链表的创建与遍历,链表结点的插入与删除等)进行了详尽的介绍,如果你想继续深入了解更多数据结构与算法知识或深入学习Java,可参考以下学习路线。
👉Java全栈学习路线可参考:【Java全栈学习路线】最全的Java学习路线及知识清单,Java自学方向指引,内含最全Java全栈学习技术清单~ 👉算法刷题路线可参考:算法刷题路线总结与相关资料分享,内含最详尽的算法刷题路线指南及相关资料分享~