【数据结构与算法】详解单向无头非循环链表的基本操作

简介: 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

链表的概念


链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

链表的种类


链表的种类有很多,常见的链表结构有单向和双向,循环和非循环,带头节点和不带头节点这几种结构,把这些结构组合一下就有8种链表结构,这些不要求全部掌握,我们只需要掌握单向非循环不带头结点的链表,并且了解双向非循环不带头节点的链表这两种即可。什么

因此,接下来就来实现单向非循环不带头结点链表的基本操作。


单链表基本操作的实现


链表有数据域和指针域,因此我们在实现链表的两个结构使用类抽象出来,如下:

static class ListNode {

       public int val;//数据域

       public ListNode next;//指针域

       public ListNode(int val) {

           this.val = val;

       }

   }

   public ListNode head;


头插法


单链表是用户不断申请存储单元和改变链接关系而得到的一种特殊数据结构,将链表的左边称为链头,右边称为链尾。头插法建单链表是将链表右端看成固定的,链表不断向左延伸而得到的。头插法最先得到的是尾结点。


头插法每次插入数据都是在头节点的左边进行插入,头插法很简单,不需要考虑特殊情况,即使头节点为空,也不用担心,因为是将插入结点的指针域指向头节点,即使头节点为空,插入结点的指针域就是null,然后在将头节点指向刚才插入的那个结点就可以了,代码实现如下:

public void addFirst(int data){

       ListNode node = new ListNode(data);

       node.next = head;

       head = node;

   }


尾插法

若将链表的左端固定,链表不断向右延伸,这种建立链表的方法称为尾插法。尾插法建立链表时,头指针固定不动,故必须设立一个搜索指针,向链表右边延伸,在最后一个结点进行插入。尾插法最先得到的是头结点。


尾插法与头插法不同,头插法不用担心头结点为空的情况,但是尾插法不同,尾插法是插入在头节点之后,如果头节点为空,就没办法将结点插入,会报空指针异常的错误。

