单链表原理及应用举例
单链表是一种链式存取的数据结构,单链表中的数据是以结点的形式存在,每一个结点是由数据元素和下一个结点的存储的位置组成。单链表与数组相比的最大差别是:单链表的数据元素存放在内存空间的地址是不连续的,而数组的数据元素存放的地址在内存空间中是连续的,这也是为什么根据索引无法像数组那样直接就能查询到数据元素。
单链表的逻辑结构如下(带头节点):
链表是有序的列表,他在内存中的存储结构如下:
1.链表是以节点的方式来存储,是链式存储
2.每个节点都包含data域和next域:指向下一个节点
3.链表的各个节点不一定是连续存储的
4.链表分带头节点的链表和没有头节点的链表(根据实际需求来确定)
5.head节点:不存放具体的数据,作用就是表示单链表的头
应用举例
使用带头节点的单链表实现水浒英雄排名,进行基本增删改查操作
添加思路分析:
第一种:直接在链表尾部添加新英雄
第二种:通过排名(no)来指定插入英雄位置
分析示意图如下:
1.head节点不能动,因此我们需要定义一个temp辅助节点找到待添加的节点的前一个节点,通过遍历实现
2.新节点.next=temp.next
3.temp.next=新节点.next
代码实现
package com.atxihua; import java.util.Stack; public class SingleLinkedListDemo { public static void main(String[] args) { //测试 //先创建节点 HeroNode hero4=new HeroNode(4,"林冲","豹子头"); HeroNode hero3=new HeroNode(3,"吴用","智多星"); HeroNode hero2=new HeroNode(2,"卢俊义","玉麒麟"); HeroNode hero1=new HeroNode(1,"宋江","及时雨"); SingleLinkedList singleLinkedList=new SingleLinkedList(); //添加到链表 // singleLinkedList.add(hero1); // singleLinkedList.add(hero2); // singleLinkedList.add(hero3); // singleLinkedList.add(hero4); //按照编号排序添加 singleLinkedList.addByOrder(hero1); singleLinkedList.addByOrder(hero2); singleLinkedList.addByOrder(hero3); singleLinkedList.addByOrder(hero4); //显示链表 singleLinkedList.list(); //测试修改节点信息 //修改吴用的nickname HeroNode newHeroNode=new HeroNode(3,"吴用","智多星偏偏无用"); singleLinkedList.update(newHeroNode); System.out.println("修改后的链表"); singleLinkedList.list(); //删除豹子头林冲 singleLinkedList.del(4); System.out.println("删除节点后的链表"); singleLinkedList.list(); //后面的测试方法为大厂面试题 System.out.println("求单链表的节点个数"); System.out.println(singleLinkedList.getLength(singleLinkedList.getHead())); //测试一下看看是否得到了倒数第K个节点 HeroNode res=singleLinkedList.findLastKNode(singleLinkedList.getHead(),1); System.out.println(res); //反转单链表 singleLinkedList.reverseList(singleLinkedList.getHead()); singleLinkedList.list(); System.out.println("逆向打印单链表"); singleLinkedList.reversePrint(singleLinkedList.getHead()); } } //定义HeroNode,每个HeroNode对象就是一个节点 class HeroNode{ public int no; public String name; public String nickname; //指向下一个节点 public HeroNode next; //构造器 public HeroNode(int hNo,String hName,String hNickName){ this.no=hNo; this.name=hName; this.nickname=hNickName; } //为了显示方便,重写toString @Override public String toString() { return "HeroNode{" + "no=" + no + ", name='" + name + '\'' + ", nickname='" + nickname + '\'' + ", next=" + next + '}'; } } //定义一个SingleLinkedList来管理英雄 class SingleLinkedList{ //先定义一个头节点,头节点不要动,不存放具体的数据 private HeroNode head=new HeroNode(0,"",""); //添加节点到单链表中 //思路,当不考虑编号顺序时 //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 addByOrder(HeroNode heroNode){ //因为head节点不能动,因此我们需要一个辅助遍历temp HeroNode temp=head; //flag标志添加的编号是否存在,默认为false boolean flag=false; //遍历链表,找到最后一个节点 while (true){ if(temp.next==null){ //说明temp已经在链表的最后 break; } if(temp.next.no>heroNode.no){ //说明位置已找到,就在temp后面插入 break; }else if(temp.next.no==heroNode.no){ //说明该位置排名已有其他英雄,该编号已存在 flag=true; break; } //如果没有找到,将temp后移 temp=temp.next; } if(flag){//不能添加,说明编号存在 System.out.println("该英雄排名的位置已有其他英雄!!!"+heroNode.no); }else { //插入到链表中,temp的后面 heroNode.next=temp.next; temp.next=heroNode; } } //修改节点信息,根据编号no来修改,即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; } if(flag){ temp.name= newHeroNode.name; temp.nickname= newHeroNode.nickname; }else{ System.err.println("该英雄不存在!!!请检查输入排名是否正确"); } } //删除节点 //1.head不能动,因此我们需要定义一个temp辅助节点找到待删除的节点的前一个节点 //2.我们在比较时,是比较temp.next.no和需要删除节点的no public void del(int no){ //判断链表是否为空 if(head.next==null){ System.out.println("链表为空"); return; } HeroNode temp=head; //flag标志是否找到待删除的节点 boolean flag=false; //遍历链表,找到最后一个节点 while (true){ if(temp.next==null){ //说明temp已经在链表的最后 break; } if(temp.next.no>no){ //说明位置已找到 break; }else if(temp.next.no==no){ //说明找到待删除节点的前一个节点temp flag=true; break; } //如果没有找到,将temp后移,遍历 temp=temp.next; } if(flag){//找到,可以删除 temp.next=temp.next.next; }else { System.err.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=temp.next; } } public HeroNode getHead() { return head; } //求链表的有效节点个数 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; } //查找单链表中的倒数第K个节点(新浪面试题) //编写一个方法,接受head节点,同时接收一个K //先把链表从头到尾遍历,得到链表的总长度getLength //得到长度size后,我们从链表的第一个开始遍历(size-K)个就可以了 //如果找到,则返回该节点,否则返回null public static HeroNode findLastKNode(HeroNode head,int K){ //判断链表是否为空,返回null if(head.next==null){ return null;//没找到 } //遍历得到链表长度 int size=getLength(head); //第二次遍历size-K的位置 //先做校验 if(K<=0||K>size){ return null; } //定义给辅助变量,for循环定位到倒是K的位置 HeroNode cur=head.next; for(int i=0;i<size-K;i++){ cur=cur.next; } return cur; } /* 单链表的反转,腾讯面试题 * 思路: * 先定义一个节点reverseHead=new HeroNode(); * 从原来的链表从头遍历每一个节点,将其取出,并放在新的链表reverseHead的最前端 * 原来的链表的head.next=reverseHead.next * */ public static void reverseList(HeroNode head){ //如果当前链表为空,或者只有一个节点,无需反转,直接返回 if(head.next==null||head.next.next==null){ return; } //定义一个辅助变量,帮助我们遍历原链表 HeroNode cur =head.next; HeroNode next=null;//指向当前节点[cur]的下一个节点 HeroNode reverseHead=new HeroNode(0,"",""); while (cur!=null){ next=cur.next;//先暂时保存当前节点的下一个节点,因为后面需要使用 cur.next=reverseHead.next;//将cur的下一个节点指向新的链表的最前端 reverseHead.next=cur;//将cur连接到新链表上 cur=next;//让cur后移 } //将head.next指向reveseHead.next,实现单链表的反转 head.next=reverseHead.next; } /* * 从头到尾打印单链表(百度) * 思路 * 1.先将单链表进行反转操作,然后遍历即可,这样做会破坏原来的单链表结构,不建议 * 2.可以利用栈这个数据结构,将各个节点压入到栈中,利用栈先进后出的特点,就实现了逆序打印 * */ 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;//cur后移,继续压入下一节点 } //将栈中的节点进行打印,pop出栈 while (stack.size()>0){ System.out.println(stack.pop());//先进后出 } } }
结果展示:
结果与预期结果一致,达到使用单链表实现的预期结果。
后面的大厂面试题结果没有展示,只有反转单链表有点麻烦。
思路分析图如下:
代码分析
public static void reverseList(HeroNode head){ //如果当前链表为空,或者只有一个节点,无需反转,直接返回 if(head.next==null||head.next.next==null){ return; } //定义一个辅助变量,帮助我们遍历原链表 HeroNode cur =head.next; HeroNode next=null;//指向当前节点[cur]的下一个节点 HeroNode reverseHead=new HeroNode(0,"",""); while (cur!=null){ //先暂时保存当前节点的下一个节点,因为后面需要使用,此时,cur=数据2,next=数据5 next=cur.next; //将cur的下一个节点指向新的链表的最前端,此时,新的reverseHead头指针指向数据5 cur.next=reverseHead.next; //将cur连接到新链表上,此时reverHead的头指针指向数据5,在数据5之后增加了节点2,此时单链表为reverseHead=>5=>2 reverseHead.next=cur; //让cur后移,此时cur=5,重复上述操作,最终得到reverseHead=>9=>5=>2 cur=next; } //将head.next指向reveseHead.next,实现单链表的反转, // 此时reverseHead单链表已经实现反转, // 原来的单链表结构已经破坏,将原来的单链表的头指针,指向reverseHead的第一个节点,即得到反转后的单链表 head.next=reverseHead.next; }