1.链表介绍
链表是有序的列表,但是他在内存中并不一定是连续的。链表是以节点的方式进行存储,是链式存储。每一个节点包括data域和next域,next域指向下一个节点。链表分为带头节点的链表和没有头结点的链表,根据实际的需求来确定。
其中单链表的逻辑示意图如下:
2.单链表
2.1单链表的应用实例
使用带head头的单项链表排行榜完成对水浒英雄任务的增删改查操作。
2.1.1第一种方法在添加英雄时,直接添加到链表的尾部
2.1.2第二种方式在添加英雄时,根据排名插入到指定位置
2.1.3修改节点的功能
思路一,先找到该节点,然后遍历,然后修改就中了。
2.1.4删除节点
思路分析示意图:
2.1.5代码
//定义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;
}
//为了显示方便
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
//定义SingleLinkedList管理我们的英雄
class SingleLinkedList{
public HeroNode getHead() {
return head;
}
public void setHead(HeroNode head) {
this.head = head;
}
//初始化一个头结点,头结点不要动
private HeroNode head=new HeroNode(0,"","");
//添加节点到单向链表
//1、找到当前链表最后节点
//2、将最后这个节点的next指向新的节点
public void add(HeroNode heroNode){
//因为头结点不能动,所以我们需要一个辅助遍历temp
HeroNode temp=head;
//遍历链表,找到最后
while (true){
//
if (temp.next==null){
break;
}
//如果没有找到最后,就将temp后移动
temp=temp.next;
}
//当退出while循环时候,temp就指向了链表最后
//将这个节点的next指向新的节点
temp.next=heroNode;
}
//第二种方式再添加英雄时候,根据排名将英雄插入到指定位置
public void addByOrder(HeroNode heroNode){
//因为头结点不能动,所以我们通过辅助变量帮助找到添加的位置
//我们找的temp位于天啊及位置的前一个节点,否则插入不了
HeroNode tempt=head;
//标识添加的编号是否存在
boolean flag=false;
while (true){
if (tempt.next==null){
break;
}
if (tempt.next.no>heroNode.no){
//位置找到,就在temp后面插入
break;
}else if (tempt.next.no==heroNode.no){
flag=true;
break;
}
tempt=tempt.next;
}
//判断flag的值
if (flag){//不能添加说明标号存在
System.out.println("编号已经存在不能加入"+heroNode.no);
}else {
//插入到链表中,temp后面
heroNode.next=tempt.next;
tempt.next=heroNode;
}
}
//修改节点的信息,根据编号修改,no编号不能改
public void update(HeroNode newheroNode){
//判断是否为空
if (head.next==null){
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
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判断是否找到要修改的节点
if (flag){
temp.name=newheroNode.name;
temp.nickname=newheroNode.nickname;
}else {
//没有找到这个节点
System.out.println("没有找到编号是这个的"+newheroNode.no);
}
}
//删除节点
//思路
//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){
//找到了待删除节点的前一个节点
flag=true;
break;
}
temp=temp.next;
}
//判断flag
if (flag){//找到
//可以删除
temp.next=temp.next.next;
}else {
System.out.println("没找到");
}
}
//显示链表,通过遍历
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.next;
}
}
}
2.2单链表面试题
2.2.1求单链表有效的节点个数
//方法:获取单链表的节点个数,如果是带头节点的链表,需求不统计头结点
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;
}
2.2.2查找单链表中倒数第n个节点
//查找单链表中的倒数第k个节点
//1、编写一个方法,接收head节点,同时接收一个index
//2、index表示倒数第index个节点
//3、先把链表从头到尾遍历一下,得到链表的总的长度
//4、得到size后,从链表第一个开始遍历,遍历size-index个
//5、如果找到嘞,就返回该节点,否则返回null
public static HeroNode findLastIndexNode(HeroNode head,int index){
//1、如果链表为空,返回null
if (head.next==null){
return null;
}
//第一次遍历得到链表的长度
int size=getLength(head);
//第二次遍历,size-index位置,就是我们倒数第k个节点
//先做一个index的校验
if (index<=0 || index >size){
return null;
}
//定义一个辅助变量
HeroNode cur=head.next;
//
for (int i=0;i<size-index;i++){
cur=cur.next;
}
return cur;
}
2.2.3单链表的反转
思路分析如下:
代码如下:
//将单链表反转
public static void reversetList(HeroNode head){
//如果当前链表为空,或者只有一个节点,无需翻转直接返回
if (head.next==null||head.next.next==null){
return;
}
//定义一个辅助的指针,帮助我们遍历原来的链表
HeroNode cur=head.next;
HeroNode next=null;//定义指向当前节点的下一个节点
HeroNode reverseHead=new HeroNode(0,"","");
//遍历原来的链表
// 并从头遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead的最前端
while (cur!=null){
next=cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用
cur.next=reverseHead.next;//将cur下的一个节点指向新的链表
reverseHead.next=cur;//
cur=next;//让cur后移
}
//将head.next指向reverseHead.next,实现了单链表的反转
head.next=reverseHead.next;
}
2.2.4从尾到头打印链表
思路分析
代码实现
//可以使用栈的主句结构,将各个节点压到栈中,然后利用栈的先进后出的特点,实现了逆序打印的效果
public static void reversePrint(HeroNode head){
if (head.next==null){
return;//空链表,无法打印
}
//创建一个栈,将各节点压入栈中
Stack<HeroNode> stack=new Stack<HeroNode>();
HeroNode cur=head.next;
//将链表的所有节点压入栈中
while (cur!=null){
stack.push(cur);
cur=cur.next;
}
//将栈中的节点进行打印,pop出栈
while (stack.size()>0){
System.out.println(stack.pop());
}
}
3.双链表
3.1双向链表应用实例
3.1.1双向链表操作分析和实现
使用带head头的双向链表实现-水浒英雄榜
管理单项链表缺点分析:
- 单向链表,查找的方向只能是一个方向,但是双向链表是可以向前也可以向后查找。
- 单向链表不能自我删除,需要依赖辅助接点,但是双向链表可以自我删除
- 双向链表如何完成遍历、添加、修改和删除的思路。
1)遍历:和单链表是一样的,只是可以向前查找也可以向后查找。
2)添加:默认添加到双向链表的最后- 先找到双向链表的最后这个节点
- temp.next=newHeroNode;
- newHeroNode.pre=temp;
3)修改 思路和原来的单向链表是一样的
4)删除 - 因为是双向链表,因此我们可以实现自我删除某个节点
- 直接找到这个节点,比如temp
- temp.pre.next=temp.next
- temp.next.pre=tem.pre
双向链表的代码实现如下
public class DoubleLinkedListDemo {
public static void main(String[] args) {
//测试
System.out.println("双向链表的测试");
HeroNode2 heroNode1=new HeroNode2(1,"宋江","及时雨");
HeroNode2 heroNode2=new HeroNode2(2,"卢俊义","玉麒麟");
HeroNode2 heroNode3=new HeroNode2(3,"吴用","智多星");
//创建一个双向链表
DoubleLinkedList doubleLinkedList=new DoubleLinkedList();
doubleLinkedList.add(heroNode1);
doubleLinkedList.add(heroNode2);
doubleLinkedList.add(heroNode3);
doubleLinkedList.list();
//修改
HeroNode2 newHeroNode=new HeroNode2(3,"公孙胜","入云龙");
doubleLinkedList.update(newHeroNode);
doubleLinkedList.list();
//删除
doubleLinkedList.del(3);
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 (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=temp.next;
}
//当退出while循环时候,temp就指向了链表最后
//将这个节点的next指向新的节点
temp.next=heroNode;
heroNode.pre=temp;
}
//修改一个节点的内容
public void update(HeroNode2 newheroNode){
//判断是否为空
if (head.next==null){
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
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.println("没有找到编号是这个的"+newheroNode.no);
}
}
//从双向链表中删除一个节点
//说明
//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后移,遍历
}
//判断flag
if (flag){//找到
//可以删除
temp.pre.next=temp.next;
//如果是最后一个节点 就不需要执行下面那个
if (temp.next!=null){
temp.next.pre=temp.pre;
}
}else {
System.out.println("没找到");
}
}
}
class HeroNode2{
public int no;
public String name;
public String nickname;
public HeroNode2 next;//指向下一个节点
public HeroNode2 pre;//指向前一个节点
public HeroNode2(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 + '\'' +
'}';
}
}
4.单项环形链表
Josephu(约瑟夫)问题
设编号为1,2,3,…n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,直到m的那个人出列,他的下一位又从1开始报数,数到m的那个人又出列,以此类推,直到所有人出列为止,由此得出产生一个出队列的编号的序列。
提示:用一个不带头节点的循环链表来处理约瑟夫问题,先构成一个有n个节点的单循环链表,然后从k开始从1开始计数,记到m时,对应节点从链表删除,然后再从被删除节点的下一个节点又从1开始计数,知道最后一个节点从链表中删除,算法结束。
4.1单向环形链表介绍
4.2约瑟夫问题
- 创建链表思路图解
- 约瑟夫问题-小孩出圈的思路分析
4.3约瑟夫问题代码
public class Josepfu {
public static void main(String[] args) {
//测试一把看看构建环形链表。遍历是否ok
CircleSingleLinkedList circleSingleLinkedList=new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5);
circleSingleLinkedList.showBoy();
//测试一下小孩出圈是否正确
circleSingleLinkedList.countBoy(1,2,5);
}
}
//创建一个环形的单项链表
class CircleSingleLinkedList{
//创建一个first节点
private Boy first;
//添加节点,构建环形链表
public void addBoy(int nums){
//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);//构成环
curBoy=first;//让curBoy指向第一个小孩
}else {
curBoy.setNext(boy);
boy.setNext(first);
curBoy=boy;
}
}
}
//遍历所有的环形链表
public void showBoy(){
//判断链表是否为空
if (first==null){
System.out.println("没有任何小孩");
return;
}
//因为first不能动,因此我们依然使用一个辅助指针完成
Boy curBoy=first;
while (true){
System.out.printf("小孩的编号%d \n",curBoy.getNo());
if (curBoy.getNext()==first){//说明遍历完毕
break;
}
curBoy=curBoy.getNext();
}
}
//根据用户输入,计算出圈的顺序
/**
*
* @param startNo:表示从第几个小孩开始数
* @param count:表示数几下
* @param nums:表示最初有多少小孩在圈中
*/
public void countBoy(int startNo,int count,int nums){
//先对数据进行校验
if (first==null||startNo<1||startNo >nums){
System.out.println("参数输入有误,请从新输入");
return;
}
//创建一个辅助指针,帮助完成小孩出圈
Boy helper=first;
//需求创建一个辅助指针变量helper,实现应该形成环形链表的最后这个节点
while (true){
if (helper.getNext()==first){//说明helper指向最后的小孩节点
break;
}
helper=helper.getNext();
}
//小孩宝数千,先让first和helper移动k-1次
for (int j=0;j<startNo-1;j++){
first=first.getNext();
helper=helper.getNext();
}
//当小孩报数时,让first和helper指针同时移动m-1次
//这是一个循环的操作,直到圈中只有一个节点
while (true){
if (helper==first){//说明圈中只有一个节点
break;
}
//让first和helper同时移动countNum-1
for (int j=0;j<count-1;j++){
first=first.getNext();
helper=helper.getNext();
}
//这时,first指向的节点,就是要出圈的节点
System.out.printf("小孩%d出圈 \n",first.getNo());
//这时将first指向的小孩出圈
first=first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中的小孩编号:%d \n",first.getNo());
}
}
//创建一个boy类,表示一个节点
class Boy{
private int no;//编号
private Boy next;//指向下一个节点,默认是null
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;
}
}