因此我们在进行尾插之前,要先判断头节点是否为空,如果为空,那个要插入的结点就是头节点,如果不为空,就进行尾插的操作。 \color{#0000FF}{要先判断头节点是否为空,如果为空,那个要插入的结点就是头节点,如果不为空,就进行尾插的操作。}要先判断头节点是否为空,如果为空,那个要插入的结点就是头节点,如果不为空,就进行尾插的操作。

因为头节点不能够移动,头节点移动的话,链表的结构就改变了。就需要在创建一个新的指针来遍历链表,在遍历到链表的最后一个结点时进行尾插。


public void addLast(int data){

       ListNode node = new ListNode(data);

       ListNode cur = this.head;

       if(cur == null) {

           this.head = node;

       }else {

           while (cur.next != null) {

               cur = cur.next;

           }

           //cur已经是尾巴节点了

           cur.next = node;

       }

   }

避雷: \color{#FF0000}{避雷:}避雷:之前在写尾插法的时候,没有加else,因为都是先判断头节点是否为空,不为空就进行下面的操作。乍一看没有什么问题,但是里面的问题大了,因为在头结点为空的这种情况下,如果不加else,它就也会执行cur,next = node这行代码,前面有执行了this.head = node这行代码了,它就会指向自己,这就形成闭环了。


获得链表的长度

获得链表的长度,需要我们在遍历链表的同时进行计数,我们要定义一个指针遍历链表,如果这个指针不为空,就让count自增1,直到指针为空。


   public int size(){

       int count = 0;

       ListNode cur = head;

       while(cur != null){

           count++;

           cur = cur.next;

       }

       return count;

   }


在任意位置插入一个数据


在任意位置插入一个数据,第一个数据节点为0号下标。

数据结构是一门很严谨的学科,稍微有一点没注意到的地方,就会出现BUG。在单链表中插入数据如果处理不好,会出现很多问题。

先看下图,如果我要在2结点的后面插入3这个结点,需要怎么做?

135.png

答案是先先遍历链表,走到2这个位置,先让3这个结点的指针域等于2这个结点的指针域,然后再把3这个结点的地址赋值给2个结点的指针域。看下图:

136.png

一定要先把2的指针域存到3的指针域中,在把3的地址赋值给2的指针域中,顺序不能颠倒。

对于上面插入数据是一般情况,我们还要考虑到在特殊位置插入数据 \color{#FF0000}{特殊位置插入数据}特殊位置插入数据,就比如说在 0 位置插入数据,还有在链表的最后插入数据 \color{#0000FF}{在0位置插入数据,还有在链表的最后插入数据}在0位置插入数据,还有在链表的最后插入数据 。

如果插入位置为0时,此时插入数据就会变成头插法插入数据。

如果插入位置刚好在链表的最后,此时插入就变成尾插法插入数据。


对于插入数据,只考虑插入位置的特殊性还是不够的,我们还要考虑到插入位置的合法性。 \color{#FF0000}{我们还要考虑到插入位置的合法性。}我们还要考虑到插入位置的合法性。

插入数据的位置是否合法我们也要进行判断,如果插入位置为负数或者超过链表的最大长度时,就无法插入数据。我们要让用户知道当然位置是无法插入数据的,我们可以在判断之后输出一个插入位置不合法,以及使用自定义异常来抛一个插入位置不合法的异常。


如果插入位置合法就进行数据的插入操作。分析完之后,就可以开始写代码了。

   //任意位置插入,第一个数据节点为0号下标

   public boolean addIndex(int index,int data){

       if(index < 0||size()<index){

           System.out.println("Index位置不合法");

           throw new IndexWrongFullExcption("Index位置不合法");

       }

       //插入位置为0时,头插法

       if(index == 0){

           addFirst(data);

       }

       //当插入位置为最后

       if(index == size()){

           addLast(data);

       }

       //当插入位置为中间时,要先走index-1步

       ListNode cur = findIndexSubOne(index);

       ListNode node = new ListNode(data);

       node.next = cur.next;

       cur.next = node;

       return false;

   }

   //找到index-1位置的结点

   private ListNode findIndexSubOne(int index) {

       ListNode cur = this.head;

       while (index-1 != 0) {

           cur = cur.next;

           index--;

       }

       return cur;

   }

我将找到index-1位置的结点这部分写成一个方法,是方便下次可能会在其它地方用到,如果会用到,直接调用这个方法即可,就不用复制粘贴甚至是重写一遍了,可以帮助我们减轻代码量。用不到也没关系。


判断链表中是否有值为key的结点


判断链表中是否有值为key的结点,我们只需要遍历一遍单链表就可以了,在遍历时让结点的数据域与key做比较,相等就返回true,不相等就继续遍历,直到遍历完,返回false

   //查找是否包含关键字key是否在单链表当中

   public boolean contains(int key){

       ListNode cur = this.head;

       while(cur!=null){

           if(cur.val == key){

               return true;

           }

           cur = cur.next;

       }

       return false;

   }


删除第一个等于给定值为key的结点


如果我要删除下面图中的2这个结点应该怎么做?

137.png

答案是将2结点的指针域赋值给1这个结点的指针域,就可以实现删除。效果如下图所示:

138.png

我们在遍历链表时,1这个结点的指针域发生了改变,因此1会直接指向3这个结点,从而实现删除。

当然我们也要保证链表中有值为key的这个结点,有才能进行删除。如果没有,就输出一个没有值为key的数字之类的提示用户。

在删除时,从第二个结点开始之后结点的删除方法都是一样的,只有头结点不一样 \color{#0000FF}{在删除时,从第二个结点开始之后结点的删除方法都是一样的 ,只有头结点不一样}在删除时,从第二个结点开始之后结点的删除方法都是一样的,只有头结点不一样,我们也要把头节点想到,如果头节点的数据域为key时应该怎样删。只需要让head = head.next让头节点往后走一步就可以了,虽然很简单,但是不要忽略头节点的问题。

最特殊的还是空链表问题,如果时空链表就不用删除,直接返回就可以了。

//删除第一次出现关键字为key的节点

   public void remove(int key){

       if(this.head == null) {

           return;

       }

       //删除头节点

       if(this.head.val == key) {

           this.head = this.head.next;

           return;

       }

       ListNode cur = findPrevOfKey(key);

       if(cur == null) {

           System.out.println("没有你要删除的数字!");

           return;

       }

       ListNode del = cur.next;

       cur.next = del.next;

   }

   private ListNode findPrevOfKey(int key) {

       ListNode cur = this.head;

       while (cur.next != null) {

           if(cur.next.val == key){

               return cur;

           }

           cur = cur.next;

       }

       return null;

   }


删除所有等于给定值为key的结点


删除所有等于给定值为key的结点这个只需要在上面删除第一个值为key的基础上,遍历完整个链表,删除即可。

其中有一点不同: \color{#0000FF}{其中有一点不同:}其中有一点不同:前面时删除第一个值等于key的结点, 所以要先判断头节点的值是否为key,但是这个题要放在最后在进行判断,如果不放在最后可能会发生没删完这种情况。

想一下,如果先判断头结点是否为key,如果是将头节点向后走一步,但如果后面一个结点的值也为key呢?如果只判断一次,那么这个值为key的结点就留下来了。如果想先判断头节点是否为key这个问题,就要使用循环,直到头节点的值不为key或者头节点为空。

public void removeAllKey(int key){

        if(head == null){

            return;

        }

        ListNode cur = head.next;

        ListNode prev = head;

        while(cur != null){

            if(cur.val == key){

                prev.next = cur.next;

                cur = cur.next;

            }else{

                prev = cur;

                cur = cur.next;

            }

        }

        if(head.val == key){

            head = head.next;

        }

    }

如果觉得不够详细或者不是很懂的小伙伴,也可以去看一下我的这篇文章:【数据结构算法篇】链表面试题2—删除链表中等于给定值 val 的所有节点


打印链表


头节点不能动,所以同样需要定义一个指针cur对单链表进行遍历,cur不为空,打印数据域,cur指向下一个结点,直到指针为空时停止。

   public void display(){

       ListNode cur = this.head;

       while(cur != null){

           System.out.print(cur.val+" ");

           cur = cur.next;

       }

   }


清空链表


清空单链表也很简单,只需要将头结点置为空就可以了,头节点为空了,就代表头节点与后面的链表失去了联系,从而找不到后面的结点了。

 public void clear(){

        this.head = null;

   }


完整代码:

public class MyLinkList {

   static class ListNode {

       public int val;

       public ListNode next;

       public ListNode(int val) {

           this.val = val;

       }

   }

   public ListNode head;

   // 1、无头单向非循环链表实现

   // 头插法

   public void addFirst(int data){

       ListNode node = new ListNode(data);

       node.next = head;

       head = node;

   }

   //尾插法

    public void addLast(int data){

       ListNode node = new ListNode(data);

       ListNode cur = this.head;

       if(cur == null) {

           this.head = node;

       }else {

           while (cur.next != null) {

               cur = cur.next;

           }

           //cur已经是尾巴节点了

           cur.next = node;

       }

   }

   //任意位置插入,第一个数据节点为0号下标

   public boolean addIndex(int index,int data){

       if(index < 0||size()<index){

           System.out.println("Index位置不合法");

           throw new IndexWrongFullExcption("Index位置不合法");

       }

       //插入位置为0时,头插法

       if(index == 0){

           addFirst(data);

       }

       //当插入位置为最后

       if(index == size()){

           addLast(data);

       }

       //当插入位置为中间时,要先走index-1步

       ListNode cur = findIndexSubOne(index);

       ListNode node = new ListNode(data);

       node.next = cur.next;

       cur.next = node;

       return false;

   }

   //找到index-1位置的结点

   private ListNode findIndexSubOne(int index) {

       ListNode cur = this.head;

       while (index-1 != 0) {

           cur = cur.next;

           index--;

       }

       return cur;

   }

   //查找是否包含关键字key是否在单链表当中

   public boolean contains(int key){

       ListNode cur = this.head;

       while(cur!=null){

           if(cur.val == key){

               return true;

           }

           cur = cur.next;

       }

       return false;

   }

  //删除第一次出现关键字为key的节点

   public void remove(int key){

       if(this.head == null) {

           return;

       }

       //删除头节点

       if(this.head.val == key) {

           this.head = this.head.next;

           return;

       }

       ListNode cur = findPrevOfKey(key);

       if(cur == null) {

           System.out.println("没有你要删除的数字!");

           return;

       }

       ListNode del = cur.next;

       cur.next = del.next;

   }

//找到数据域为key的前面一个结点

   private ListNode findPrevOfKey(int key) {

       ListNode cur = this.head;

       while (cur.next != null) {

           if(cur.next.val == key){

               return cur;

           }

           cur = cur.next;

       }

       return null;

   }

   /** 删除所有值为key的节点

    *  可能会遇到的问题

    *  1.空链表

    *  2.头节点的值为key

    * @param key

    */

    public void removeAllKey(int key){

        if(head == null){

            return;

        }

        ListNode cur = head.next;

        ListNode prev = head;

        while(cur != null){

            if(cur.val == key){

                prev.next = cur.next;

                cur = cur.next;

            }else{

                prev = cur;

                cur = cur.next;

            }

        }

        if(head.val == key){

            head = head.next;

        }

    }

    //得到单链表的长度

    public int size(){

       int count = 0;

       ListNode cur = head;

       while(cur != null){

           count++;

           cur = cur.next;

       }

       return count;

   }

   //打印链表

   public void display(){

       ListNode cur = this.head;

       while(cur != null){

           System.out.print(cur.val+" ");

           cur = cur.next;

       }

   }

   //清空单链表

   public void clear(){

        this.head = null;

   }

}


自定义异常

public class IndexWrongFullExcption extends RuntimeException{

public IndexWrongFullExcption() {

}

public IndexWrongFullExcption(String message) {

super(message);

}

}

相关文章
|
1月前
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
55 4
|
24天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
45 5
|
1月前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
87 4
|
1月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
1月前
|
存储 Web App开发 算法
2024重生之回溯数据结构与算法系列学习之单双链表【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构之单双链表按位、值查找;[前后]插入;删除指定节点;求表长、静态链表等代码及具体思路详解步骤;举例说明、注意点及常见报错问题所对应的解决方法
|
1月前
|
算法
数据结构之购物车系统(链表和栈)
本文介绍了基于链表和栈的购物车系统的设计与实现。该系统通过命令行界面提供商品管理、购物车查看、结算等功能,支持用户便捷地管理购物清单。核心代码定义了商品、购物车商品节点和购物车的数据结构,并实现了添加、删除商品、查看购物车内容及结算等操作。算法分析显示,系统在处理小规模购物车时表现良好,但在大规模购物车操作下可能存在性能瓶颈。
48 0
|
1月前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
61 0
|
2月前
|
存储
[数据结构] -- 双向循环链表
[数据结构] -- 双向循环链表
26 0
|
2月前
|
存储
探索数据结构:便捷的双向链表
探索数据结构:便捷的双向链表
|
2月前
|
存储
探索数据结构:单链表的实践和应用
探索数据结构:单链表的实践和应用