一、单向链表
介绍
- 链表是逻辑有序,空间无序的。
如下图所示,这是一个带头节点的链表在内存中的存储结构。相邻的链表节点存储内存并不一定在一起,而是通过next去指向下一个节点所在的位置
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域:指向下一个节点
- 如图:发现链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
实例
- 使用带 head 头的单向链表实现 –根据添加人物的排名对水浒英雄人物的增删查操作(该对象属性包括:姓名,排名,昵称,以及其指向的next节点对象)
首先我们来分析增加如何实现:
我们需要根据传入人物的排名将该人物插入到对应的链表中间(如果有这个排名,则添加失败,并给出提示),思路分析
我的理解:假设要将数据2插入到数据1和4之间,那我们应该先将数据2的下个节点设置为数据4,再将数据1的下个节点设置为数据2。可以理解为一个人拉着另外一个要掉下悬崖人的手,现在要再这两人中间再加上一个人,那肯定让下面那个人连上新加的人的手,再让新加人的手连上一开始上面的人,如果反之的下面的人就没有人跟他连接了,他不就直接掉下去了嘛。详情见代码
再来分析一下如何删除一个指定节点
1.我们需要找到需要删除的这个节点的前一个节点temp
2.temp.next = temp.next.next【直接将需要删除的前一个节点指向下下个节点,绕过被删除节点,这样java的垃圾回收器就回去回收它】
代码
public class SingleLinkedListDemo {
public static void main(String[] args) {
//测试
//创建节点
HeroNode hero1 = new HeroNode(1,"宋江","及时雨");
HeroNode hero2 = new HeroNode(2,"卢俊义","玉麒麟");
HeroNode hero3 = new HeroNode(3,"吴用","智多星");
HeroNode hero4 = new HeroNode(4,"林冲","豹子头");
//创建链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
//加入
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
//修改链表之前的情况
singleLinkedList.list();
//从链表中删除一个节点
singleLinkedList.del(2);
//删除链表之后的情况
System.out.println("======华丽的分割线=======");
singleLinkedList.list();
}
}
/**
* 定义SingleLinkedList 管理英雄
*/
class SingleLinkedList{
/**
* 初始化头结点,不存储具体数据,注意:头结点不要动,因为我们需要根据头结点去找到下一个节点
*/
private HeroNode head = new HeroNode(0,",","");
/**
*第二种在添加英雄时,根据排名将英雄插入到指定位置
* 如果有这个排名,则添加失败,并给出提示
*/
public void addByOrder(HeroNode heroNode) {
//因为头结点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置
//因为单链表,因为我们找的temp是位于添加位置的前一个节点,否则插入不了
HeroNode temp = head;
//flag标志添加的编号是否存在,默认为false
boolean flag = false;
while(true) {
//说明temp已经在链表的最后面
if(temp.next == null){
break;
}
if(temp.next.no>heroNode.no) {
//位置找到了,因为temp的下一个节点大于传入值得节点
break;
}else if(temp.next.no == heroNode.no){
//说明希望添加的heroNode已经存在
flag = true;
break;
}
//后移,继续遍历当前链表
temp = temp.next;
}
//判断flag的值
if(flag){
//不能添加,说明编号已经存在
System.out.printf("准备插入的英雄编号 %d 已经存在\n",heroNode.no);
}else{
//插入到链表中
heroNode.next = temp.next;
temp.next = heroNode;
}
}
/**
* 显示列表【遍历】
*/
public void list() {
//判断链表是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
//因为头节点,不能动,因此我们需要一个辅助变量来遍历
HeroNode temp = head.next;
while (temp != null) {
//判断是否到链表最后
//输出节点信息
System.out.println(temp);
//将temp后移
temp = temp.next;
}
}
/**
* 删除节点
* 思路
* 1.head 不能动,因此我们需要一个temp辅助节点找到待删除节点的前一个节点
* 2.说明我们在比较时,是temp.next.no 和 需要删除的节点的no比较
*/
public void del(int no) {
HeroNode temp = head;
//记录该值是否能被删除
boolean flag = false;
while(true){
if(temp.next == null){
//查到最后一个节点都没有我们要删除的对象
break;
}
if(temp.next.no == no) {
//找到待删除节点的前一个节点:temp
flag = true;
break;
}
//temp后移
temp = temp.next;
}
//判断flag从而得知能否删除该节点
if(!flag) {
System.out.printf("不跟删除节点为 %d 的值",no);
}else{
temp.next = temp.next.next;
}
}
}
/**
*定义HeroNode,每个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 +
'}';
}
}
面试题
一、查询链表有效节点的个数
思路 :遍历节点后的个数即可
二、查找单链表中的倒数第index个结点 【新浪面试题】
1、先遍历得到 有效数据个数(总长度–不包括头)
2、有效个数 - index = 需要从头查找的次数n
3、遍历链表n次即可得到倒数第个节点
三、 单链表的反转【腾讯面试题,有点难度】
思路:
1.先定义一个节点 reverseHead = new HeroNode()
2.定义一个next指向遍历中原链表的下一个节点,如果链表为1-2-3-4,那么遍历该链表的第一次next的值就为2-3-4,而不单单是2
3.从头到尾遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
4.将原来的链表指向倒转后的链表head.next = reverseHead.next
5.对于初学者来说可能有点难理解,debug多跑几次,多想想
四、从尾到头打印单链表 【百度】
思路:
1.先反转链表,再打印【缺点:破坏链表结构,建议】
2.使用栈先进后出的特性,遍历链表,将各个节点压入栈,再遍历栈,这样最近进栈的第一个节点就最后打印出
代码
/**
* 题目一 查询有效节点的个数
*/
public static int getLength(HeroNode head) {
int length = 0;
if(head.next == null) {
//空链表
return length;
}
//定义一个辅助变量
HeroNode cur = head.next;
while(cur != null){
length++;
cur = cur.next;
}
return length;
}
/**
* 题目二:查找单链表中的倒数第index个结点 【新浪面试题】
*思路:
* 1、先遍历得到 有效数据个数(总长度--不包括头)
* 2、有效个数 - index = 需要从头查找的次数n
* 3、遍历链表n次即可得到倒数第个节点
*/
public static HeroNode findLastIndexNode(HeroNode head,int index){
// 判空
if (head.next == null) {
return null;
}
//链表总长度
int size = getLength(head);
//对index的合理性做出校验
if (index <= 0 || index > size) {
// 查找的位置不能大过总长
return null;
}
// 定义给辅助变量, for 循环定位到倒数的index
HeroNode cuc = head.next;
//如果链表长度为4,查询倒数第一个,cuc应该.next三次
for (int i = 0; i < size - index; i++) {
cuc = cuc.next;
}
return cuc;
}
/**
* 题目三:单链表的反转【腾讯面试题,有点难度】
*/
public static void reversetList(HeroNode head) {
// 若链表为空,或者只有一个则不需要反转
if (head.next == null || head.next.next == null) {
return;
}
// 定义一个辅助指针,用户遍历原始未反转的那个链表
HeroNode cuc = head.next;
// 用于动态指向 当前节点【cuc】的 下一个节点
HeroNode next = null;
// 新链表(反转后的)的链表头
HeroNode reversetHead = new HeroNode(0, "", "");
// 思路: 遍历原始链表,没遍历一个节点将其取出,并放在新链表头(reversetHead)的后面的最前端
// 每一个新节点,都会取代旧的,与新链表头牵手
while (cuc != null) {
// 暂时先保存当前节点的下一个节点,用于后移
next = cuc.next;
// 将要 遍历出来的新节点,指向 新链表头 后面的最前端(将情敌的手,从她手上拨开)
cuc.next = reversetHead.next;
// 将 cuc 连接到 新的链表头后面的最前端(她的手和你牵了)
reversetHead.next = cuc;
// cuc 后移,换下一个情敌来打你
cuc = next;
}
// 将head.next 指向 reverseHead.next , 实现单链表的反转
// 新链表(她)池塘养的鱼和旧链表头(她的闺蜜)共享
head.next = reversetHead.next;
}
/**
* 题目四:从尾到头打印单链表 【百度,要求方式1:反向遍历 。 方式2:Stack栈】
* 思路:
* 1.我们可以先将链表倒转,再遍历。缺点:破坏链表原有结构
* 2.将链表遍历入栈,再遍历栈。栈特点:先进后出
*/
public static void stackPrint(HeroNode head) {
//如果链表为空直接return
if(head.next == null){
System.out.println("链表为空");
return;
}
//创建一个栈,将各个节点压入栈
Stack<HeroNode> stack = new Stack<>();
//遍历链表将节点压入栈
while(head.next != null) {
stack.push(head.next);
//链表指针后移
head = head.next;
}
//遍历stack,将栈中的节点打印,弹栈
while(!stack.isEmpty()){
System.out.println(stack.pop());
}
}
双向链表
区别
1、单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找,从双向链表中的任意一个结点开始,都可以很方便地访问前驱结点和后继结点。
2、单向链表不能自我删除,需要靠辅助(前一个)节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到temp,temp.next = temp.next.next 待删除节点的前一个节点
3.双向链表:增加删除节点复杂,需要多分配一个指针存储空间。
4.单向链表:单个结点创建非常方便,普通的线性内存通常在创建的时候就需要设定数据的大小,结点的访问方便,可以通过循环或者递归的方法访问到任意数据。
双向链表的CURD
1.添加,还是根据传入HeroNode的编号属性进行添加,插在第一个比传入的编号值大的节点前面,详情解释见注释
2.修改,根据传入编号修改属性
3.删除,找到要删除的节点,让其上一个节点的next指针指向被删除节点的下一个节点,让被删除节点的下一个节点的pre指向被删除节点的上一个节点
4.查询,遍历节点
public class DoubleLinkedListDemo {
public static void main(String[] args) {
HeroNode2 heroNode = new HeroNode2(1,"宋江","及时雨");
HeroNode2 heroNode2 = new HeroNode2(2,"吴用","智多星");
HeroNode2 heroNode3 = new HeroNode2(3,"豹子头","林冲");
HeroNode2 heroNode4 = new HeroNode2(4,"纪枫","大枫");
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.addByOrder(heroNode);
doubleLinkedList.addByOrder(heroNode2);
doubleLinkedList.addByOrder(heroNode4);
doubleLinkedList.addByOrder(heroNode3);
doubleLinkedList.list();
}
}
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 (temp != null) {
//判断是否到链表最后
//输出节点信息
System.out.println(temp);
//将temp后移
temp = temp.next;
}
}
/**
* 第二种在添加英雄时,根据排名将英雄插入到指定位置
*/
public void addByOrder(HeroNode2 heroNode) {
//定义变量,能添加时true,不能添加false
boolean flag = true;
//定义变量
HeroNode2 temp = head;
while(true) {
if(temp.next == null){
break;
}
if(temp.next.no>heroNode.no) {
break;
}else if(temp.next.no == heroNode.no){
flag = false;
System.out.println("该节点已存在,不可添加");
}
temp = temp.next;
}
//是否能插入该节点
if(flag){
//插入节点的向下指针指向比它大的数字
heroNode.next = temp.next;
//比他大的那个节点的pre节点要指向插入节点
//判断该节点下一个节点是否为空,空的话就不用设置下一个节点的pre指针
//这两步就是将插入节点与它后面的那个节点连上
if(temp.next != null){
temp.next.pre = heroNode;
}
//将插入节点的pre连上要插入两个节点中间的前一个节点
heroNode.pre = temp;
//同理前一个节点的next指针指向插入节点
temp.next = heroNode;
}
}
/**
* 修改一个节点【单双向链表修改节点的方法一样】
*/
public void update(HeroNode2 heroNode) {
//判断被修改的节点是否为空
if(head.next == null) {
System.out.println("链表为空");
return;
}
//定义辅助变量
HeroNode2 temp = head;
//表示是否找到该节点
boolean flag = false;
//遍历链表根据传入节点的no得到被修改的节点
while(true){
if(temp.next == null ){
System.out.println("没有找到被修改节点");
break;
}
if(temp.next.no == heroNode.no){
//找到了
flag = true;
break;
}
temp = temp.next;
}
//修改节点
if(flag){
temp.next.name = heroNode.name;
temp.next.nickname = heroNode.nickname;
}else{
System.out.println("没有找到该节点");
}
}
/**
* 删除一个节点
*/
public void del(HeroNode2 heroNode) {
//判断链表是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
//定义辅助变量,指向节点本身
HeroNode2 temp = head.next;
//定义变量,用于确定是否找到被删除节点
boolean flag = false;
//遍历节点,寻找被删除节点
while(true) {
//到链表最后了
if(temp == null){
System.out.println("没有找到待删除节点");
break;
}
//已找到被删除节点
if(temp.no == heroNode.no){
flag = true;
}
temp = temp.next;
}
//判断是否找到待删除节点
if (!flag){
System.out.println("没有找到待删除节点");
}else{
//将待删除节点的前一个节点的next指向待删除节点的next
heroNode.pre.next = heroNode.next;
//判断待删除节点是否处于节点最后
if(heroNode.next != null){
heroNode.next.pre = heroNode.pre;
}
}
}
}
class HeroNode2{
public int no;
public String name;
public String nickname;
/**
* 指向下一个节点,默认为空
*/
public HeroNode2 next;
/**
* 指向上一个节点,默认为空
*/
public HeroNode2 pre;
@Override
public String toString() {
return "HeroNode2{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
public HeroNode2(int no, String name, String nickname) {
this.no = no;
this.name = name;
this.nickname = nickname;
}
}