java语言实现单链表---不含头结点

  • 一、相关概念
    • 1.什么是线性表
    • 2.什么是顺序表
    • 3.什么是链表
    • 4.单链表、双链表、循环单链表、循环双链表
    • 5.头结点和首结点
    • 6.常见的栈和队列与线性表的关系
  • 二、实现过程
    • 1.提供结点类:Node
    • 2.实现单链表:NoHeadLinkedTable
    • 3.提供根据下标插入方法:insert(int i,Object object)
    • 4.头插法、尾插法:insertHead(Object object)、insertHail(Object object)
    • 5.提供判空(isEmpty)、长度(length)、打印(display)、清空(clear)等方法
    • 6.提供根据下标获取数据元素的方法:get(int i)
    • 7.提供根据下标删除的方法:remove(int i)
    • 8.提供获取某数据元素第一次出现的下标方法:indexOf(Object object)
    • 9.附上单链表实现的完整代码
  • 三、总结
    • 1.含头结点的单链表与不含头结点的单链表区别
    • 2.链表的缺点
    • 3.链表的优点
    • 4.如何使用链表

前言:
线性表是我们最常用的一种数据结构,线性表包含顺序表和链表,顺序表典型应用就是我们常用的ArrayList,链表的典型应用其中就有我们常用的LinkedList。LinkedList他的底层就是使用链表来存储数据元素的。这篇文章用以总结单链表的实现以及使用并且对比单链表与顺序表的优缺点,确定其使用功能场景,且该篇文章总结的单链表是不带有头结点的实现方式,若是想要看带有头结点的单链表的实现请看这里: java实现单链表—含头结点,且该篇文章与另一篇含头结点的实现单链表的文章第一部分均是重复的,若看可忽略,说到这里必须要写下含不含头结点的区别,带有头结点是为了更好的实现单链表的功能,带有头结实现的insert、remove等方法相比于不带头结点的单链表会更简洁一些,此外,学习不带头结点的和带有头结点的单链表是没有区别的。

一、相关概念

第一部分主要介绍下和单链表有关的相关概念,这里给出的概念都是书本上的官方定义,并不是作者胡诌的,为什么给出这些官方的定义呢 ?因为笔者认为官方给出的定义,是对一个概念最本质的解释,它的解释经过了时间的考验,被认为是解释的最合理深入简明扼要的。

1.什么是线性表

线性表是由n个数据元素构成的有限序列,n为线性表的表长,当n为0时表示当前线性表是空表。线性表有且仅有一个开始结点和终端节点。且开始结点没有前驱,终端节点没有后继,其余节点有且仅有一个前驱和后继。线性表一般分为顺序表和链表。

2.什么是顺序表

采用顺序存储结构存储数据元素的线性表被称为顺序表,顺序表,要求逻辑上相邻的数据元素在物理内存的存储上也是相邻的,当在顺序表的第i(0<=i<=n)插入一个数据元素时,需要后移n-i+1个数据元素,当删除第i个元素时需要移动n-i个数据元素。java实现顺序表

3.什么是链表

采用链式存储结构存储数据元素的线性表被称为链表,链表不要求逻辑上相邻的数据元素内存上必须相邻,链表的每个节点都包含两部分,一部分是数据域用以存储数据,一部分是指针域用以存储指向相邻结点的指针或者引用。链表通过每个节点的指针域将一串数据串联成链。当结点只有一个指针域时,被称为单链表。

4.单链表、双链表、循环单链表、循环双链表

当链表的结点只有一个指针域时被称为单链表,循环单链表是单链表的一种特殊变化,只是将尾结点又指向了头结点(也可能是首结点)。
当链表的结点有两个时,一个指向前驱,一个指向后继这种链表便是双链表,循环双链表是双链表的一种特殊变化,只是将尾结点又指向了头结点(也可能是首结点)。
所有的链表的实现均有两种方式,一种是带有头结点,一种是不带有头结点的实现方式,两种实现略有区别。

