链表(Linked List)
链表是有序的列表,但是它在内存中是存储如下
介绍
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含data域,next域:指向下一个节点.
- 如图:发现链表的各个节点不一定是连续存储
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
1.单链表
单链表(带头结点)逻辑结构示意图如下
1.1单链表的创建和遍历
- 添加
- 先创建一个head头节点,作用就是表示单链表的头
- 后面我们每添加一个节点,就直接加入到链表的最后
- 遍历
- 通过一个辅助变量遍历,帮助遍历整个链表
代码实现
要创建两个对象一个是节点对象,一个是链表对象
做add添加时,先找到链表的最后,如果这个链表没有最后,那么我们加入的这个node节点就是这次的头指针指向下一个节点
publicclassSingleLinkedListDemo { publicstaticvoidmain(String[] args) { //进行测试//先创建节点HeroNodehero1=newHeroNode(1, "宋江", "及时雨"); HeroNodehero2=newHeroNode(2, "卢俊义", "玉麒麟"); HeroNodehero3=newHeroNode(3, "吴用", "智多星"); HeroNodehero4=newHeroNode(4, "林冲", "豹子头"); //创建一个链表SingleLinkedListsingleLinkedList=newSingleLinkedList(); //加入singleLinkedList.add(hero1); singleLinkedList.add(hero2); singleLinkedList.add(hero3); singleLinkedList.add(hero4); singleLinkedList.list(); } } //定义SingleLinkedList 管理我们的HeroclassSingleLinkedList{ //先初始化一个头节点,头节点不要动,不存放具体的数据privateHeroNodehead=newHeroNode(0,"",""); //返回头节点publicHeroNodegetHead(){ returnhead; } //添加节点到单向链表//思路:当不考虑编号顺序时//1.找到当前链表的最后节点//2.将最后这个节点的next 指向新的节点publicvoidadd(HeroNodeheroNode){ //因为head节点不能动,因此我们需要一个辅助变量tempHeroNodetemp=head; //遍历链表.找到最后while(true){ //找到链表的最后if(temp.next==null){ break; } //如果没有找到最后,将temp后移temp=temp.next; } //当退出while循环时,temp就指向了链表的最后//将最后这个节点的next指向新的节点temp.next=heroNode; } //显示链表[遍历]publicvoidlist(){ //判断链表是否为空if (head.next==null){ System.out.println("链表为空"); return; } //因为头节点不能动,因此我们需要一个辅助变量来遍历HeroNodetemp=head.next; while(true){ //判断是否到链表最后if (temp==null){ break; } //输出节点信息System.out.println(temp); //将temp后移,一定注意temp=temp.next; } } } //定义一个HeroNode,每个HeroNode对象就是一个节点classHeroNode{ publicintno; publicStringname; publicStringnickName; publicHeroNodenext;//指向下一个节点publicHeroNode(intno, Stringname, StringnickName) { this.no=no; this.name=name; this.nickName=nickName; } //为了显示方法,我们重新toStringpublicStringtoString() { return"HeroNode{"+"no="+no+", name='"+name+'\''+", nickName='"+nickName+'\''+'}'; } }
1.2按顺序插入
代码实现
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置publicvoidaddByOrder(HeroNodeheroNode){ //因为头节点不能动,因此我们仍然通过一个辅助指针(变量)来帮助找到添加的位置//因为单链表,因此我们找的temp是位于添加位置的前一个节点,否则插入不了HeroNodetemp=head; booleanflag=false; //flag标志添加的编号是否存在,默认为falsewhile(true){ if (temp.next==null) break; //说明temp已经在链表最后if (temp.next.no>heroNode.no){ //位置找到,就在temp的后面插入break; }elseif (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来修改即可publicvoidupdate(HeroNodenewHeroNode){ //判断是否为空if (head.next==null){ System.out.println("链表为空"); return; } //找到需要修改的节点,根据no编号//定义一个铺助变量HeroNodetemp=head.next; booleanflag=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代码实现
publicclassDoubleLinkedListDemo { publicstaticvoidmain(String[] args) { System.out.println("双向链表的测试"); HeroNode2hero1=newHeroNode2(1, "宋江", "及时雨"); HeroNode2hero2=newHeroNode2(2, "卢俊义", "玉麒麟"); HeroNode2hero3=newHeroNode2(3, "吴用", "智多星"); HeroNode2hero4=newHeroNode2(4, "林冲", "豹子头"); //创建一个双向链表DoubleLinkedListdoubleLinkedList=newDoubleLinkedList(); doubleLinkedList.add(hero1); doubleLinkedList.add(hero2); doubleLinkedList.add(hero3); doubleLinkedList.add(hero4); doubleLinkedList.list(); //修改HeroNode2newHeroNode=newHeroNode2(4,"公孙胜","入云龙"); doubleLinkedList.update(newHeroNode); System.out.println("修改后的链表情况"); doubleLinkedList.list(); //删除doubleLinkedList.del(3); System.out.println("删除后的链表情况"); doubleLinkedList.list(); } } //创建一个双线链表的类classDoubleLinkedList{ //先初始化一个头节点,头节点不要动,不存放具体的数据privateHeroNode2head=newHeroNode2(0,"",""); //返回头节点publicHeroNode2getHead(){ returnhead; } //遍历双向链表的方法//显示链表[遍历]publicvoidlist(){ //判断链表是否为空if (head.next==null){ System.out.println("链表为空"); return; } //因为头节点不能动,因此我们需要一个辅助变量来遍历HeroNode2temp=head.next; while(true){ //判断是否到链表最后if (temp==null){ break; } //输出节点信息System.out.println(temp); //将temp后移,一定注意temp=temp.next; } } //添加一个节点到双向链表的最后publicvoidadd(HeroNode2heroNode){ //因为head节点不能动,因此我们需要一个辅助变量tempHeroNode2temp=head; //遍历链表.找到最后while(true){ //找到链表的最后if(temp.next==null){ break; } //如果没有找到最后,将temp后移temp=temp.next; } //当退出while循环时,temp就指向了链表的最后//形成一个双向链表temp.next=heroNode; heroNode.pre=temp; } //修改一个节点的内容,可以看到双向链表的节点内容修改和单向链表一样//知识节点类型改成了HeroNode2publicvoidupdate(HeroNode2newHeroNode){ //判断是否为空if (head.next==null){ System.out.println("链表为空"); return; } //找到需要修改的节点,根据no编号//定义一个铺助变量HeroNode2temp=head.next; booleanflag=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 找到后,自我删除即可publicvoiddel(intno){ //判断当前链表是否为空if (head.next==null){ System.out.println("链表为空,无法删除"); return; } HeroNode2temp=head.next;//辅助变量(指针)booleanflag=false; //标志是否找到待删除节点while(true){ if (temp==null) break;//已经到链表的最后if (temp.no==no){ //找到了待删除的节点的前一节点tempflag=true; break; } temp=temp.next; //temp后移,遍历 } //判断flagif (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对象就是一个节点classHeroNode2{ publicintno; publicStringname; publicStringnickName; publicHeroNode2next;//指向下一个节点,默认为nullpublicHeroNode2pre;//指向前一个节点,默认为nullpublicHeroNode2(intno, Stringname, StringnickName) { this.no=no; this.name=name; this.nickName=nickName; } //为了显示方法,我们重新toStringpublicStringtoString() { 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代码实现(构建环形链表和遍历)
publicclassJosephus { publicstaticvoidmain(String[] args) { //测试一把看看构建环形链表,和遍历是否QkCircleSingleLinkedListcircleSingleLinkedList=newCircleSingleLinkedList(); circleSingleLinkedList.addBoy(5); circleSingleLinkedList.showBoy(); } } //创建一个环形单向链表classCircleSingleLinkedList{ //创建一个first节点,当前没有编号的privateBoyfirst=null; //添加小孩的节点,构建成一个环形链表publicvoidaddBoy(intnums){ //nums 做一个数据校验if (nums<1){ System.out.println("nums的值不正确"); return; } BoycurBoy=null;//辅助指针,帮助构建环形链表//使用for循环来创建环形链表for (inti=1; i<=nums; i++) { //根据编号,创建小孩节点Boyboy=newBoy(i); //如果是第一个小孩if (i==1){ first=boy; first.setNext(first); //构成环curBoy=first;//让curBoy指向第一个小孩 }else { curBoy.setNext(boy); boy.setNext(first); curBoy=boy; } } } //遍历当前环形链表publicvoidshowBoy(){ //判断链表是否为空if (first==null){ System.out.println("链表为空,没有任何小孩"); return; } //因为first不能动,因此我们仍然使用一个辅助指针完成遍历BoycurBoy=first; while (true){ System.out.printf("小孩的编号%d\n",curBoy.getNo()); if (curBoy.getNext()==first){//说明已经遍历完毕break; } curBoy=curBoy.getNext(); //curBoy后移 } } } //创建一个Boy类,表示一个节点classBoy{ privateintno;//编号privateBoynext;//指向下一个节点,默认nullpublicBoy(intno) { this.no=no; } publicintgetNo() { returnno; } publicvoidsetNo(intno) { this.no=no; } publicBoygetNext() { returnnext; } publicvoidsetNext(Boynext) { this.next=next; } } 复制代码
3.4代码实现(出圈操作)
1.需求创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点 补充:小孩报数前,先让first和helper移动k-1次
2.当小孩报数时,让first和helper指针同时的移动m-1次
3.这时就可以将first指向的小孩节点出圈 first = first.next helper.next=first 原来fist指向的节点就没有任何引用,就会被回收
//根据用户的输入,计算小孩出圈的顺序/*** @param startNo 表示从第几个小孩开始数数* @param countNum 表示数几下* @param nums 表示最初有多少小孩在圈中*/publicvoidcountBoy(intstartNo, intcountNum, intnums) { //先对数据进行校验if (first==null||startNo<1||startNo>nums) { System.out.println("参数输入有误,请重新输入"); return; } //创建辅助指针,帮助小孩 出圈Boyhelper=first; //需求创建一个辅助指针(变量)helper,事先应该指向环形链表的最后这个节点while (true) { if (helper.getNext() ==first) {//说明helper指向最后小孩节点break; } helper=helper.getNext(); } //小孩报数前,先让first和helper移动k-1次for (inti=0; i<startNo-1; i++) { first=first.getNext(); helper=helper.getNext(); } //当小孩报数时,让first和helper指针同时的移动m-1次,然后出圈//这里是一个循环操作,知道圈中只有一个节点while(true){ if (helper==first){//说明圈中只有一个节点break; } //让first和helper指针同时移动countNum-1for (inti=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.代码实现
publicclassArrayStackDemo { publicstaticvoidmain(String[] args) { //测试一下ArrayStack//先创建一个Arraystack对象->表示栈ArrayStackstack=newArrayStack(4); Stringkey=""; booleanloop=true;//控制是否退出菜单Scannerscanner=newScanner(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("请输入一个数字"); intval=scanner.nextInt(); stack.push(val); break; case"pop": try{ intres=stack.pop(); System.out.printf("出栈的数据是%d\n",res); }catch (Exceptione){ System.out.println(e.getMessage()); } break; case"exit": scanner.close(); loop=false; break; default: break; } } System.out.println("程序退出了"); } } //定义一个ArrayStack 表示栈classArrayStack{ privateintmaxSize;//栈的大小privateint[] stack;//数组.数组模拟栈.数据就放在该数组中privateinttop=-1;//top表示栈顶,初始化位-1publicArrayStack(intmaxSize) { this.maxSize=maxSize; stack=newint[this.maxSize]; } //栈满publicbooleanisFull(){ returntop==maxSize-1; } //栈空publicbooleanisEmpty(){ returntop==-1; } //入栈-pushpublicvoidpush(intvalue){ //先判断栈是都满if (isFull()){ System.out.println("栈满"); return; } top++; stack[top]=value; } //出栈-pop,将栈顶的数据返回publicintpop(){ //先判断栈是否空if (isEmpty()){ //抛出异常thrownewRuntimeException("栈空,没有数据"); } intvalue=stack[top]; top--; returnvalue; } //遍历栈的情况 遍历时,需要从栈顶开始显示数据publicvoidlist(){ if (isEmpty()){ System.out.println("栈空,没有数据"); return; } for (inti=top; i>=0 ; i--) { System.out.printf("stack[%d]=%d\n",i,stack[i]); } } }