写点什么

Qz 学算法 - 数据结构篇 (链表、栈)

作者:浅辄
  • 2023-04-22
    吉林
  • 本文字数:10330 字

    阅读完需:约 34 分钟

链表(Linked List)

链表是有序的列表,但是它在内存中是存储如下



介绍

  • 链表是以节点的方式来存储,是链式存储

  • 每个节点包含 data 域,next 域:指向下一个节点.

  • 如图:发现链表的各个节点不一定是连续存储

  • 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定

1.单链表

单链表(带头结点)逻辑结构示意图如下



1.1 单链表的创建和遍历


  • 添加

  • 先创建一个 head 头节点,作用就是表示单链表的头

  • 后面我们每添加一个节点,就直接加入到链表的最后

  • 遍历

  • 通过一个辅助变量遍历,帮助遍历整个链表

代码实现

要创建两个对象一个是节点对象,一个是链表对象


做 add 添加时,先找到链表的最后,如果这个链表没有最后,那么我们加入的这个 node 节点就是这次的头指针指向下一个节点


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.add(hero1); singleLinkedList.add(hero2); singleLinkedList.add(hero3); singleLinkedList.add(hero4); singleLinkedList.list();
}}
//定义SingleLinkedList 管理我们的Heroclass 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){ break; } //如果没有找到最后,将temp后移 temp = temp.next; } //当退出while循环时,temp就指向了链表的最后 //将最后这个节点的next指向新的节点 temp.next = heroNode; }
//显示链表[遍历] 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; } }
}
//定义一个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 + '\'' + '}'; }}
复制代码

1.2 按顺序插入

代码实现

//第二种方式在添加英雄时,根据排名将英雄插入到指定位置public void addByOrder(HeroNode heroNode){    //因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置    //因为单链表,因此我们找的temp是位于添加位置的前一个节点,否则插入不了    HeroNode temp = head;    boolean flag = false; //flag标志添加的编号是否存在,默认为false    while(true){        if (temp.next == null) break;  //说明temp已经在链表最后        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{        //插入到链表中,temp后面        heroNode.next = temp.next;        temp.next = heroNode;    }}
复制代码


    heroNode.next = temp.next;        temp.next = heroNode;
复制代码


这里我们把 hero.next=temp.next 操作是因为,我们要插入一个节点,那么就需要让索引后移一维,即插入位置的后一个节点指向临时变量节点指向的后一个节点,临时变量指向的节点等于要插入的节点


我们按照 1423 的顺序插入


singleLinkedList.addByOrder(hero1);singleLinkedList.addByOrder(hero4);singleLinkedList.addByOrder(hero2);singleLinkedList.addByOrder(hero3);
复制代码



1.3 修改节点信息

代码实现

//修改节点信息  根据no编号来修改,即no编号不能改,//说明//1. 根据根据newHeroNode的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.printf("没有找到编号%d的节点",newHeroNode.no);
}}
复制代码



1.4 删除节点

思路


  • 我们先找到需要删除的这个节点的前一个节点 temp

  • temp.next =temp.next.next

  • 被删除的节点,将不会有其它引用指向,会被垃圾回收机制回收

代码实现

//删除节点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.next;   //temp后移,遍历    }    //判断flag    if (flag){//找到        //可以删除        temp.next = temp.next.next;    }else {        System.out.printf("要删除的%d节点不存在",no);    }}
复制代码

2.双向链表

2.1 单链表的缺点

  • 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。

  • 单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除,所以前面我们单链表删除时节点,总是找到 temp 的下一个节点来删除的

2.2 双向链表的操作思路

  • 遍历方式和单链表一样,只是可以向前查找.也可以向后查找

  • 添加(默认添加到双线链表的最后)

  • 先找到双线链表的最后这个节点

  • temp.next = newHeroNode

  • newHeroNode.pre=temp

  • 修改思路和原来的单向链表一样

  • 删除

  • 因为是双向链表,因此,我么可以实现自我删除某个节点

  • 直接找到要删除的这个节点,比如 temp

  • temp.pre.next = temp.next

  • temp.next.pre = temp.pre

2.3 代码实现

