单链表和双链表、循环链表

    科技2026-02-19  5

    1、链表(Linked List)介绍

    1.1、内存结构

    内存上来看:链表存储空间不连续(不像数组)假设有一个线性表为(a1,a2,a3,a4,a5,a6),则其对应的带有头结点的链表表示及存储结构如图所示,其中head指针的值为2020H。

    1.2 、逻辑结构

    逻辑上来看:链表属于线性结构含有n个元素的线性表通过每个结点的指针域链接成一个链表。又由于此链表的每个结点中只有一个指向后继的指针,所以称其为单链表或线性链表。下图的单链表是带有头结点的单链表,头结点的作用是方便单链表的特殊操作,简化代码。其中,头指针head指向链表的头结点,各元素结点的指针指向下一个结点,而最后一个结点的指针为“空”(NULL)。从头指针开始便可沿着链找到链表各个元素,因此可用头指针来表示一个单链表。

    2、单链表的实现

    2.1、水浒英雄榜

    单链表的应用实例使用带head头的单向链表实现 –水浒英雄排行榜管理完成对英雄人物的增删改查操作, 注: 删除和修改,查找可以考虑学员独立完成,也可带学员完成第一种方法在添加英雄时,直接添加到链表的尾部第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)

    2.2、链表节点定义

    no :英雄编号name :英雄名字nickName :英雄昵称next :指向下一个 HeroNode 节点 //定义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 + '\'' + '}'; } }

    2.3、链表定义

    Head :头结点不存放数据,仅仅作为当前链表的入口head 字段的值不能改变,一旦改变,就丢失了整个链表的入口,我们也就无法通过 head 找到链表了 /定义SingleLikedList管理英雄 class SingleLikedList{ //先初始化头结点,固定不要动,不存放数据 private HeroNode head = new HeroNode(0,"","");

    2.4、遍历链表

    定义辅助变量 temp ,相当于一个指针,指向当前节点何时遍历完成?temp == null 表明当前节点为 null ,即表示已到链表末尾如何遍历?temp = temp.next ,每次输出当前节点信息之后,temp 指针后移 //显示链表【遍历】 public void list(){ //链表先判空 if (head.next == null){ System.out.println("链表为空"); return; } //添加一个辅助的节点temp,因为head节点不能动 HeroNode temp = head.next; while (true){ if (temp == null)break; //已经遍历完退出循环 System.out.println(temp); //输出链表信息 temp = temp.next; //继续遍历,temp后移一个节点 } }

    2.5、添加节点

    代码思路

    定义辅助变量 temp ,相当于一个指针,指向当前节点如何在链表末尾插入节点?首先需要遍历链表,找到链表最后一个节点,当 temp.next == null时,temp 节点指向链表最后一个节点然后在 temp 节点之后插入节点即可:temp.next = heroNode //添加节点到单向链表,第二种方式有顺序,根据排名添加 public void addByOrder(HeroNode heroNode){ //添加一个辅助的节点temp,因为head节点不能动 //找到temp是位于添加位置的前一个节点,否则添加不了 HeroNode temp = head; boolean flag = false; //flag代表需要添加的节点是否存在,默认false while (true){ if (temp.next == null)break; //已经遍历完退出循环 if (temp.next.no > heroNode.no){ //位置找到,就在temp后面插入,temp的下一个节点和要添加的节点比较。 break; }else if (temp.next.no == heroNode.no){ flag = true; //代表需要添加的heroNode节点存在 break; } temp = temp.next; //继续遍历,temp后移一个节点 } //遍历完链表后判断flag的值 if (flag){ //不能添加,节点已经存在 System.out.printf("准备插入的英雄编号%d已经存在,不能加入\n",heroNode.no); }else { //插入到链表中,temp的后面 heroNode.next = temp.next; temp.next = heroNode; } }

    2.6、修改节点

    代码思路

    定义辅助变量 temp ,相当于一个指针,指向当前节点如何找到指定节点?temp.no = newHeroNode.no

    代码实现

    /修改节点的信息,根据no编号来修改,即no编号不能修改 //根据newHeroNode的no来修改 public void update(HeroNode newHeroNode){ //判断是否为空 if (head.next == null){ System.out.println("链表为空"); return; } //添加一个辅助的节点temp,因为head节点不能动 //根据no编号找到需要修改的节点 HeroNode temp = head.next; boolean flag = false; //flag代表需要添加的节点是否存在,默认false while (true){ if (temp == null)break; //已经遍历完退出循环 if (temp.no == newHeroNode.no){ //找到需要修改的编号 flag = true; break; } temp = temp.next; //继续遍历,temp后移一个节点 } //根据flag值判断是否找到需要修改的节点 if (flag){ temp.name = newHeroNode.name; temp.nickname = newHeroNode.nickname; }else { //没有找到 System.out.printf("没有找到编号%d的节点,不能修改\t",newHeroNode.no); } }

    2.7、删除节点

    代码思路

    定义辅助变量 temp ,相当于一个指针,指向当前节点如何找到待删除的节点?遍历链表,当 temp.next == no 时,temp.next 节点就是待删除的节点如何删除?temp = temp.next.next 即可删除 temp.next 节点,该节点没有引用指向它,会被垃圾回收机制回收

    代码实现

    //删除节点 //思路:head不能动,因此需要一个辅助节点找到待删除节点的前一个节点 //说明比较时,是temp.next.no和需要删除的节点的no比较 public void del(int no){ HeroNode temp = head; boolean flag = false; //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的节点不存在\n",no); } }

    2.9、单链表总结

    遍历链表,执行操作时,判断条件有时候是 temp ,有时候是 temp.next ,Why?对于插入、删除节点来说,需要知道当前待操作的节点(heroNode)前一个节点的地址(指针)如果直接定位至当前待操作的节点 heroNode ,那没得玩。。。因为不知道heroNode 前一个节点的地址,无法进行插入、删除操作,所以 while 循环中的条件使用 temp.next 进行判断对于更新、遍历操作来说,我需要的仅仅就只是当前节点的信息,所以 while 循环中的条件使用 temp进行判断头结点与首节点 参考资料:https://blog.csdn.net/WYpersist/article/details/80288056 头结点是为了操作的统一与方便而设立的,放在第一个元素结点之前,其数据域一般无意义(当然有些情况下也可存放链表的长度、用做监视哨等等)。 首元结点也就是第一个元素的结点,它是头结点后边的第一个结点。

    3.双链表的实现

    3.1 双向链表

    以上讨论的链表,结点中只有一个指向其后继结点的指针域next,若已知某个结点,要找其前驱结点,则只能从表头指针出发。也就是说,找后继的时间复杂度为O(1),而找前驱的时间复杂度为O(n),如果希望找前驱的时间复杂度也为O(1),则需付出空间的代价,在每个结点中再设一个指向前驱的指针域,结点结构如图所示,由这种结点组成的链表称为双向链表。

    3.2 与单向链表的比较

    单向链表, 查找的方向只能是一个方向, 而双向链表可以向前或者向后查找 单向链表不能自我删除, 需要靠辅助节点而双向链表, 则可以自我删除, 所以前面我们单链表删除时节点, 总是找到 temp ,temp 是待删除节点的前一个节点(认真体会)

    3.3 链表节点定义

    在单向链表节点的基础上,增加 pre ,用于指向前一个节点 class HeroNode { public int no; public String name; public String nickname; public HeroNode next; public HeroNode pre; //指向上一节点 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 + '\'' + '}'; } }

    3.4 遍历链表

    定义一个头结点head,作为链表的头部入口,不需要存放数据 class DoubleLikedList{ //初始化头结点,固定不动,不存放数据 private HeroNode head = new HeroNode(0,"",""); //遍历列表 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; } }

    3.5 双链表插入数据

    如何插入?temp.next 指向新的尾节点 heroNode :temp.next = heroNode;heroNode .pre 指向旧的尾节点 temp :heroNode.pre = temp;

    代码实现

    //添加节点 public void add(HeroNode heroNode){ HeroNode temp = head; while (true){ if (temp.next == null)break; temp = temp.next; } //退出while循环就代表已经遍历到链表最后 temp.next = heroNode; //下一个节点指向插入的节点 heroNode.pre = temp; //上一个节点指向temp }

    3.6 双链表删除节点

    对于双向链表,可以直接找到需要删除的节点,找到后自我删除即可

    如何删除?主要是实现上一节点和下一节点相连,注意尾节点的空指针问题temp 的前一个节点的 next 域指向 temp 的后一个节点:temp.pre.next = temp.next;temp 的后一个节点的 pre 域指向 temp 的前一个节点:temp.next.pre = temp.pre;有个地方需要注意,如果 temp 已经是链表尾节点,temp 已经没有下一个节点这时只需要将 temp 的前一个节点的 next 指向 null 即可所以 temp.next.pre = temp.pre; 执行的前提条件是 temp.next != null

    代码实现

    //删除节点 // 对于双向链表,可以直接找到需要删除的节点,找到后自我删除即可 public void del(int no){ if (head.next == null){ System.out.println("空链表,无法删除"); } HeroNode temp = head.next; //辅助变量 boolean flag = false; while (true){ if (temp == null)break; if (temp.no == no){ //找到需要删除的节点 flag = true; break; } temp = temp.next; } if (flag){ temp.pre.next = temp.next; //将上一个节点和下一个节点相连 if (temp.next != null){ //如果是最后一个节点就不需要执行这句话,否则会空指针异常 temp.next.pre = temp.pre; } }else { //没有找到 System.out.printf("要删除的%d的节点不存在\n", no); } }

    3.7. 双链表的修改

    和单链表基本类似

    代码思路

    定义辅助变量 temp ,相当于一个指针,指向当前节点如何找到指定节点?temp.no = newHeroNode.no

    代码实现

    /修改节点的信息,根据no编号来修改,即no编号不能修改 //根据newHeroNode的no来修改 public void update(HeroNode newHeroNode){ //判断是否为空 if (head.next == null){ System.out.println("链表为空"); return; } //添加一个辅助的节点temp,因为head节点不能动 //根据no编号找到需要修改的节点 HeroNode temp = head.next; boolean flag = false; //flag代表需要添加的节点是否存在,默认false while (true){ if (temp == null)break; //已经遍历完退出循环 if (temp.no == newHeroNode.no){ //找到需要修改的编号 flag = true; break; } temp = temp.next; //继续遍历,temp后移一个节点 } //根据flag值判断是否找到需要修改的节点 if (flag){ temp.name = newHeroNode.name; temp.nickname = newHeroNode.nickname; }else { //没有找到 System.out.printf("没有找到编号%d的节点,不能修改\t",newHeroNode.no); } }

    4.循环链表

    循环链表是另一种形式的链表,它的特点是表中最后一个结点的指针域不再为空,而是指向表头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到链表中其他结点,如图2.12所示。

    4.1 单向环形链表应用场景

    Josephu 问题

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

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

    4.2 环形链表的构建与遍历

    Boy 节点就是个普普通通的单向链表节点 // 创建一个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; } } first 节点为单向循环链表的首节点,是真实存放数据的节点,不是头结点 // 创建一个环形的单向链表 class CircleSingleLinkedList { // 创建一个first节点,当前没有编号 private Boy first = null; // ...

    4.2.1 构建单向循环链表

    代码思路

    长度为 1 的情况:新创建的 boy 节点即是首节点:first = boy;

    自封闭(自己构成环形链表):first.setNext(first);

    此时 first 节点既是首节点,也是尾节点,辅助指针也指向 first :curBoy = first;

    长度不为 1 的情况:将 boy 节点添加至环形链表的最后:curBoy.setNext(boy);

    curBoy 节点永远是环形链表的尾节点构成环形链表(最):boy.setNext(first);

    辅助指针后移,指向环形链表的尾节点:curBoy = boy;

    代码实现

    // 添加小孩节点,构建成一个环形的链表 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 节点 first.setNext(first); // 构成环 curBoy = first; // 让curBoy指向第一个小孩 } else { curBoy.setNext(boy); // 将 boy 节点加到链表尾部 boy.setNext(first); // 构成环 curBoy = boy; // curBoy 指针后移 } } }

    4.2.2 遍历单向循环链表

    代码思路定义辅助变量 curBoy ,相当于一个指针,指向当前节点何时退出 while 循环?当 curBoy 已经指向环形链表的尾节点:curBoy.getNext() == first

    代码实现

    // 遍历当前的环形链表 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后移 } }

    4.3 解决 Josephu 问题

    代码思路:

    1.辅助变量 helper :helper 永都指向环形链表的尾节点,环形链表的尾节点永远都指向首节点,可得出: helper.getNext() == first 2. 如何将 helper 定位至环形链表的尾节点? 初始化时,让 helper = first ,此时 helper 指向环形链表的首节点 while 循环终止条件?helper.getNext() == first :此时 helper 已经移动至环形链表的尾节点 3. 如何定位至第 startNo 个节点? 如果想要定位至第 2 个节点,那么则需要让 first 和 helper 都移动 1 步,所以让 first 和 helper 都移动 (startNo - 1)步即可 4.如何数 nums 下? 让 first 和 helper 都移动 (nums - 1)步即可 5.如何实现出圈? 我们需要将 first 指向的节点出圈,first 前一个节点的地址在 helper 中存着(环形链表) 先让 first 后移一步:first = first.getNext; 出圈:helper.setNext(first); ,原来的 first 节点由于没有任何引用,便会被垃圾回收机制回收 6.while 循环终止条件? 圈中只剩一人:helper == first

    代码实现

    // 根据用户的输入,计算出小孩出圈的顺序 /** * * @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 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 < countNum - 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()); }

    总结

    操作单向链表:对于插入、删除操作,只能定位至待操作节点的前一个节点,如果定位至当前节点,那么其上一个节点的信息便会丢失

    Processed: 0.009, SQL: 9