5.头结点和首结点

首节点:真正存储第一个数据元素的结点被称为首节点。
头结点:当链表采用带头结点方式实现时,会创建一个数据域为null的节点,该节点的指针域存储的指针指向首节点的指针,他的唯一作用是标识链表的开始,带有头结点的链表更易于操作。

6.常见的栈和队列与线性表的关系

栈和队列也是常见的数据结构,但是栈和队列并不是线性表的一种,线性表只包含了顺序表和链表。不过我们可以将栈和队列看成是特殊的线性表。怎么特殊呢,栈是被限制了只能在一端进行插入和删除操作的线性表,所以栈的特性是先入后出,队列是被限制了只能在一端插入另一端删除的线性表,所以队列的特性是先入先出。既然栈和队列都是特殊的线性表,那么栈和队列自然就可以使用线性表来实现了,通常可以使用线性表中的顺序表和队列来实现栈和队列。

二、实现过程

第二部分总结单链表中常用的方法,比如insert、remove、get、length、isEmpty、display等方法的实现。对方法实现的思路加以总结,点名该方法实现的核心思想。

1.提供结点类:Node

实现单链表、结点类则是前提。根据单链表的定义我们可以知道,单链表中每个结点都是包含了两部分,一部分是数据域,一部分是指针域。数据域存储数据,指针域存储指向下一个结点的引用,所以Node应该有两个属性,然后我们还需要提供必需的构造方法,同时我们有两处策略处理Node的两个属性,要么声明成public类型的变量,这样便于操作,要么声明成private的私有变量,这样更符合面向对象的思想,我们操作时需通过get、set方法操作,这里使用将两个属性声明成public的方式,实现如下:

public class Node {public Object object;//数据域public Node next;//指针域public Node(){}public Node(Object object){this.object = object;}public Node(Node next){this.next = next;}public Node(Object object,Node next){this.object = object;this.next = next;}@Overridepublic String toString() {return "Node{" +"object=" + object +", next=" + next +'}';}
}

2.实现单链表:NoHeadLinkedTable

在头结点的单链表里,我们需要声明一个头结点,用以存储指向首节点的指针,同样为了插入、删除等方法的实现,在不带头结点的单链表中我们也需要提供一个结点—首结点,不过该结点是可以存储数据的,这个结点也就是我们的首结点(头结点不存储数据,首结点存储数据),但是我们无需去提供构造方法初始化首结点,直接使用默认的空参构造即可,在需要插入的地方我们再手动创建首结点的实现。

/*** 这是不带头结点的单链表* 说明:带头结点的标志是,第一个结点(头结点)只存储指向下一个节点的指针,其数据域为空*       此处的firstNode是首结点,他的数据域和指针域都是可以存储数据的。* @author pcc* @version 1.0.0* @className NoHeadLinkedTable* @date 2021-04-15 15:31*/
public class NoHeadLinkedTable {private Node firstNode;
}

3.提供根据下标插入方法:insert(int i,Object object)