public class DoubleLinkedListDemo {    public static void main(String[] args) {        System.out.println("双向链表的测试");        HeroNode2 hero1 = new HeroNode2(1, "宋江", "及时雨");        HeroNode2 hero2 = new HeroNode2(2, "卢俊义", "玉麒麟");        HeroNode2 hero3 = new HeroNode2(3, "吴用", "智多星");        HeroNode2 hero4 = new HeroNode2(4, "林冲", "豹子头");        //创建一个双向链表        DoubleLinkedList doubleLinkedList = new DoubleLinkedList();        doubleLinkedList.add(hero1);        doubleLinkedList.add(hero2);        doubleLinkedList.add(hero3);        doubleLinkedList.add(hero4);
doubleLinkedList.list();
//修改 HeroNode2 newHeroNode = new HeroNode2(4,"公孙胜","入云龙"); doubleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况"); doubleLinkedList.list();
//删除 doubleLinkedList.del(3); System.out.println("删除后的链表情况"); 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 = temp.next; } }
//添加一个节点到双向链表的最后 public void add(HeroNode2 heroNode){
//因为head节点不能动,因此我们需要一个辅助变量temp HeroNode2 temp = head; //遍历链表.找到最后 while(true){ //找到链表的最后 if(temp.next==null){ break; } //如果没有找到最后,将temp后移 temp = temp.next; } //当退出while循环时,temp就指向了链表的最后 //形成一个双向链表 temp.next = heroNode; heroNode.pre = temp; }
//修改一个节点的内容,可以看到双向链表的节点内容修改和单向链表一样 //知识节点类型改成了HeroNode2 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.printf("没有找到编号%d的节点",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){ //找到了待删除的节点的前一节点temp flag = true; break; } temp = temp.next; //temp后移,遍历 } //判断flag 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节点不存在",no); } }}
//定义一个HeroNode2,每个HeroNode对象就是一个节点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 + '\'' + '}'; }
复制代码

3.环形链表和约瑟夫问题

3.1 引入

Josephus(约瑟夫、约瑟夫环)问题


Josephus 问题为:设编号为 1,2,…n 的 n 个人围坐一圈,约定编号为(1<=k<=n)的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列,依次类推,直到所有人出列为止,由此产生一个出队编号的序列。


提示:


用一个不带头结点的循环链表来处理 Josephus 问题:先构成一个有个结点的单循环链表,然后由结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中册则除算法结束。




3.2 思路

构建一个单向的环形链表思路


  • 先创建第一个节点,让 first 指向该节点,并形成环形

  • 后面当我们每创建一个新的节点,就把该节点,加入到已有的环形链表中即可


遍历环形链表


  • 先让一个辅助指针(变量)curBoy,指向 first 节点

  • 然后通过一个 while 循环遍历该环形链表即可 curBoy.next=first 结束

3.3 代码实现(构建环形链表和遍历)

public class Josephus {    public static void main(String[] args) {        //测试一把看看构建环形链表,和遍历是否Qk        CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();        circleSingleLinkedList.addBoy(5);        circleSingleLinkedList.showBoy();    }}
//创建一个环形单向链表class CircleSingleLinkedList{ //创建一个first节点,当前没有编号的 private Boy first = null; //添加小孩的节点,构建成一个环形链表 public void addBoy(int nums){ //nums 做一个数据校验 if (nums<1){ System.out.println("nums的值不正确"); return; }
Boy curBoy = null;//辅助指针,帮助构建环形链表
//使用for循环来创建环形链表 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(); //curBoy后移 } }
}

//创建一个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; }}
复制代码

3.4 代码实现(出圈操作)

1.需求创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点补充:小孩报数前,先让 first 和 helper 移动 k-1 次 2.当小孩报数时,让 first 和 helper 指针同时的移动 m-1 次 3.这时就可以将 first 指向的小孩节点出圈 first = first.nexthelper.next=first 原来 fist 指向的节点就没有任何引用,就会被回收


