大家好,我是被白菜拱的猪。
一个热爱学习废寝忘食头悬梁锥刺股,痴迷于girl的潇洒从容淡然coding handsome boy。
一、写在前言
让我们一天一个脚印,今天就来解决数据结构中的链表问题。
二、链表
(一)链表介绍
链表是有序的列表,但是它在内存中存储是不连续的,如下图所示;
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域:指向下一个节点.
- 链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头节点的链表,根据实际
所以我们在创建链表的时候,必须先创建结点。
(二)单链表
单链表的逻辑图
我们构造一个场景,使用带head头的单向链表实现 –水浒英雄排行榜管理 完成对英雄人物的增删改查操作。从而加深对链表的印象。
首先我们要创建一个英雄的结点:
class HeroNode{
public int no;//排名
public String name;//姓名
public String nickName;//外号
HeroNode next;
//alt+insert 快捷键生成构造器
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、添加
第一种方法在添加英雄时,直接添加到链表的尾部 。
思路:
- 在链表中先创建一个head头结点,作用就是表示单链表的头
- 后面我们每添加一个节点,就直接加入到链表的最后
第二种放在根据英雄的排名添加到指定位置。
思路:
- 因为头结点是不能动的,所以我们使用辅助结点,查找到指定位置的前一个结点,为什么是前一个结点,我们要通过操作前一个的next从而把这个结点挂上去。
2.然后把新的结点挂上去,这里先把新的结点挂上去,然后在操作前一个结点连上,为什么要这么做?假如我们先连,如下图所示,2先将3连上,那么我们就不知道4的位置,从而无法将3插到2,4之间,因为4的位置我们是通过2的next找到的。
这里说一下我们怎么找到要插入地方的前一个结点呢?temp.next.no>newNode.no。就好像我们小时候排队一样,链表就是从头开始往下找,我们可能是找到第一个比我们高的人,然后排在他的前面,假如我们找比自己矮的,那么我们找到第一个人,发现他比我们矮,然后我们就排在了他的后面,殊不知后面好几个人都比我矮,显而易见这样的找法是错的。
2、查询
通过一个辅助变量,帮助我们遍历整个链表
3、修改
通过辅助变量,找到该结点然后对其操作
4、删除
从单链表中删除一个节点的思路:
- 找到需要删除的这个节点的前一个节点temp
- temp.next=temp.next.next
- 被删除的结点,将不会有其他引用指向,会被垃圾回收机制回收
public void delete(int no) {
//判断链表是否为空
if(head.next == null) {
System.out.println("链表为空");
return;
}
HeroNode temp = head;
boolean flag = false;//是否找到要删除的结点
while(true) {
if(temp.next == null){//已经到了链表的最后
break;
}
//要找到删除结点的前一个元素
if(temp.next.no == no){
flag = true;
break;
}
temp = temp.next;
}
if(flag) {
//可以删除
temp.next = temp.next.next;
}else {
System.out.println("要删除的结点不存在");
}
}
注意这里一定是要找到要删除结点的前一个元素
5、代码与总结
总结:
我们不难发现,对单链表的操作都是从头开始查找,而且往往都借用了辅助变量(指针)来查找,在进行删除,添加的时候,我们要找到要操作位置的前一位置,这样才能操作数据,另外,要弄清结点next添加的顺序,是先把结点挂在后移结点呢,还是先让前面的连起来,理解了这个,对链表也掌握的大部分。
alright,no 代码 you say 个 jb,小二上代码。代码有详细的注释,可以方便我们理解,在看的同时不要忘了自己动手操作一遍哦
package com.codingboy.linkedlist;
/**
* @author: ljl
* @date: 2020/8/5 21:46
* @description:
*/
public class SingleLinkedListDemo {
public static void main(String[] args) {
SingleLinkedList heroList = new SingleLinkedList();
HeroNode hero1 = new HeroNode(1,"宋江","及时雨");
HeroNode hero2 = new HeroNode(2,"卢俊义","玉麒麟");
HeroNode hero3 = new HeroNode(3,"吴用","智多星");
HeroNode hero4 = new HeroNode(4,"公孙胜","入云龙");
heroList.addByOrder(hero1);
heroList.addByOrder(hero4);
heroList.addByOrder(hero3);
heroList.addByOrder(hero2);
heroList.list();
/* HeroNode newHero = new HeroNode(2,"小卢","阿玉");
heroList.update(newHero);
System.out.println("---修改过后----");
heroList.list();*/
System.out.println("--删除过后--");
heroList.delete(1);
heroList.delete(2);
heroList.delete(3);
heroList.delete(4);
heroList.list();
}
}
class SingleLinkedList{
//创建头结点
private HeroNode head = new HeroNode(0,"","");
//添加元素
//思路,找到next为null的结点,然后把元素添加到这个结点的后面
public void add(HeroNode heroNode){
//因为头结点不能动,所以我们找一个辅助的变量
HeroNode temp = head;
//找next==null的结点
while(temp.next != null ){
temp = temp.next;
}
temp.next = heroNode;
return;
}
//根据英雄的排名插入到指定位置
//如果有这个排名,则添加失败,并给出提示
public void addByOrder(HeroNode heroNode){
//因为头结点不能动,所以找一个辅助指针(变量)来帮助我们找到添加位置
//因为是单链表,因此我们找的temp是位于添加位置的前一节点,否则插入不了
HeroNode temp = head;
boolean flag = false; //表明排名是否已经存在,默认为false
while(true) {
if(temp.next == null) {
//说明链表已经走到最后了
break;
}
//注意这里为什么要用大于号,我们排队的时候一个一个找,
//肯定是找到后面比我高的地方插进去,而不是看见前面比我矮就插进去
if(temp.next.no > heroNode.no){
//说明已经找到了
break;
}else if (temp.next.no == heroNode.no){
//说明已经存在该编号
flag = true;
break;
}
//没有找到,继续往下找
temp = temp.next;
}
if(flag){
System.out.println("已经存在该英雄,插入失败");
}else{
//先将英雄结点挂上去,在修改temp
heroNode.next = temp.next;
temp.next = heroNode;
}
}
//修改结点的信息,根据no编号来修改,即no编号不能改
public void update(HeroNode heroNode){
//判断是否为空
if(head.next == null) {
System.out.println("链表为空");
return;
}
//辅助变量帮助我们找结点
HeroNode temp = head.next;
boolean flag = false;//表示是否找到
while(true) {
if(temp == null) {
//说明已经遍历完,没有找到
break;
}
if(temp.no == heroNode.no) {
flag = true;
break;
}
temp = temp.next;
}
if(!flag) {
System.out.println("没有找到该结点");
return;
}else {
temp.name = heroNode.name;
temp.nickName = heroNode.nickName;
}
}
public void delete(int no) {
//判断链表是否为空
if(head.next == null) {
System.out.println("链表为空");
return;
}
HeroNode temp = head;
boolean flag = false;//是否找到要删除的结点
while(true) {
if(temp.next == null){//已经到了链表的最后
break;
}
//要找到删除结点的前一个元素
if(temp.next.no == no){
flag = true;
break;
}
temp = temp.next;
}
if(flag) {
//可以删除
temp.next = temp.next.next;
}else {
System.out.println("要删除的结点不存在");
}
}
//遍历链表
public void list(){
//遍历之前先判断是否为空
if(head.next == null){
System.out.println("链表为空");
return;
}
//头结点不能动,找一个辅助,注意这里与上面的区别,上面是temp=next
HeroNode temp = head.next;
while (temp != null){
System.out.println(temp);
temp = temp.next;
}
return;
}
}
//水浒英雄
class HeroNode{
public int no;//排名
public String name;//姓名
public String nickName;//外号
HeroNode next;
//alt+insert 快捷键生成构造器
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 + '\'' +
'}';
}
}
(三)双向链表
单向链表有很多不方便的地方,所以双向链表脱颖而出。
- 单向链表查找的方向只能是一个方向,而双向链表可以向前向后查找。
- 单向链表不能自我删除,需要靠辅助结点,(temp是待删除结点的前一个结点)而双向链表可以自我删除。
- 双向链表除了单向链表所具有的next,还有pre域
双向链表的遍历,添加,修改,删除的思路:
- 遍历:和单链表一样,既可以向前,也可以向后
- 添加(默认添加尾部):
- 找到尾部
- temp.next = newNode
- newNode.pre = temp
- 修改:修改的思路与单链表一样
- 删除:我们不需要像单链表一样找到要删除结点的前一个结点,而是要身处结点本身
1) 找到要删除结点本身
2) temp.pre.next = temp.next
3) temp.next.pre = temp.pre
注意这里假如要删除最后一个结点的话,java会报空指针异常,因为temp.next=null,null.pre就会出错。所以这里要判断一下是否为空。
(四)单向环形链表(约瑟夫问题)
说起Joseph问题,我好像在学校稀里糊涂的就没学明白,可能当初没有这个心思去想,那么如今可不能懒惰了,让我们勇敢的去面对吧。
Josephu(约瑟夫、约瑟夫环) 问题
Josephu 问题为:设编号为1,2,… n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m 的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。
提示:用一个不带头结点的循环链表来处理Josephu 问题:先构成一个有n个结点的单循环链表,然后由k结点起从1开始计数,计到m时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从1开始计数,直到最后一个结点从链表中删除算法结束。
单向环形列表:
创建一个单向环形链表思路
1.先创建一个结点,first指向该结点,然后构成一个环
2.每当创建一个新的结点,就把该结点加入到环中
这里需要注意的是,first的作用是告诉我们第一个结点在哪,然后添加的新结点连到first才能构成环,然后curBoy的作用是代表当前结点,用来连接新的结点。
遍历环形链表思路:
1.使用一个辅助指针(变量)curBoy指向first
2.while循环遍历环形链表,假如curBoy.next=first则结束
约瑟夫问题求解思路:
根据用户的输入,输出一个出圈的顺序
n=5,有五个人
k=1,从第一个人开始数
m=2,数两下
- 这里需要两个辅助指针,first(指向第一个结点),helper(指向最后一个结点,即第一个结点前面一个结点,因为是环形的)
我们有疑惑了,为什么会无缘无故需要一个helper指针呢?其实这里的helper是帮助小孩出圈的,
补充: 在移动之前,先让first和helper移动k-1次,找到开始报数的那个小孩。 - 当小孩报数时,让first和helper同时移动m-1次
- 这时将first指向的小孩出圈,就1中所问如何出圈,so easy
first=first.next,helper.next=first。
代码实现,略微有些复杂
package com.codingboy.linkedlist;
import sun.awt.windows.ThemeReader;
/**
* @author: ljl
* @date: 2020/8/6 20:14
* @description:
*/
public class Josephu {
public static void main(String[] args) {
SingleCircleLinkedList circle = new SingleCircleLinkedList();
circle.addBoy(5);
//测试遍历
circle.show();
//测试约瑟夫问题
circle.josephu(1,2,5);
}
}
//单向环形链表
class SingleCircleLinkedList {
private Boy first = null;
//构造环形链表
public void addBoy(int nums) {
if(nums < 1) {
System.out.println("你输入的数字无效,无法构成环形链表");
return;
}
//创建一个辅助变量,来表示当前结点
Boy curBoy = null;
for(int i = 1;i <= nums;i++) {
//根据编号,创建小孩结点
Boy boy = new Boy(i);
if (i == 1) {
first = boy;
first.setNext(first);
}else {
//先像单链表一样将结点在尾部挂上去
curBoy.setNext(boy);
//与头连起来
boy.setNext(first);
}
curBoy = boy;
}
}
//遍历环形链表
public void show() {
if(first == null) {
System.out.println("链表为空");
return;
}
//因为first不能动,所以使用辅助变量帮助我们遍历
Boy curBoy = first;
while(true) {
System.out.println(curBoy.getNo());
if(curBoy.getNext() == first) {//遍历完了
break;
}
curBoy = curBoy.getNext();
}
}
//解决约瑟夫问题,start从哪开始数,count数几个,nums几个人
public void josephu(int start, int count, int nums) {
//先判断输入的数据是否有问题
if(first == null || start < 1 || start > nums) {
System.out.println("参数有误");
return;
}
//1.找到helper指针,helper在first前面,但是由于是单向循环链表,所以要绕一大圈
Boy helper = first ;
while(helper.getNext() != first) {
helper = helper.getNext();
}
//2.helper和first移动要第一个报数的boy
for(int i = 0;i < start - 1;i++) {
first = first.getNext();
helper = helper.getNext();
}
//3.开始报数,移动count -1 次
while(true) {
//假如只有一个结点,则结束循环
if(first == helper) {
break;
}
//移动count-1次
for(int i = 0;i < count - 1;i++) {
first = first.getNext();
helper = helper.getNext();
}
System.out.println("要出圈的男孩的标号为:" + first.getNo());
//移动好了,就要出圈了
first = first.getNext();
helper.setNext(first);
}
System.out.println("最后剩下的男孩的标号为:" + first.getNo());
}
}
//创建男孩当做结点
class Boy {
private int no;
private Boy next;
public Boy(int no) {
this.no = no;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public Boy getNext() {
return next;
}
public void setNext(Boy next) {
this.next = next;
}
@Override
public String toString() {
return "Boy{" +
"no=" + no +
'}';
}
}
三、结束语
终于把链表搞定了,so tired,昨天跟学弟学妹说今天要搞定链表,哥说到做到,不知不觉竟然学到了十点,crazy。
最后在说一下,代码一定一定一定要自己敲一遍,这感觉完全不一样。