链表下标从0开始,这里的i指的是待插入位置的下标,该方法的实现思路就是找到i-1位置的数据元素,然后将i-1的next的属性交给这个新的数据元素的next属性,再将新的数据元素交给i-1位置数据元素的next即可,此外需要注意的是首结点的元素需要单独判断下。

    /**** @param i 下标* @param object 待存储对象*/public void insert(int i,Object object) throws Exception{if(i<0 || i>length())throw new Exception("下标不合法");if(null == object)throw new Exception("数据不可为空");//不使用头结点时,首结点特殊处理,使用头结点的意义就是方便处理//从这里也可以看到,不使用头结点,需要单独处理首结点的数据if(i==0)if(length()==0){firstNode = new Node(object);return;}else{firstNode = new Node(object,firstNode);return;}Node node = firstNode;int j = 0;while(node.next !=null && j<i-1) {j++;node = node.next;}node.next = new Node(object,node.next);}

上面是不带有头结点单链表的插入实现,下面再展示下带有头结点的时的插入实现,做个对比。

        //提供插入方法:根据下标插入/*** 思路:* 1.寻找i位置的前一个数据元素,若是找不到则说明i输入有误()* 2.在指定位置插入object* @param i* @param object*/public void insert(int i,Object object) throws Exception{if(i<0 || i>length() ||object==null)throw new Exception("下标或者值无效");Node node = head;int j = 0;while(node.next!=null && j<i){node = node.next;j++;}node.next = new Node(object,node.next);}

对比以上两处代码,核心的区别就是i==0时的这一串逻辑,这串逻辑就是单独处理在头部插入时的逻辑的代码,总结可以看出不带有头结点时,实现只需要多关注下再头部插入的问题即可,其他位置插入时操作没有区别的。

4.头插法、尾插法:insertHead(Object object)、insertHail(Object object)

头插法和尾插法的实现也很简单,我们可以直接调用根据下标插入的方法,在下标为0的位置插入就是头插,在下标为length()-1的位置插入就是尾插法,此外还可以直接根据头结点尾结点进行操作,因为上篇文章是使用根据下标实现的头插法和尾插法,这里就使用另外一种方式实现。

    //头部插入public void insertHead(Object object) throws Exception{firstNode = new Node(object,firstNode);}

以上是头插法的代码,思路:直接生成一个新的首结点,将以前的首结点作为新首结点的next属性,若是以前首结点就是null,那么新首结点的next属性也为null。以下是尾插法的实现:

    //尾部插入public void insertTail(Object object) throws Exception{if(length()==0)firstNode = new Node(object);else{Node node = firstNode;while(node.next!=null){node = node.next;}node.next = new Node(object);}}

以上便是尾插法的实现代码,思路:同样是先判断链表是否为空(使用length方法判断是一样的效果),为空我们直接创建首结点即可,此时首结点就是尾结点。不为空我们就需要遍历了,找到next属性为null的结点,该节点就是尾结点(尾结点的标志就是尾结点的next属性为null),然后在尾部插入即可。

5.提供判空(isEmpty)、长度(length)、打印(display)、清空(clear)等方法

这些方法都比较简单,直接将代码展示在这里

    //判空public boolean isEmpty(){return firstNode.object==null;}//链表长度public int length(){if(firstNode!=null){Node node = firstNode;int i = 0;while(node!=null){i++;node = node.next;}return i;}elsereturn 0;}//输出链表public void display(){if(firstNode!=null){Node node = firstNode;while(node != null){System.out.println("结点:"+node.object);node = node.next;}}}//清空链表public void clear(){if (firstNode!=null){firstNode.object = null;firstNode.next=null;}}

在这里我们就可以初步测试下根据下标插入、头插法、尾插法等其他方法了,测试场景如下,根据代码,我们猜测输出结果应该是:唐僧、孙悟空、沙和尚、猪八戒、白龙马,效果如下图,可以看到如预期所想,这些方法的实现没有问题,其他场景笔者也有测试这里就不一一展示测试结果了。

6.提供根据下标获取数据元素的方法:get(int i)

下标从0开始,此处i代表下标为i的元素,实现思路:若i是0则返回首结点,否则遍历链表寻找下标为i的数据元素。

    //根据下标获取方法public Object get(int i) throws Exception{if(i<0 || i>length()-1)throw new Exception("下标不合法");if(length() == 0)throw new Exception("链表为空");//单独处理首结点情况if(i==0)return firstNode.object;Node node = firstNode;int j =0;while(node.next != null && j < i){j++;node = node.next;}return node.object;}

然后测试下上方代码是否有效,我们获取里阿尼宝的第一个和最后一个,然后再从链表中间位置任意位置获取一个,看看输出是否正确,如下:

根据上方测试代码和输出,我们可以看到根据下表获取的方法实现是没有问题的,其他场景笔者也有测试这里就不一一展示测试结果了。

7.提供根据下标删除的方法:remove(int i)

这里的i指的都是链表元素的下标,删除的思路:判断i的值是否是首结点,如果是直接将以前的第二个结点变成首结点,这样就实现了删除首结点,若不是,则需要遍历寻找下标为i-1的数据元素,将该元素的指针域指向下标为i+1的数据元素即可实现删除,代码如下:

    //根据下标删除方法public void remove(int i) throws Exception{if(i<0 || i>length()-1)throw new Exception("下标不合法");if(length() == 0)throw  new Exception("链表为空");//特殊处理i为0的时候。if(i==0){firstNode = firstNode.next;return;}//遍历时,与带有头结点的链表不同的是,无头结点的链表Node node = firstNode;int j = 0;while(node.next != null && j< i-1){j++;node =node.next;}//找到的是下标为i-1的元素node.next = node.next.next;}

然后我们测试下该方法,我们将刚刚根据下标获取到的三个元素删了,若是正常剩下的元素应该是孙悟空、猪八戒,如下图:

根据上方的输出内容我们可以看到,输出结果都是正常的。

8.提供获取某数据元素第一次出现的下标方法:indexOf(Object object)

该方法是获取某数据元素第一次出现的下标位置,所以我们需要遍历链表,当链表的元素与目标元素相等则返回下标,否则继续遍历,若遍历完成依然没有找到该与目标元素相等的数据元素,则说明链表不含有该元素,返回-1,代码如下:

    //获取某元素第一次出现的下标public int indexOf(Object object) throws Exception{if(object==null)throw new Exception("数据不合法");if(length() == 0)throw new Exception("链表为空");Node node = firstNode;int j = 0;//这里找的是当前元素下标,插入、删除都是获取的i-1的下标//区别是获取当前元素时,我们应该判断的是node!=null,获取i-1时,我们//应该判断node.next != null。while(node!=null ){if(node.object.equals(object))return j;j++;node = node.next;}return -1;}

测试该代码的实现是否正确,我们使用猪八戒来测试,若返回下标时1,说明该方法实现没有问题 ,结果如下图,可以看到返回的下标确实是1,说明该方法实现正确。

9.附上单链表实现的完整代码

/*** 这是不带头结点的单链表* 说明:带头结点的标志是,第一个结点(头结点)只存储指向下一个节点的指针,其数据域为空*       此处的firstNode是首结点,他的数据域和指针域都是可以存储数据的。* @author pcc* @version 1.0.0* @className NoHeadLinkedTable* @date 2021-04-15 15:31*/
public class NoHeadLinkedTable {private Node firstNode;/**** @param i 下标* @param object 待存储对象*/public void insert(int i,Object object) throws Exception{if(i<0 || i>length())throw new Exception("下标不合法");if(null == object)throw new Exception("数据不可为空");//不使用头结点时,首结点特殊处理,使用头结点的意义就是方便处理//从这里也可以看到,不使用头结点,需要单独处理首结点的数据if(i==0)if(length()==0){firstNode = new Node(object);return;}else{firstNode = new Node(object,firstNode);return;}Node node = firstNode;int j = 0;while(node.next !=null && j<i-1) {j++;node = node.next;}node.next = new Node(object,node.next);}//头部插入public void insertHead(Object object) throws Exception{firstNode = new Node(object,firstNode);}//尾部插入public void insertTail(Object object) throws Exception{if(length()==0)firstNode = new Node(object);else{Node node = firstNode;while(node.next!=null){node = node.next;}node.next = new Node(object);}}//根据下标获取方法public Object get(int i) throws Exception{if(i<0 || i>length()-1)throw new Exception("下标不合法");if(length() == 0)throw new Exception("链表为空");//单独处理首结点情况if(i==0)return firstNode.object;Node node = firstNode;int j =0;while(node.next != null && j < i){j++;node = node.next;}return node.object;}//根据下标删除方法public void remove(int i) throws Exception{if(i<0 || i>length()-1)throw new Exception("下标不合法");if(length() == 0)throw  new Exception("链表为空");//特殊处理i为0的时候。if(i==0){firstNode = firstNode.next;return;}//遍历时,与带有头结点的链表不同的是,无头结点的链表Node node = firstNode;int j = 0;while(node.next != null && j< i-1){j++;node =node.next;}//找到的是下标为i-1的元素node.next = node.next.next;}//获取某元素第一次出现的下标public int indexOf(Object object) throws Exception{if(object==null)throw new Exception("数据不合法");if(length() == 0)throw new Exception("链表为空");Node node = firstNode;int j = 0;//这里找的是当前元素下标,插入、删除都是获取的i-1的下标//区别是获取当前元素时,我们应该判断的是node!=null,获取i-1时,我们//应该判断node.next != null。while(node!=null ){if(node.object.equals(object))return j;j++;node = node.next;}return -1;}//判空public boolean isEmpty(){return firstNode.object==null;}//链表长度public int length(){if(firstNode!=null){Node node = firstNode;int i = 0;while(node!=null){i++;node = node.next;}return i;}elsereturn 0;}//输出链表public void display(){if(firstNode!=null){Node node = firstNode;while(node != null){System.out.println("结点:"+node.object);node = node.next;}}}//清空链表public void clear(){if (firstNode!=null){firstNode.object = null;firstNode.next=null;}}}

三、总结

1.含头结点的单链表与不含头结点的单链表区别

两者在性能上是没有区别的,区别在于根据下标插入、删除等方法的实现。略有区别,不含头结点时,我们需要对首结点进行单独的判断,这样会相对复杂一些,但是在总体的时间复杂度上是可以忽略不计的,因此在实际中开发中,这两种实现应该是都可以使用,没有太强的优劣之分。

2.链表的缺点

线性表的两种实现顺序表、链表。相比于顺序表,链表的缺点就是查找元素比较慢,查找元素的时间复杂度是O(n),而顺序表的时间复杂度是O(1),在查找上顺序表要优于链表,链表查找慢,就是它的缺点了。

3.链表的优点

顺序表在指定下标x位置插入元素,组需要后移n-x+1个元素,若是删除下标为x的数据元素,则需要向前移动n-x个数据元素,但是链表则不需要移动任何元素,链表需要做的只是找到对应的元素将指针域中的指针进行更新即可。链表的插入和删除的时间复杂度可以看成O(1),而顺序表插入和删除操作的都是O(n).所以链表的优点就是插入和删除比较快。

4.如何使用链表

所以综合链表的优点与缺点我们可以发现,顺序表更适合存储“静态”型数据,而链表更适合存储“动态”型数据,何为动态型呢,就是那些插入、删除操作比较频繁的数据。这类数据更适合使用链表存储。而数据结构不易改变的数据使用顺序表存储更合适。顺序表可类比java中的ArrayList,链表可类比java中的LinkedList。这两者都是顺序表与链表的典型应用。

java语言实现单链表---不含头结点相关推荐

  1. 单链表指针和头结点的理解

    线性表使用顺序(数组)存储时有个弊端,那就是在插入和删除时需要大量的移动数据,这显示是非常消耗时间的,所以可以采用链式存储,即有一个指针域(单链表),来记录下个结点的存储位置(地址),这样在插入和删除 ...

  2. c语言给定一个单链表输入k,C语言实现单链表(不带头结点)的基本操作

    链表在数据结构和算法中的重要性不言而喻.这里我们要用C来实现链表(单链表)中的基本操作.对于链表的基本概念请参考<数据结构与算法之链表>这篇博客.示例代码上传至 https://githu ...

  3. 单链表操作10-带头结点的单链表逆置(个人学习笔记,仅供参考)

    带头结点的单链表逆置 题目要求 单链表结点定义 函数接口定义 测试程序样例 输入样例 输出样例 答案 题目要求 设计算法Reverse( ),将带头结点的单链表A逆置,要求利用原有链表的链点,最后输出 ...

  4. C语言版单链表:按序号查找结点值和按值查找表结点的代码总结

    先上函数模块 按序号查找结点 //按序号查找结点 LNode *GetElem(LinkList L,int i){int j=1;LNode *p=L->next;if(i==0)return ...

  5. 带头结点单链表、不带头结点单链表(头指针单链表)

    1.头结点和头指针的区别 1.1区别: 头指针表明了链表的结点,可以唯一确定一个单链表. 头指针指向链表的第一个结点,其记录第一个存储数据的结点的地址. 头结点是点链表的第一个结点,若单链表有头结点, ...

  6. C语言实现单链表的逆置-实训

    任务描述 本关需要你设计一个程序,实现单链表的逆置. 相关知识 单链表的逆置分为两种方法:头插法和就地逆置法,这两种方法虽然都能够达到逆置的效果,但还是有着不小的差别. 编程要求 按程序提示输入并创建 ...

  7. 单链表实现增删改查(含头结点)

    ** 单链表实现增删改查(含头结点) ** **## 单链表** package com.Linked;public class HeadLinked {public String name;// 链 ...

  8. C语言基于单链表的词典软件

    C语言基于单链表的词典软件 实验1: 日期:2022-10-4 类型:设计型 题目:基于单链表的词典软件 内容: 利用单链表存储词典,可以实现从文件中加载数据.查询单词.添加词条.删除词条.将词典保存 ...

  9. c语言用链表实现成绩管理系统,C语言基于单链表得学生成绩管理系统

    前言 传说中讲课通俗而不庸俗,说话风流而不下流的小编又来了哦,今天的主要内容是:C语言基于单链表得学生成绩管理系统.首先给年夜家总结下为什么你们学习C语言会觉得难,尤其是对单链表这块,主要原因得是因为 ...

最新文章

  1. 技术以外的功夫 ----作者:李天平
  2. Android NDK JNI C++ 13 pthread多线程
  3. 飞机大战boss入场代码_他又来了,BOSS直聘和国足谁先出线卡塔尔?
  4. 用数据挖掘来支持音乐创作
  5. 【NLP】Transformer大家庭简介!
  6. html5 摆动的花朵,CSS3实现一个旋转的花朵
  7. 第三次学JAVA再学不好就吃翔(part9)--基础语法之键盘录入
  8. 如何理解Mysql的索引及他们的原理--------二叉查找树和平衡二叉树和B树和B+树
  9. easyUI学习笔记二
  10. Linux oracle(常用命令)启动、停止、监听
  11. sql server managerment 给表加说明
  12. java+mysql学科竞赛管理系统(java,web)
  13. SuperMap WebGL拖拽式画圆和画矩形
  14. 交互式电子白板android,交互式电子白板_触摸屏电子白板_智能电子白板_GESEE国视科技...
  15. 年轻人,劝你不要做前端
  16. 动画 + 大白话讲清楚React渲染原理
  17. tensorflow的early stopping模型保存方式
  18. 张五常先生谈读书和思考的方法
  19. 《jdk8u源码分析》6.1.GetJREPath
  20. 自动驾驶系类文档(二)-- 定位模块(Location)

热门文章

  1. 华为智慧屏 鸿蒙如何获得,华为智慧屏鸿蒙UI系统获得2020德国红点设计大奖
  2. 黑苹果睡眠重启后死机_苹果8p突然黑屏,卡机,死机等怎么办?苹果8p维修指南
  3. Python生成二维列表
  4. 按键控制led灯python程序_按键控制led灯
  5. shell for循环遍历数组
  6. 二、BMZCTF-[easy_php]-[本地复现]-[简单的代码审计]
  7. 按头安利 好看又实用的摩托车壁纸素材看这里
  8. ▷Scratch课堂丨使用scratch画了一棵圣诞树的实例代码分享
  9. 什么是DDD分层架构
  10. 普通人如何不被 OpenAI 取代?