一.概述
1.概念
链表: 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。每一个链表都包含多个节点,节点又包含两个部分,一个是数据域(储存节点含有的信息),一个是引用域(储存下一个节点或者上一个节点的地址)。
2.使用链表的优缺点
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
3.链表分类
链表分为单向链表和双向链表
二. 单向链表
1.单向链表概述
单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点由元素和指针构成。在Java中,我们可以将单链表定义成一个类,单链表的基本操作即是类的方法,而结点就是一个个实例化的对象,每个对象中都有“元素值”和“下一结点地址”两个属性。在“下一结点地址”属性中存储的是下一个对象的引用,这样,一个个对象连在一起就成为了单链表。
2.单链表的逻辑结构
单链表是链表中结构最简单的。一个单链表的节点(Node)分为两个部分,第一个部分(data)保存或者显示关于节点的信息,另一个部分存储下一个节点的地址。最后一个节点存储地址的部分指向空值。
3.单链表的实现(水浒英雄排名案例)
(1)链表的创建及添加
1)节点类
//定义HeroNode节点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; //ָ指向下一个节点
//构造器
public HeroNode(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
//重写toString
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
}
}
2)添加(直接添加到尾部,不考虑排序)
//链表类SingleLinkedList,用于管理链表的增删改查
class SingleLinkedList {
//初始化头节点,不存放具体数据
private HeroNode head = new HeroNode(0, "", "");
public HeroNode getHead() {
return head;
}
//添加节点到单向链表,不考虑排序
//思路
//1.找到当前链表的最后节点
//2.将找到的最后节点的next指向新节点
public void add(HeroNode heroNode) {
//因为head节点不能动,需要一个辅助遍历的 temp节点
HeroNode temp = head;
//遍历链表,找到最后
while(true) {
//找到了链表的最后一个节点
if(temp.next == null) {//最后一个的next为null
break;
}
//如果没有找到就将temp后移(将下一个节点赋给temp)
temp = temp.next;
}
//当退出while循环是,证明已经找到了链表的最后一个节点
//最后节点已经赋给了temp,对temp的操作就是对最后节点的操作
//将最后节点的next指向新添加的节点
temp.next = heroNode;
}
}
3) 单链表的遍历
//显示链表[遍历]
public void list() {
//判断链表是否为空
if(head.next == null) {
System.out.println("链表为空");
return;
}
//使用辅助节点变量帮助遍历
HeroNode temp = head.next;
while(true) {
//判断链表是否已经到了最后
if(temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将temp后移
temp = temp.next;
}
}
4)根据序号,添加到指定的位置(如果序号存在,提示添加失败)-----有序链表
//第二种添加方式,根据序号按顺序添加,如果序号已经存在,则提示失败
public void addByOrder(HeroNode heroNode) {
//头节点不能改变,因此需要一个辅助指针变量(节点)来帮住找到要添加的位置
//因为是单链表,因此我们找到的temp要位于添加位置的前一个节点
//找到需要添加节点的前一个节点后(也就是temp),只需将原来temp指向的next节点让新节点的next来指向,然后将temp的next指向新节点
HeroNode temp = head;
boolean flag = false; // flag标志添加的编号是否存在,默认false
while(true) {
if(temp.next == null) {//说明temp已经到了链表的最后
break; //
}
if(temp.next.no > heroNode.no) { //位置找到
break;
} else if (temp.next.no == heroNode.no) {//说明编号已经存在
flag = true;
break;
}
temp = temp.next; //temp后移继续查找
}
//根据flag给提示
if(flag) { //编号存在
System.out.printf("编号%d 已经存在,不能加入\n", heroNode.no);
} else {
//放到temp的后面
//temp原来指向的节点由新节点重新指向
heroNode.next = temp.next;
//temp的next重新指向新节点
temp.next = heroNode;
}
}
5)修改节点
//根据编号修改节点
//根据新节点的编号找到找到要修改的节点
public void update(HeroNode newHeroNode) {
//判断链表是否为空
if(head.next == null) {
System.out.println("链表为空");
return;
}
//根据新节点编号找到需要修改的节点
//定义一个辅助节点
HeroNode temp = head.next;
boolean flag = false; //标志是否找到要修改的节点
while(true) {
if (temp == null) {
break; //遍历完了
}
if(temp.no == newHeroNode.no) {
//找到了要修改的节点
flag = true;
break;
}
//temp后移继续寻找
temp = temp.next;
}
//是否找到要修改的节点
if(flag) {
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
} else { //没有找到要修改的节点
System.out.printf("没有找到编号 %d 的节点,不能修改\n", newHeroNode.no);
}
}
6)删除节点
//根据编号删除节点
//思路
//根据要节点编号找到要删除的节点前一个节点,
// 将要删除的节点的前一个节点的next指向由要删除的节点指向要删除节点的下一个节点
public void del(int no) {
HeroNode temp = head;
boolean flag = false; //标志是否找到要删除的节点
while(true) {
if(temp.next == null) { //遍历结束
break;
}
if(temp.next.no == no) {
//找到了要删除的节点的前一个节点
flag = true;
break;
}
temp = temp.next; //temp后移继续查找
}
//是否找到要删除的节点的前一个节点
if(flag) {
//将要删除的节点的前一个节点的next指向由要删除的节点指向要删除节点的下一个节点
temp.next = temp.next.next;
}else {
System.out.printf("要删除的%d 不存在\n", no);
}
}
7)获取有效节点个数
//获取单链表的节点个数,如果带头节点的链表,要求不统计节点头
/**
*
* @param head 节点头
* @return 有效节点个数
*/
public static int getLength(HeroNode head) {
if(head.next == null) {
return 0;
}
int length = 0;
//辅助指针变量
HeroNode cur = head.next;
while(cur != null) {
length++;
cur = cur.next; //节点后移
}
return length;
}
}
三.双向链表
1.双向链表概述
双向链表每一个结点不仅配有next引用,同时还有一个prev引用,指向其上一个结点(前驱结点), 没有前驱的时候就为NULL。如jdk中的LinkedList就是一个双向链表
2.双链表结构逻辑图
3.双链表实现
1)双向链表的节点类
//定义双向链表的节点
class HeroNode2 {
public int no;
public String name;
public String nickname;
public HeroNode2 next; //指向下一个节点,默认为null
public HeroNode2 pre; //指向前一个节点,默认为null
//构造器
public HeroNode2(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
//重写toString
@Override
public String toString() {
return "HeroNode [no=" + no + ", name=" + name + ", nickname=" + nickname + "]";
}
}
2) 双向链表的创建及添加及节点的遍历展示
//创建一个双向链表类
class DoubleLinkedList {
//先初始化一个头节点,不存放具体数据
private HeroNode2 head = new HeroNode2(0, "", "");
//返回头节点
public HeroNode2 getHead() {
return head;
}
//遍历双向链表
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//辅助指针变量
HeroNode2 temp = head.next;
while (true) {
//判断链表是否到尾
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//指针变量后移
temp = temp.next;
}
}
//添加一个节点到双向链表的最后
public void add(HeroNode2 heroNode) {
//辅助指针变量temp
HeroNode2 temp = head;
//遍历
while (true) {
//找到链表的最后
if (temp.next == null) {//
break;
}
//指针变量后移
temp = temp.next;
}
//当退出循环是指针变量temp就是链表的最后一个节点
//放到双向链表的最后
temp.next = heroNode;
heroNode.pre = temp;
}
}
3)修改
//修改一个节点的内容,和单向链表一样
public void update(HeroNode2 newHeroNode) {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空~");
return;
}
HeroNode2 temp = head.next;
boolean flag = false; //表示是否找到要修改的节点
while (true) {
if (temp == null) {
break; //链表已经遍历完
}
if (temp.no == newHeroNode.no) {
//找到要修改的节点
flag = true;
break;
}
temp = temp.next;
}
//根据flag判断是否找到要修改的节点
if (flag) {
temp.name = newHeroNode.name;
temp.nickname = newHeroNode.nickname;
} else { // 没有找到
System.out.printf("没有找到编号%d 的节点\n", newHeroNode.no);
}
}
4) 删除
//从双向链表中删除一个节点
// 1 对于双向链表,我们可以直接找到要删除的这个节点
// 2 找到后自我删除
public void del(int no) {
//判断当前链表是否为空
if (head.next == null) {
System.out.println("当前链表为空,无法删除");
return;
}
HeroNode2 temp = head.next; // 辅助指针变量
boolean flag = false; //判断是否找到待删除节点
while (true) {
if (temp == null) { //已经找到链表最后
break;
}
if (temp.no == no) {
// 找到要删除的节点
flag = true;
break;
}
temp = temp.next; // temp后移
}
// 判断是否找到要删除的节点
if (flag) {
//删除
// temp.next = temp.next.next;[单向链表的删除]
temp.pre.next = temp.next;
// 如果是最后一个节点,就不需要下面的,否则会出现空指针
if (temp.next != null) {
temp.next.pre = temp.pre;
}
} else {
System.out.printf("要删除的节点%d 不存在\n", no);
}
}
四.总结
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
在链表中一般在大多数需要使用有序数组的场合也可以使用有序链表。在数据处理效率上, 有序链表优于有序数组的地方是插入的速度(因为元素不需要移动),另外链表可以扩展到全部有效的使用内存,而数组只能局限于一个固定的大小中。在有序链表中插入和删除某一项最多需要O(N)次比较,平均需要O(N/2)次,因为必须沿着链表上一步一步走才能找到正确的插入位置,然而可以最快速度删除最值,因为只需要删除表头即可,如果一个应用需要频繁的存取最小值,且不需要快速的插入,那么有序链表是一个比较好的选择方案。比如优先级队列可以使用有序链表来实现。
来源:CSDN
作者:weixin_42232931
链接:https://blog.csdn.net/weixin_42232931/article/details/104191768