    //根据用户的输入,计算小孩出圈的顺序
/** * @param startNo 表示从第几个小孩开始数数 * @param countNum 表示数几下 * @param nums 表示最初有多少小孩在圈中 */ public void countBoy(int startNo, int countNum, 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 i = 0; i < startNo-1; i++) { first = first.getNext(); helper = helper.getNext(); } //当小孩报数时,让first和helper指针同时的移动m-1次,然后出圈 //这里是一个循环操作,知道圈中只有一个节点 while(true){ if (helper==first){//说明圈中只有一个节点 break; } //让first和helper指针同时移动countNum-1 for (int i = 0; i < countNum-1; i++) { 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()); }}
复制代码

需求引入

计算式:[7*2*2-5+1-5+3-3]


请问:计算机底层是如何运算得到结果的?注意不是简单的把算式列出运算,因为我们看这个算式 7*2*2-5,但是计算机怎么理解这个算式的对计算机而言,它接收到的就是一个字符串),我们讨论的是这个问题。=>栈

1.介绍

  • 栈的英文为(stack)

  • 栈是一个先入后出(FILO-First In LastOut))的有序列表。

  • 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另端为固定的一端,称为栈底(Bottom)。

  • 根据栈的定义可知,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除

  • 出栈和入栈的概念(如图所示)


2.应用场景

  • 子程序的调用:在跳往子程序前,会先将下个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。

  • 处理递归调用:和子程序的调用类似,只是除了储存下一个指令的地址外,也将参数、区域变量等数据存入堆栈中。

  • 表达式的转换[中缀表达式转后缀表达式]与求值(实际解决)。

  • 二叉树的遍历。

  • 图形的深度优先(depth 一 first)搜索法:

3.思路分析


  • 使用数组来模拟栈

  • 定义一个 top 来表示栈顶,初始化为 1

  • 入栈的操作,当有数据加入到栈时,top++;stack[top]=data;

  • 出栈的操作,int value=stack[top];top--,return value;

4.代码实现

public class ArrayStackDemo {    public static void main(String[] args) {        //测试一下ArrayStack        //先创建一个Arraystack对象->表示栈        ArrayStack stack = new ArrayStack(4);        String key="";        boolean loop = true;//控制是否退出菜单        Scanner scanner = new Scanner(System.in);        while(loop){            System.out.println("show:表示显示栈");            System.out.println("exit:退出程序");            System.out.println("push:添加数据到栈(入栈)");            System.out.println("show:从栈取出数据(出栈)");            System.out.println("请输入你的选择");            key = scanner.next();            switch (key){                case "show":                    stack.list();                    break;
case "push": System.out.println("请输入一个数字"); int val = scanner.nextInt(); stack.push(val); break; case "pop": try{ int res = stack.pop(); System.out.printf("出栈的数据是%d\n",res); }catch (Exception e){ System.out.println(e.getMessage()); } break; case "exit": scanner.close(); loop = false; break; default: break; } } System.out.println("程序退出了"); }}//定义一个ArrayStack 表示栈class ArrayStack{ private int maxSize;//栈的大小 private int[] stack;//数组.数组模拟栈.数据就放在该数组中 private int top = -1;//top表示栈顶,初始化位-1
public ArrayStack(int maxSize) { this.maxSize = maxSize; stack = new int[this.maxSize]; }
//栈满 public boolean isFull(){ return top==maxSize-1; }
//栈空 public boolean isEmpty(){ return top==-1; }
//入栈-push public void push(int value){ //先判断栈是都满 if (isFull()){ System.out.println("栈满"); return; } top++; stack[top]=value; }
//出栈-pop,将栈顶的数据返回 public int pop(){ //先判断栈是否空 if (isEmpty()){ //抛出异常 throw new RuntimeException("栈空,没有数据"); } int value = stack[top]; top--; return value; }
//遍历栈的情况 遍历时,需要从栈顶开始显示数据 public void list(){ if (isEmpty()){ System.out.println("栈空,没有数据"); return; } for (int i = top; i >=0 ; i--) { System.out.printf("stack[%d]=%d\n",i,stack[i]); } }
}
复制代码


发布于: 刚刚阅读数: 3
用户头像

浅辄

关注

大丈夫生于天地之间,岂能郁郁久居人之下 2022-11-08 加入

Java、Spider、Go

评论

发布
暂无评论
Qz学算法-数据结构篇(链表、栈)_数据结构_浅辄_InfoQ写作社区