链表介绍
链表一般分为
- 单向链表
- 双向链表
- 环形链表
链表属于线性结构,有且只有一个根节点,且每个节点最多有一个直接前驱和一个直接后继的非空数据结构
链表是有序的列表,在内存中是如下存储的
- 链表是以节点的方式来存储的,是链式存储
- 每个节点包含data域,next域:指向下一个节点
- 如图: 链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头节点的链表,需要根据实际的需求来确定
链表和数组的比较
数组的优缺点
数组的优势在于可以方便的遍历查询所需要的数据,比如说(查询数组中第3个元素的位置),只需要1次操作即可,时间复杂度为(O(1)),因为数组在内存中是连续存储的,可以非常方便的找到指定元素,但是数组在插入和删除的时候,需要移动数组中大量的元素,而且数组是静态内存分配,定义数组的时候必须指定数组的长度,因此空间效率差。
链表的优缺点
链表的优势在于可以方便的插入和删除数据,时间复杂度为(o1),链表在内存中是不连续存储的,因此链表能节省很多空间。但是链表在查询数据的时候很麻烦,时间复杂度为O(n)。
单链表
这里我们使用带head头结点来实现单向链表
head结点说明
- 不存放具体的数据
- 作用就是表示单链表表的头节点
单链表之添加节点
节点类
//创建节点 我们通过节点来链表中存储数据的
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;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName +
'}';
}
}
第一种方法:在添加数据的时候,直接到数据添加到链表的尾部,代码如下
//添加节点 思路 不按编号进行添加
// 1.通过一个变量temp来接收头节点(因为头节点是不能变的), 用来找到最后一个节点
// 2. 更新temp的指向
// 3.将要添加的节点 添加到最后一个节点的next变量上
public void add(HeroNode heroNode){
HeroNode temp = head;
while (true){
//如果temp中的next 是null 的话 那么就找到了最后一个节点
if(temp.next == null){
break;
}
//如果temp中的next 不是null的话,那么就需要更改temp的指向
temp = temp.next;
}
//遍历完后 temp是指向最后一个节点的, 更新最后节点next的指向
temp.next = heroNode;
}
第二种方法: 根据no属性,从小到大按顺序添加节点
//添加节点 按编号添加节点 思路
//1.先通过一个变量temp接收头节点
//2.遍历所有节点 用来找到当前这个节点编号的位置 用两个变量来保存当前要插入节点的上一个节点(before) 和下一个节点(after)
//3.before.next 就指向当前插入的节点 当前插入的节点的next 用来指向 after
public void add1(HeroNode heroNode){
HeroNode temp = head;
HeroNode after_temp = null; //用来保存需要插入节点next指向 需要初始化为null 防止当前链表中只有一个节点
HeroNode before_temp = head;
while (true){
//temp就指向了最后一个节点 就直接插入到这个节点的后面
if(temp.next == null){
break;
}
//更新节点的位置
temp = temp.next;
//如果temp.next后面还有节点,我们就需要根据编号来进行插入
//如果当前节点的编号 小于需要插入节点的编号 那么就将不断的更新after_temp和before_temp的指向
if(temp.no < heroNode.no){
//这个after_temp节点是用来赋值给需要插入的节点.next
after_temp = temp.next;
before_temp = temp;
}else{
//如果插入节点小于当前节点,说明已经找到了after_temp和before_temp 就立即退出循环
break;
}
}
//循环结束后 temp表示 需要插入节点的上一个节点 after_temp表示需要插入节点的下一个节点
before_temp.next = heroNode;
heroNode.next = after_temp;
}
遍历节点
//遍历节点 思路
//1. 我们也需要用一个变量temp来接收头节点,因为头节点是不能变的
//2. 更新temp的指向
//3. 输出每一个节点
public void showList(){
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;
}
}
单链表之修改节点
//修改节点 思路
//1. 先使用一个变量 保存头节点的下一个节点
//2. 遍历所有节点 从链表中找到与需要修改节点相同的编号
//3. 进行修改 newHeroNode是修改之后的值
public void update(HeroNode newHeroNode){
if(head.next == null){
return; //如果到达链表尾部
}
HeroNode temp = head.next; //直接指向头节点的下一个节点
boolean flag = false; //用来标识是否找到了相同编号的节点
while (true){
if(temp == null){
break;
}
if(temp.no == newHeroNode.no){
flag = true;
break;
}
//更新节点的指向
temp = temp.next;
}
//当flag等于true temp就是我们需要修改的节点
if(flag){
temp.name = newHeroNode.name;
temp.nickName = newHeroNode.nickName;
}else{
System.out.println("没有找到我们要修改的节点");
}
}
单链表之删除节点
//删除节点 思路
//1. 用一个遍历来接收头节点 temp
//2. 遍历找到我们要删除的节点 更新temp的指向
//3. 记录我们要删除节点的上一个节点和下一个节点
public void delete(int id){
//如果链表里面没有数据 就直接退出
if(head.next == null){
return;
}
HeroNode temp = head;
boolean flag = false;//用来标识是否有我们要删除的节点
HeroNode before_temp = head; //记录要删除节点的前一个节点
HeroNode after_temp = null;//记录要删除节点的后一个节点
while (true){
if(temp == null){
break;
}
//找到了我们要删除的节点
if(temp.next.no == id){
flag = true ;
before_temp = temp;
//判断待删除节点的下一个节点是否为空
//防止空指针异常
if(temp.next.next == null){
after_temp = null;
}else{
after_temp = temp.next.next;
}
break;
}
//更新位置
temp = temp.next;
}
//需要删除的节点指向null
temp.next = null;
//删除节点前一个节点指向删除节点后一个节点
before_temp.next = after_temp;
}
单链表之获取有效节点个数
//返回单链表中有效节点的个数
public int getLength(){
if(head.next == null){
return 0;//空链表
}
HeroNode temp = head.next;
int length = 0;
while (temp!=null){
length++;
temp = temp.next;
}
return length;
}
单链表之查询倒数第K个结点
//查询链表中倒是第K个节点 思路
//1. 传入一个index index是查询的倒数第index个节点
//2. 遍历所有的节点,找到所有的有效节点个数n
//3. n-index 就是我们要查询的节点
public HeroNode getNode(int index){
int length = getLength();
int node_index = length-index;
HeroNode node = head.next; //存储我们要查询的节点
if(index<0 || index > length){
return null;
}else{
for (int i = 0; i < node_index; i++) {
node = node.next;
}
}
return node;
}
至此为止,单链表就说到这里了。单链表完整的代码如下:
package day03;
import java.util.Stack;
public class singleLinkedListDemo {
public static void main(String[] args) {
//模拟单向链表
//1.初始化节点
HeroNode hero1 = new HeroNode(1, "小心心", "可爱");
HeroNode hero2 = new HeroNode(2, "小心心1", "性感");
HeroNode hero3 = new HeroNode(3, "小心心2", "身材好");
HeroNode hero4 = new HeroNode(4, "小心心3", "淑女");
HeroNode hero5 = new HeroNode(5, "小心心4", "漂亮");
HeroNode hero6 = new HeroNode(7, "小心心4", "漂亮");
//2.创建链表管理节点
singleLinkedList list = new singleLinkedList();
//3.直接往链表里面添加节点
// list.add(hero1);
// list.add(hero2);
// list.add(hero3);
// list.add(hero4);
//4.按照编号添加节点
list.add1(hero1);
list.add1(hero6);
list.add1(hero4);
list.add1(hero2);
list.add1(hero5);
list.add1(hero3);
//5.遍历节点
list.showList();
//修改节点信息测试
HeroNode newHero = new HeroNode(2, "小鑫鑫", "性感大美女");
list.update(newHero);
//5.遍历节点
System.out.println("修改之后的节点");
list.showList();
//测试删除的节点
list.delete(1);
list.delete(4);
System.out.println("删除之后的节点");
list.showList();
//测试返回单链表的有效个数
System.out.println(list.getLength());
//测试链表中倒数第k个节点
System.out.println(list.getNode(1));
list.showList();
}
}
//创建一个链表用来管理节点的
class singleLinkedList{
//初始化头节点
private HeroNode head = new HeroNode(0,"小鑫","大美女");
//添加节点 思路 不按编号进行添加
// 1.通过一个变量temp来接收头节点(因为头节点是不能变的), 用来找到最后一个节点
// 2. 更新temp的指向
// 3.将要添加的节点 添加到最后一个节点的next变量上
public void add(HeroNode heroNode){
HeroNode temp = head;
while (true){
//如果temp中的next 是null 的话 那么就找到了最后一个节点
if(temp.next == null){
break;
}
//如果temp中的next 不是null的话,那么就需要更改temp的指向
temp = temp.next;
}
//遍历完后 temp是指向最后一个节点的, 更新最后节点next的指向
temp.next = heroNode;
}
//添加节点 按编号添加节点 思路
//1.先通过一个变量temp接收头节点
//2.遍历所有节点 用来找到当前这个节点编号的位置 用两个变量来保存当前要插入节点的上一个节点(before) 和下一个节点(after)
//3.before.next 就指向当前插入的节点 当前插入的节点的next 用来指向 after
public void add1(HeroNode heroNode){
HeroNode temp = head;
HeroNode after_temp = null; //用来保存需要插入节点next指向 需要初始化为null 防止当前链表中只有一个节点
HeroNode before_temp = head;
while (true){
//temp就指向了最后一个节点 就直接插入到这个节点的后面
if(temp.next == null){
break;
}
//更新节点的位置
temp = temp.next;
//如果temp.next后面还有节点,我们就需要根据编号来进行插入
//如果当前节点的编号 小于需要插入节点的编号 那么就将需要插入的节点 插入到当前节点的后面
if(temp.no < heroNode.no){
//这个after_temp节点是用来赋值给需要插入的节点.next
after_temp = temp.next;
before_temp = temp;
}else{
break;
}
}
//循环结束后 temp表示 需要插入节点的上一个节点 after_temp表示需要插入节点的下一个节点
before_temp.next = heroNode;
heroNode.next = after_temp;
}
//修改节点 思路
//1. 先使用一个变量 保存头节点的下一个节点
//2. 遍历所有节点 从链表中找到与需要修改节点相同的编号
//3. 进行修改
public void update(HeroNode newHeroNode){
if(head.next == null){
return; //如果到达链表尾部
}
HeroNode temp = head.next; //直接指向头节点的下一个节点
boolean flag = false; //用来标识是否找到了相同编号的节点
while (true){
if(temp == null){
break;
}
if(temp.no == newHeroNode.no){
flag = true;
break;
}
//更新节点的指向
temp = temp.next;
}
//当flag等于true temp就是我们需要修改的节点
if(flag){
temp.name = newHeroNode.name;
temp.nickName = newHeroNode.nickName;
}else{
System.out.println("没有找到我们要修改的节点");
}
}
//删除节点 思路
//1. 用一个遍历来接收头节点 temp
//2. 遍历找到我们要删除的节点 更新temp的指向
//3. 记录我们要删除节点的上一个节点和下一个节点
public void delete(int id){
//如果链表里面没有数据 就直接退出
if(head.next == null){
return;
}
HeroNode temp = head;
boolean flag = false;//用来标识是否有我们要删除的节点
HeroNode before_temp = head; //记录要删除节点的前一个节点
HeroNode after_temp = null;//记录要删除节点的后一个节点
while (true){
if(temp == null){
break;
}
//找到了我们要删除的节点
if(temp.next.no == id){
flag = true ;
before_temp = temp;
if(temp.next.next == null){
after_temp = null;
}else{
after_temp = temp.next.next;
}
break;
}
//更新位置
temp = temp.next;
}
// 0 1 4 5
temp.next = null;
before_temp.next = after_temp;
}
//返回单链表中有效节点的个数
public int getLength(){
if(head.next == null){
return 0;//空链表
}
HeroNode temp = head.next;
int length = 0;
while (temp!=null){
length++;
temp = temp.next;
}
return length;
}
//查询链表中倒是第K个节点 思路
//1. 传入一个index index是查询的倒数第index个节点
//2. 遍历所有的节点,找到所有的有效节点个数n
//3. n-index 就是我们要查询的节点
public HeroNode getNode(int index){
int length = getLength();
int node_index = length-index;
HeroNode node = head.next; //存储我们要查询的节点
if(index<0 || index > length){
return null;
}else{
for (int i = 0; i < node_index; i++) {
node = node.next;
}
}
return node;
}
//遍历节点 思路
//1. 我们也需要用一个变量temp来接收头节点,因为头节点是不能变的
//2. 更新temp的指向
//3. 输出每一个节点
public void showList(){
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;
}
}
}
//创建节点 我们通过节点来链表中存储数据的
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;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName +
'}';
}
}
双向链表
管理单向链表的缺点分析
- 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找
- 单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,
双向链表之添加节点
节点类
//创建节点
static class HeroNode{
public int no; //编号
public String name; //名称
public String nickName;//别名
public HeroNode next; //指向下一个节点
public HeroNode pre;//指向上一个节点
public HeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName +
'}';
}
}
第一种方法默认添加到双向链表的最后,代码如下
//添加节点 添加到链表末尾(不按编号添加)
public void add(HeroNode heroNode){
HeroNode temp = head;
while (true){
if(temp.next == null){
break;
}
temp = temp.next;
}
//遍历完 就找到了最后一个节点 temp
temp.next = heroNode;
//因为是双向链表,添加节点的pre要指向temp
heroNode.pre = temp;
}
第二种方法 从小到大 按顺序添加节点 代码如下
//添加节点 按编号添加节点
public void add1(HeroNode heroNode){
HeroNode temp = head;
while (true){
if(temp.next == null){
break;
}
if(heroNode.no<temp.next.no){
break; // heroNode 要插入到temp的前一个位置
}
temp = temp.next;
}
//循环结束 要插入的节点就再temp节点的后面
//要先使当前要插入的节点的next指向 temp节点的下一个节点 不能先更新temp.next 不然会陷入死循环
heroNode.next = temp.next;
//先判断下一个节点为空 如果为空的话 temp.next.pre会报错
//如果temp是尾节点的话 temp.next就会是空的
if(temp.next !=null){
temp.next.pre = heroNode;
}
temp.next = heroNode;
heroNode.pre = temp;
}
双向链表之修改节点
修改节点的思路和单向链表一样,这里就不多说了,直接就上代码
//修改节点
public void update(HeroNode heroNode){
if(head.next == null){
return;
}
HeroNode temp = head.next;
boolean flag = false;
while (temp!=null){
if(temp.no == heroNode.no){
flag = true;
break;
}
temp = temp.next;
}
//循环结束后 temp就是我要修改的节点
if(flag){
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
}else {
System.out.println("你修改的节点不存在");
}
}
双向链表之删除节点
思路如下
- 因为是双向链表,因此,我们可以实现自我删除某个节点
- 直接找到要删除的这个节点,比如temp
- temp.pre.next = temp.next
- 要先判断temp.next是否为空,否则会出现空指针异常(比如temp是最后一个节点) temp.next.pre = temp.pre;
代码如下
//删除节点
public void delete(HeroNode heroNode){
if(head.next == null){
return;
}
HeroNode temp = head.next;
boolean flag = false;
//循环遍历找到要删除的节点
while (temp!=null){
if(temp.no == heroNode.no){
//那么temp就是我们要删除的节点
flag = true;
break;
}
//更新节点的位置
temp = temp.next;
}
if(flag){
//修改删除节点的 前后两个节点的指向
temp.pre.next = temp.next;
if(temp.next != null){
temp.next.pre = temp.pre;
}
}else{
System.out.println("没有你要删除的节点");
}
}
双向链表之遍历链表
遍历可以向前遍历,也可以向后查找
//遍历节点
public void showList(){
if(head.next == null){
return;
}
HeroNode temp = head.next;
while (temp!=null){
System.out.println(temp);
temp = temp.next;
}
}
好了,双向链表基础的知识点就到这里。 双向链表完整的代码如下
package day03;
//双向链表的实现
public class twoSingleLinkedListDemo {
public static void main(String[] args) {
//验证添加节点
twoSingleLinkedList list = new twoSingleLinkedList();
HeroNode hero1 = new HeroNode(1, "小心心", "可爱");
HeroNode hero2 = new HeroNode(2, "小心心1", "性感");
HeroNode hero5 = new HeroNode(5, "小心心4", "漂亮");
HeroNode hero3 = new HeroNode(3, "小心心2", "身材好");
HeroNode hero4 = new HeroNode(4, "小心心3", "淑女");
// list.add(hero1);
// list.add(hero2);
// list.add(hero3);
// list.add(hero4);
// list.add(hero5);
//
// list.showList();
//
//测试按编号添加节点
System.out.println("按顺序添加节点");
list.add1(hero1);
list.add1(hero4);
list.add1(hero5);
list.add1(hero2);
list.add1(hero3);
list.showList();
//测试删除节点
System.out.println("删除之后的链表");
list.delete(hero5);
list.showList();
//测试修改节点
HeroNode updateNode = new HeroNode(4, "小鑫鑫", "漂亮又可爱");
list.update(updateNode);
System.out.println("修改之后的节点");
list.showList();
}
//创建节点
static class HeroNode{
public int no; //编号
public String name; //名称
public String nickName;//别名
public HeroNode next; //指向下一个节点
public HeroNode pre;//指向上一个节点
public HeroNode(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName +
'}';
}
}
//创建双向链表
static class twoSingleLinkedList{
//定义一个头节点
private HeroNode head = new HeroNode(0,"","");
//添加节点 添加到链表末尾(不按编号添加)
public void add(HeroNode heroNode){
HeroNode temp = head;
while (true){
if(temp.next == null){
break;
}
temp = temp.next;
}
//遍历完 就找到了最后一个节点
temp.next = heroNode;
heroNode.pre = temp;
}
//添加节点 按编号添加节点
public void add1(HeroNode heroNode){
HeroNode temp = head;
while (true){
if(temp.next == null){
break;
}
if(heroNode.no<temp.next.no){
break; // heroNode 要插入到temp的前一个位置
}
temp = temp.next;
}
//循环结束 要插入的节点就再temp节点的后面
//要先使当前要插入的节点的next指向 temp节点的下一个节点 不能先更新temp.next 不然会陷入死循环
heroNode.next = temp.next;
//先判断下一个节点为空 如果为空的话 temp.next.pre会报错
//如果temp是尾节点的话 temp.next就会是空的
if(temp.next !=null){
temp.next.pre = heroNode;
}
temp.next = heroNode;
heroNode.pre = temp;
}
//删除节点
public void delete(HeroNode heroNode){
if(head.next == null){
return;
}
HeroNode temp = head.next;
boolean flag = false;
//循环遍历找到要删除的节点
while (temp!=null){
if(temp.no == heroNode.no){
//那么temp就是我们要删除的节点
flag = true;
break;
}
//更新节点的位置
temp = temp.next;
}
if(flag){
//修改删除节点的 前后两个节点的指向
temp.pre.next = temp.next;
if(temp.next != null){
temp.next.pre = temp.pre;
}
}else{
System.out.println("没有你要删除的节点");
}
}
//修改节点
public void update(HeroNode heroNode){
if(head.next == null){
return;
}
HeroNode temp = head.next;
boolean flag = false;
while (temp!=null){
if(temp.no == heroNode.no){
flag = true;
break;
}
temp = temp.next;
}
//循环结束后 temp就是我要修改的节点
if(flag){
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
}else {
System.out.println("你修改的节点不存在");
}
}
//遍历节点
public void showList(){
if(head.next == null){
return;
}
HeroNode temp = head.next;
while (temp!=null){
System.out.println(temp);
temp = temp.next;
}
}
}
}
好了,单向链表和双向链表的基础就说到这里。希望大家共同学习进步,不足之处望指出。