当前位置: 首页 > news >正文

线性表详细讲解

    • 2.1 线性表的定义和特点
    • 2.2 案例引入
    • 2.3 线程表的类型定义
    • 2.4 线性表的顺序表示和实现
      • 2.4.1 线性表的顺序存储表示
      • 2.4.2 线性表的结构类型定义
      • 2.4.3 顺序表基本操作的实现
      • 2.4.4 顺序表总结
    • 2.5 线性表的链式表示和实现
      • 2.5.1 线性表的链式存储表示
      • 2.5.2 单链表的实现
        • (1)单链表的基本操作
        • (2)单链表的取值
        • (3)单链表的查找
        • (4)单链表的插入
        • (5)单链表的删除
        • (6)单链表的建立
      • 2.5.3 循环链表的实现
      • 2.5.4 双向链表的实现
        • (1)双向链表的插入
        • (2)双向链表的删除
    • 2.6 顺序表和链表的比较
    • 2.7 线性表的应用
      • 2.7.1 线性表的合并
      • 2.7.2 有序表的合并
        • (1)顺序表实现
        • (2)链表实现

2.1 线性表的定义和特点

线性表示具有相同特性的数据元素的一个有限序列

image-20230719215210528

线性表的例子:

image-20230719215502348

同一线性表中的元素必定具有相同的特性,数据元素间的关系是线性关系。

2.2 案例引入

【案例2.1】

一元多项式的运算:实现俩个多项式加、减、乘运算

image-20230719220625861

我们可以将每个项的系数存到线程表,指数可以通过系数的下标隐含的表示。比如以下:

image-20230719220807337

以上是理想情况下,但如果像以下这种稀疏多项式:

image-20230719222401934

如果还是按照以上的方式存储,那么我们需要 20001个存储空间,但实际上却只存储了三个数据,非常浪费空间:

image-20230719222826774

因此我们可以利用一个二维数组,分别用来记录 系数指数,其他没有用的数据不要记录:

image-20230719223028196

代码表示

 int[][] table = {{1,0},{3,10000},{2,20000}};

【案例2.2】

稀疏多项式的运算

image-20230719223550574

  • 创建一个新数组c
  • 分别从头遍历线性表A和线性表B
    • 指数相同:将系数相加,若不为0,则将相加后的放到c中
    • 指数不同:将指数小的放到c中
  • 当其中一个遍历完毕后,剩余的依次放到 c 中。

image-20230719224641744

问题

这个数组C初始化多大合适呢? 如果初始化为俩个线性表大小的和,但实际可能会由于指数相等,用不了这么多的空间,因此会浪费空间。太小又会不够用。

因此对于顺序存储结构来说,他的缺点

  • 空间分配不够灵活
  • 运算空间复杂度高【需要借助数组c】

对于以上问题,我们可以使用链式存储结构解决。

image-20230719225513551

操作步骤和上面一样,区别就是不需要额外的存储空间,并且可以动态的分配存储空间:

image-20230719225623578

【案例2.3】

图书信息管理系统

image-20230719230213070

我们可以将左边的这张图书表看做为线程表,每本书的信息可以看做是一个元素。

与前俩个案例相比,该数据元素不再是一个简单的数据类型,而是一个复杂的对象,每一个对象包含:书号、书名、定价三个属性信息。

image-20230719230429589

image-20230719230542805

总结

  • 线性表中数据元素的类型可以为作简单类型也可以为复杂类型

  • 许多实际应用问题所涉的基本操作有很大相似性,不应为每个具体应用单独编写一个程序

  • 从具体应用中抽象出共性的逻辑结构和基本操作,然后实现其存储结构和基本操作

2.3 线程表的类型定义

线性表的定义

ADT List{数据对象: D={ai | ai ∈ ElemSet, i=1, 2,, n, n>=0} 数据关系: R=(<ai-1,ai> | ai-1,ai ∈D, i=2,, n}基本操作:InitList (&L)操作结果:构造一个空的线性表LDestroyList(&L)初始条件:线性表L已存在。操作结果:销毁线性表LClearList (&L)初始条件:线性表L已存在。操作结果:将L重置为空表。ListEmpty(L)初始条件:线性表L已存在。操作结果:若L为空表, 则返回true, 否则返回falseListLength(L)初始条件:线性表L已存在。操作结果:返回L中数据元素个数。GetElem(L,i,&e)初始条件:线性表L巳存在,且1<=i<=ListLength(L)。操作结果:用e返回L中第i个数据元素的值。LocateElem(L,e)初始条件:线性表L已存在。操作结果:返回L中第1个 值与e相同的元素在 L中的位置 。若这样的数据元素不存在 , 则返回值为0PriorElem(L,cur_e,&pre_e)初始条件:线性表L已存在。操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回其前驱,否则操作失败,pre_e无定义。NextElem(L,cur_e,&next_e)初始条件:线性表L已存在。操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回其后继,否则操作失败,next_e无定义。Listinsert(&L,i,e)初始条件:线性表L已存在,且1<=i<=ListLength(L)+1 操作结果:在 L中第1个位置之前插入新的数据元素 e, L的长度加1ListDelete(&L,i)初始条件:线性表L已存在且非空 ,且1<=i<=ListLength(L)操作结果:删除L的第1个数据元素,L的长度减1TraverseList(L)初始条件:线性表L已存在。操作结果:对线性表L进行遍历,在遍历过程中对 L的每个结点访问一次。
) ADT List

以上所提及的运算是逻辑结构上定义的运算。只要给出这些运算的功能是 “做什么”,至于"如何做"等实现细节,只有待确定了存储结构之后才考虑。

实现参考:2.4、2.5

2.4 线性表的顺序表示和实现

2.4.1 线性表的顺序存储表示

线性表俩种基本的存储结构:顺序存储、链式存储

顺序存储定义:把逻辑上相邻的数据元素存储在物理上相邻的存储单元中的存储结构中。

简单来说,逻辑上相邻的数据元素,在物理上也相邻

image-20230720164602342

【例子】

image-20230720164925823

image-20230720165004782

因此我们可以得知,顺序存储的最大特点就是 它会占用一片连续的存储空间(地址连续,依次存放),知道某一个元素的位置就可以计算出其他元素的位置(随机存取)

顺序表中元素存储位置的计算

image-20230720165201330

如果 a1~an每个元素占用8个存储单元, ai 存储的位置是 2000 单元,则 ai+1的存储位置是?

ai 的存储位置是:2000 ~ 2007, ai+1 的存储位置起始点为:2008

假设线性表的每个元素需占 l 个存储单元,则第 i+1个数据元素的存储位置和第i个数据元素的存储位置之间满足关系:

image-20230720165439817

由此,所有数据元素的存储位置均可由第一个数据元素的存储位置得到:

image-20230720165629092

2.4.2 线性表的结构类型定义

image-20230720170649578

在线性表定义中的删除操作是可以动态改变线性表的长度的,但是数组是固定长度,无法动态定义。因此我们可以用一个变量表示线性表的长度

知道了存储数据元素的存储结构,那么该如何定义线性表的结构类型(Java实现):

【多项式结构类型】

// 多项式非零项的定义
class Ele{private Float p; // 系数private Integer e; // 指数
}// 结构类型定义
class StructType {Ele[] elem; // 存储多项式元素int length; // 线性表长度int maxSize; // 数组初始化长度
}

【图书管理系统】

// 结构类型定义
class StructType {Book[] elem; // 存储图书信息元素int length; // 线性表长度int maxSize; // 数组初始化长度
}
// 图书信息对象
class Book {String no; // 图书编号String name; // 图书名Float price; // 图书价格
}

2.4.3 顺序表基本操作的实现

使用数组实现顺序表,它的起始下标是从0开始的,因此与逻辑位序相差1

image-20230720221021997

顺序表示意图

image-20230720221314793

用Java语言来解释的话,SqList 是一个类,L 是一个对象,也是一个顺序表,用来存储线性表的元素。

绿色部分存储的是线性表的元素,蓝色部分是线性表元素的个数。绿色部分+蓝色部分就是顺序表L。

image-20230720221632535

下面用Java实现基本操作操作

在 Java中,由于java的垃圾回收机制会自动释放不在使用的内存空间,无需我们自己操作。

// 定义结构类型
class StructTypeImplementation {// privateObject[] elem; // 存储线性表元素,自定义所需要的类型。int maxSize = 100; // 数组初始化大小int length;  // 线性表长度// 初始化public StructTypeImplementation(int maxSize) {this.elem = new Object[maxSize];this.maxSize = maxSize;this.length = 0;}// 获取线性表长度public int getLength(){return length;}// 判断是否为空public boolean isEmpty(){return  length == 0;}// 获取第i个位置的元素【随机存取】// 这也是顺序表最大的特点public Object getElem(int i){// 如果获取的位置小于1或者大于线性表的长度,那么这个参数是不合法的if (i < 1 || i > length) throw new  IllegalArgumentException("参数错误");return elem[i-1];}// 查找某个元素所在的位置。 -1表示没有找到public int LocateElem(Object keyword){for (int i = 0; i < elem.length; i++) {if (elem[i] == keyword) return i+1;}return -1;}
}

以上基本操作非常简单,就不一一讲述…

顺序表的插入算法

image-20230721144208921

线性表的插入运算是指在表的第i (i <= i <= n+1) 个位置上,插入一个新结点e,使长度为n的线性表(a1,…, ai -1,ai,…, an) 变成长度为n+1 的线性表(a1,…, ai -1, e, ai,…, an)

image-20230721104718713

假如想要将 f 插入第3个位置上(下标为2),需要将插入位置及之后的元素往后移,也就是下标范围: 【 2~n-1】 的元素。【n就是线性表的长度,对应代码中length变量】

将 e 移动下标为5 的位置上,d移动到下标为 4 的位置上,c 移动到下标为 3 的位置上。留出下标为 2 的位置,将 f 插入进去。

最后将线性表长度 n+ 1

image-20230721112137598

总结起来就是,假设想要往第 i 个位置上插入元素,就要将下标在 [ i-1 ,n- 1 ]这个范围内的元素往后移。空出第 i 个位置

image-20230721112626265

算法思想

  • 判断插入位置是否合法
    • 合法位置:1 ~ n+1
  • 判断顺序表是否已满,若已满返回ERROR
  • 将第 n 至 i 位的元素依次向后移动一个位置,空出第 i 个位置
  • 将要插入的元素 放入第 i 个位置,线性表长度+1

注意:位置和下标相差1,比如:第3个位置的元素它的下标为2

Java实现

    // 将元素 e 插入 index上public boolean insertElem(Object e, int index) {// 1、判断位置是否合法。 1 <= index <= length + 1if (index < 1 || index > getLength() + 1) return false;// 2、判断数组是否已经满了if (length == maxSize) return  false;// 移动 index-1 ~ length-1 的元素for (int i = length-1; i >= index -1; i--) {// 往后移动一位elem[i+1] = elem[i];}elem[index-1] = e;// 线性表长度+1++length;return true;}

时间复杂度分析

代码中执行次数最多的语句时 for 循环体,消耗的时间主要在移动元素上。移动的次数取决于插入的位置:

  • 若插入在尾结点之后,则根本无需移动 (特别快)
  • 若插入在首结点之前,则表中元素全部后移 (特别慢)
  • 若要考虑在各种位置插入 (共n+1种可能)的平均移动次数,该如何计算?

image-20230721143256621

平均时间复杂度为:O(n)

顺序表的删除

image-20230721144201950

线性表的删除运算是指将表的第 i (1 <= i<=n)个结点删除使长度为n 的线性表(a1, …, ai-1, ai , ai +1 …, an)变成长度为n-1的线性表 (a1,… ai-1 ai+1…, an)

image-20230721143808895

由于使用数组模拟线性表,无法直接删除一个元素,只能将删除元素后边的元素,往前移进行覆盖。

以下图为例,想要删除第3个位置的元素,需要将 第4、5个位置上元素的往前移。

image-20230721153113462

算法思想

  • 检查删除位置 i 是否合法,范围应该在: 1<= i <= n
  • 如果需要返回删除的元素,将元素赋值给一个变量。如果不需要此步骤可省略
  • 将第 [i + 1, n] 的元素往前移
  • 顺序表长度n-1

注意:位置和下标相差1,比如:第3个位置的元素它的下标为2

Java代码实现

    // 删除index位置的元素,并返回该位置的元素public Object deleteElem(int index){// 1、检查删除位置是否合法 1<=index<=lengthif (index < 1 || index > length) return false;// 删除位置的元素,提前赋值给resObject res  = elem[index-1];// 2、将[index,length)位置的元素往前移for (int i = index; i < length; i++) {elem[i-1] = elem[i];}--length;return res;}

2.4.4 顺序表总结

1、利用数据元素的存储位置表示线性表中相邻数据元素之间的前后关系,即线性表的逻辑结构与存储结构一致

2、在访问线性表时,可以快速地计算出任何一个数据元素的存储地址,一因此可以粗略的认为,访问每个元素所花时间相等

3、这种存取元素的方法称为随机存取

image-20230721154128099

  • 优点:

    • 存储密度大 (结点本身所占存储量/结点结构所占存储量)
    • 可以随机存取表中任一元素
  • 缺点:

    • 在插入、删除某一元素时,需要移动大量元素
    • 浪费存储空间
    • 属于静态存储形式数据元素的个数不能自由扩充

链式存储解决了以上的缺点~!

2.5 线性表的链式表示和实现

2.5.1 线性表的链式存储表示

结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻,线性表的链式表示又称为非顺序映像链式映像

image-20230721161200733

链表的存储熟顺序是任意的,每一个结点中不仅包括数据本身,还包括指向下一结点的指针,这样就可以将所有数据串联起来,形成一个链。

^ 表示指针域为NULL

image-20230721161339492

各结点由俩个域组成:

数据域:存储元素数值数据

指针域:存储直接后继结点的存储位置

image-20230721161929092

链式存储相关术语

1、结点: 数据元素的存储映像。由数据域指针域两部分组成

2、链表: n 个结点由指针链组成一个链表。它是线性表的链式存储映像,称为线性表的链式存储结构

image-20230721162358269

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

结点只有一个指针域的链表,称为单链表或线性链表

image-20230721162749709

结点有两个指针域的链表,称为双链表

image-20230721162806905

首尾相接的链表称为循环链表。单链表、双向链表的尾结点的指针域为NULL,而循环链表尾结点的指针域是头结点的地址。

image-20230721162818353

头指针、头结点、首元结点

头指针: 是指向链表中第一个结点的指针,头指针是一个链表中必须存在的,指明了链表的存储地址。头指针就是链表。

首元结点:是指链表中存储第一个数据元素a的结点

头结点:是在链表的首元结点之前附设的一个结点

image-20230721163242125

链式存储俩种变现形式

不带头结点:

image-20230721163503803

带头结点:

image-20230721163524004

如何表示空表

不带头结点:头指针为空时表示空表

image-20230721163845905

带头结点: 头结点的指针域为空

image-20230721164358739

image-20230721164434198

带头结点有什么好处

1、便于首元结点的处理

首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理

2、便于空表和非空表的统一处理

无论链表是否为空,头指针都是指向头结点的非空指针因此空表和非空表的处理也就统一了

假设 L 为单链表的头指针,它应该指向首元结点,则当单链表为长度 n 为 0 的空表时, L 指针为空(判定空表的条件可记为:L== NULL)。

增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。如图所示 的非空单链表,头指针指向头结点。若为空表,则头结点的指针域为空(判定空表的条件可记为: L ->next== NULL)

image-20230721165420881

头结点的数据域内装的是什么

头结点的数据域可以为空,也可存放线性表长康等附加信息,但此结点不能计入链表长度值

image-20230721165821658

链式存储结构特点

(1)结点在存储器中的位置是任意的即逻辑上相邻的数据元素在物理上不一定相邻

(2) 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等

这种存取元素的方法被称为顺序存取法

顺序表:随机存取,顺序存储

链式表:随机存储,顺序存取

不要搞混!!!!

2.5.2 单链表的实现

(1)单链表的基本操作

单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名。若头指针名是L,则把链表称为表L

image-20230721172933077

单链表的初始化(带头结点的单链表)

即构造一个如图的空表

// 单向链表的一些基本操作
class LinkedList {Node dummyHead; // 头结点// 初始化链表public LinkedList() {// 头结点,数据域可以不存储值或者存储任意值this.dummyHead = new Node(-1);}
}// 定义结点
class Node {Object data; // 数据域Node next;  // 指针域// 初始化一个节点public Node(Object data) {this.data = data;this.next = null;}
}

在 Java中存在自动垃圾回收机制,不需要使用链表时,只需要将链表设置为 NULL 即可。

清空单链表

Java中清空单链表只需要将头结点设置为null,与其他结点不可达,垃圾回收机制会自动回收其他结点

    public void clear(){dummyHead = null;}

求链表的表长

从首元结点开始,依次遍历所有结点

  • 用一个引用p【C中的指针类似于Java中的引用】指向链表的首元结点
  • 当p的next域不为空,说明有下一个结点,i++ 并且移动p指向下一个结点【p=p.next】
  • 当p的next域为空,说明p指向了最后一个结点,结束循环。

image-20230724215954251

代码实现

    // 获取链表的长度public int length() {// 记录链表长度int i = 0;// p指向首元结点Node p = dummyHead.next;while (p != null) {i++;// 移动pp = p.next;}return i;}

(2)单链表的取值

获取单链表中第 i 个元素的内容

  • 初始化变量: j =1 :表示遍历到第j结点。p 指向第一个首元结点。
  • 循环遍历:从首元结点开始循环遍历,循环终止条件:p = null
    • 比较 j 和 i 的值,相等则返回 p 指向结点的 data 域
    • 不相等,则移动p 并且将 j++,继续判断 j 和 i 的值

特殊情况:在循环之前,必须要校验i 的合法性,也就是说想要查找的元素必须在: [j, length] 这个区间之内。length为链表的长度。

image-20230724221520703

通过以上分析,其实也可以得知,链表不是随机存取结构,想要获取某个结点必须从首元结点开始遍历

代码实现

   public Object getELement(int i){// 初始化int j = 1;Node p = dummyHead.next;// 循环遍历while(p != null) {if (j == i) {return p.data;}// 移动p指向下一个结点p = p.next;j++;}//  可以在这里直接抛出一个异常, 代替i的校验//  因为范围内的i必然会找到throw new IndexOutOfBoundsException("Index i is out of bounds");}

(3)单链表的查找

获取与 e 值相等的结点的位置

  • 初始化变量: j =1 :表示遍历到第j结点。p 指向第一个首元结点。
  • 循环遍历:从首元结点开始循环遍历,循环终止条件:p= null
    • 比较p.data 和 e ,相等则返回 j
    • 不相等,则移动p 并且将 j++,继续判断 data域和e的值

代码实现:

    public int locateEle(Object e){// 初始化int j = 1;Node p = dummyHead.next;while(p != null) {if (p.data == e) {return j;}// 移动p指向下一个结点p = p.next;j++;}// 没有找到return -1;}

时间复杂度

因线性表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)

(4)单链表的插入

在第i个结点前插入值为e的新结点

步骤

  • 首先找到 ai-1 的存储位置p
  • 生成一个数据域为e的新结点S
  • 插入新结点
    • 1、新结点的指针域指向结点 ai , s.next = p.next
    • 2、结点 a i-1 的指针域指向新结点, p.next = s

image-20230724231112542

特殊情况: i 的范围 [j, length+1]

注意: 2 和 1 不能互换,如果先执行 2 ,ai 的地址会被 p.next = s 覆盖

代码实现:

   public void insertELe(int i,Object e) {// 初始化,p指向头结点int j  = 0;Node p = dummyHead;while(p != null) {// 找到插入位置的前一个结点if (j==i-1){// 创建新结点Node node = new Node(e);node.next = p.next;p.next = node;return;}p = p.next;j++;}// i 参数错误if (j < i-1 || i > length()+1) {throw new IllegalArgumentException("Index i is out of bounds");}}

(5)单链表的删除

删除第i个结点

image-20230725222137592

步骤

  • 仍然是先找到 ai-1 的存储位置p,如果有需要则保存 ai 的值
  • 将 ai-1 的 指针域指向 ai+1 【p.next = p.next.next】
  • 将 ai 结点置空,释放 空间

image-20230725222721526

代码实现

    public boolean delEle(int i) {// 初始化:指向第一个结点int j = 0;Node p = dummyHead;while(p != null) {// 找到删除结点的前一个结点if (j == i -1) {// 删除p.next = p.next.next;p.next.next = null;return true;}// 后移p = p.next;j++;}return false;}

删除和插入时间复杂度

因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。

但是,如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n)

(6)单链表的建立

头插法

将元素插入在链表头部,也叫前插法。

从最后一个结点开始,依次将各结点插入到链表的前端

image-20230725224323029

步骤

  • 将新结点的 指针域 ,指向头结点 指针域
  • 将头结点的 指针域 指向新结点

代码实现:

    public void createList_head(Node node){node.next = dummyHead.next;dummyHead.next = node;}

尾插法

将新结点插入链表的尾部,也叫后插法

  • 使用一个变量p指向头结点,然后循环将p指向最后一个结点
  • 将p结点的指针域指向新结点
    public void createList_tail(Node node) {Node p =  dummyHead;// 指向最后一个结点while(p.next != null) {p = p.next;}p.next = node;}

时间复杂度:O(n) , 需要将 p 移动到链表的尾部

2.5.3 循环链表的实现

循环链表:是一种头尾相接的链表。(即:表中最后一个结点的指针域指向头结点,形成一个环)

image-20230726221214240

优点: 从表中任一结点出发均可找到表中的其他结点

注意

在单链表中判断非空时的条件为 p.next 是否为空,而在循环链表中则要判断 p.next 是否等于头结点。

单链表的循环条件判断 p!=null 或者 p.next != null

循环链表的循环条件判断 p!= head 或者 p.next != head

带尾指针的循环链表

image-20230726223354821

举例

尾指针循环链表的合并(将Tb合并在Ta之后)

image-20230726223815721

分析有哪些操作?

  • 将Ta的尾结点指向Tb的首元结点
  • 将Tb的尾结点指向Ta的头结点
  • 将Ta的尾结点设置成Tb的尾结点
  • 将Tb的头结点置空

image-20230726223931687

代码实现

public class CircularLinkedList {public static void main(String[] args) {CircularList ta = new CircularList();ta.insertTail(new CircularNode(1));ta.insertTail(new CircularNode(2));ta.insertTail(new CircularNode(3));// ta.print();CircularList tb = new CircularList();tb.insertTail(new CircularNode(4));tb.insertTail(new CircularNode(5));tb.insertTail(new CircularNode(6));// tb.print();ta.merge(tb);ta.print();}
}// 带尾指针循环链表
class CircularList {CircularNode dummyHead; // 头结点CircularNode tail; // 尾结点/*** 初始化链表* */public CircularList() {// 头结点,数据域可以不存储值或者存储任意值this.dummyHead = new CircularNode(-1);// 初始状态,尾结点也是 头结点,形成一个闭环this.tail = this.dummyHead;}/*其他操作省略...*/// 尾插法public void insertTail(CircularNode  node){tail.next = node;node.next = dummyHead;tail = node; // 更新尾结点}// 打印链表public void print(){CircularNode current = dummyHead.next;while(current != dummyHead) {// !!! 这里如果打印current会报错栈溢出// 原因是: 打印current会调用CircularNode中的toString,而toSting中的next又会调用CircularNode中的toString,无限循环System.out.print(current.data + "\t");current = current.next;}}// 合并俩个循环链表(不考虑空表的情况)public  void merge(CircularList tb) {// 将ta的尾节点指向tb的首元结点tail.next = tb.dummyHead.next;// 将 tb 的尾结点指向 ta 的头结点tb.tail.next = dummyHead;// 将ta的尾结点设置成tb的尾结点tail = tb.tail;// 将tb头结点置空tb.dummyHead = null;}}
class CircularNode {Object data; // 数据域CircularNode next;  // 指针域// 初始化一个节点public CircularNode(Object data) {this.data = data;this.next = null;}@Overridepublic String toString() {return "CircularNode{" +"data=" + data +", next=" + next +'}';}
}

2.5.4 双向链表的实现

以上讨论的链式存储结构的结点中 只有一个指示 直接后继的指针域, 由此, 从某个结点 出发 只能顺指针向后寻查其他结点。 若要寻查结点的直接前驱,则必须从表头指针出发。 换句话说, 在单链表中,查找直接后继结点的执行时间为 0(1), 而查找直接前驱的执行时间为O(n)。

为克服 单链表这种单向性的缺点,可利用双向链表 (Double Linked List)。

双向链表: 在单链表的每个结点里在增加一个指向其直接前驱的指针域 prior,这样链表中就形成了有俩个方向不同的链,故称为双向链表

双向链表的结构:

prior 指向前一个结点

next 指向后一个结点

image-20230727221257169

空表时,prior 和 next域都为空

image-20230727221531832

双向链表的定义

class DNode {Object data;DNode next; // 指向后继结点指针域DNode prior; //  指向前驱结点的指针域// 初始化一个结点public DNode(Object data) {this.data = data;this.next = null;this.prior = null}@Overridepublic String toString() {return "DNode{" +"data=" + data +", next=" + next +", prior=" + prior +'}';}
}

双向循环链表

和单链的循环表类似,双向链表也可以有循环表。

  • 让头结点的前驱指针指向链表的最后一个结点
  • 让最后一个结点的后继指针指向头结点

image-20230727221448562

双向链表的结构的对称性

假设指针p指向某一个结点

image-20230727221723134

image-20230727221729751

在双向链表中有些操作(如: istLenth、GetElem等),因仅涉及个方向的指针,故它们的算法与线性链表的相同。但在插入、删除时,则需同时修改两个方向上的指针,两者的操作的时间复杂度均为 On

(1)双向链表的插入

在第i个位置上插入新结点

与单链表相比,除了要处理next域,还要处理 prior 域。

单链表插入需要找到 i-1 个结点,而双向链表直接找到第 i 个结点,通过prior 域找到 i-1 的结点

image-20230727223323727

代码实现:

public class DoubleLinkedListDemo {public static void main(String[] args) {DoubleLinkedList doubleLinkedList = new DoubleLinkedList();doubleLinkedList.insert(1,new DNode(1));doubleLinkedList.insert(1,new DNode(2));doubleLinkedList.insert(1,new DNode(3));doubleLinkedList.insert(3,new DNode(4));// 插入位置不合法doubleLinkedList.insert(5,new DNode(4));doubleLinkedList.print();System.out.println(doubleLinkedList.getLength());}
}
// 双向链表
class DoubleLinkedList {// 头结点DNode dummyHead;// 初始化链表public DoubleLinkedList() {this.dummyHead = new DNode(-1);}/*** 获取链表长度* */public int getLength(){int i = 0;// 指向第一个首元结点DNode p = dummyHead.next;while (p != null) {i++;p = p.next;}return  i ;}/*** 双向链表的插入* 在第 i 个位置插入 node 结点* */public void insert(int i,DNode node) {int j = 0;DNode p = dummyHead;// 判断插入的是否是第一个结点if (p.next == null && i == 1 ) {p.next = node;node.prior = p;return;}// 判断插入位置是否合理if (i <= 0 || i>getLength()) {throw new IndexOutOfBoundsException("Index i is out of bounds");}while(p != null) {// 无需找到 i-1 个位置,直接找到第i个位置if (j == i) {// 插入结点四步操作node.prior = p.prior;p.prior.next = node;node.next = p;p.prior = node ;return;}p = p.next;j++;}}/*** 打印结点* */public void print(){DNode p = dummyHead.next;while(p != null) {System.out.print(p.data + "  ");p = p.next;}}
}class DNode {Object data;DNode next; // 指向后继结点指针域DNode prior; //  指向前驱结点的指针域// 初始化一个结点public DNode(Object data) {this.data = data;this.next = null;this.prior = null;}@Overridepublic String toString() {return "DNode{" +"data=" + data +", next=" + next +", prior=" + prior +'}';}
}

(2)双向链表的删除

删除第 i 个位置的结点

image-20230727230743475

代码实现:

    public boolean delEle(int i) {// 初始化:指向第一个结点int j = 0;DNode p = dummyHead;while(p != null) {// 找到删除结点的前一个结点if (j == i ) {// 删除p.next.prior = p.prior;p.prior.next = p.next;return true;}// 后移p = p.next;j++;}return false;}

2.6 顺序表和链表的比较

链式存储结构的优点

  • 结点空间可以动态申请和释放
  • 数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素.

链式存储结构的缺点

存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。

image-20230730150917505

image-20230730151015761

链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该结点,这增加了算法的复杂度。

顺序表和链表的比较

image-20230730151234417

2.7 线性表的应用

image-20230730151701670

2.7.1 线性表的合并

算法步骤

  • 遍历Lb,在 La 中查找是否存在Lb中的每一个元素
  • 如果不存在,就将该元素插入到La的后面

单链表代码实现

      // 构建链表LaLinkedList La = new LinkedList();La.insertELe(1,7);La.insertELe(2,5);La.insertELe(3,3);La.insertELe(4,11);// 构建链表LbLinkedList Lb = new LinkedList();Lb.insertELe(1,2);Lb.insertELe(2,6);Lb.insertELe(3,3);// 将Lb合并到Lafor (int i = 1; i <= Lb.length(); i++) {// 遍历Lb,查看La中是否存在Lb中的元素int index = La.locateEle(Lb.getELe(i));if (index == -1) {// 说明没有元素,将元素插入La的尾部La.createList_tail(new Node(Lb.getELe(i)));}}// 遍历LaLa.print();

2.7.2 有序表的合并

image-20230730153405071

(1)顺序表实现

算法步骤

  • 创建一个空表Lc
  • 依次从 La、Lb中摘取元素比较小的结点,插入到Lc的尾部,直到一个表为空
  • 将不为空的表中的元素全部插入到 Lc 的尾部
// LaStructTypeImplementation La = new StructTypeImplementation(3);La.insertElem(1,1);La.insertElem(7,2);La.insertElem(8,3);// LbStructTypeImplementation Lb = new StructTypeImplementation(6);Lb.insertElem(2,1);Lb.insertElem(4,2);Lb.insertElem(6,3);Lb.insertElem(8,4);Lb.insertElem(10,5);Lb.insertElem(11,6);// 创建一个空表LcStructTypeImplementation Lc = new StructTypeImplementation(La.getLength() +Lb.getLength());// 使用俩个指针,分别指向La、Lbint i = 1;int j = 1;// 只要有一个指向链表的尾端就停止while (i <= La.getLength() && j <= Lb.getLength()) {// 获取俩个链表中的元素Integer a = (Integer) La.getElem(i);Integer b = (Integer) Lb.getElem(j);if (a < b) {// 将a插入Lc尾部中Lc.insertTail(a);// 将La的指针向后移动i++;}else {// 将b插入Lc中Lc.insertTail(b);// 将Lb的指针向后移动j++;}}if (i > La.getLength()) {while(j <= Lb.getLength()) {// 说明是La空了,直接将Lb尾部的结点插入到LcLc.insertTail(Lb.getElem(j));j++;}}else {while(j <= Lb.getLength()) {// 说明是Lb空了,直接将La尾部的结点插入到LcLc.insertTail(La.getElem(j));i++;}}Lc.print();

插入尾部的方法:其余方法在线性表的顺序实现都说详细说明。

    // 将元素插入到表的尾部public void insertTail(Object data) {for (int i = 0; i < this.elem.length; i++) {if (elem[i] == null) {System.out.println("插入的值" + data);elem[i] = data;return;}}}

时间复杂度:O(La.length + Lb.length)

空间复杂度:O(La.length + Lb.length)

(2)链表实现

算法步骤

  • 使用La或者Lb作为最终结果的链表,比如我用 La,下面为了区分,将最终的链表以 Lc 表示,其实就是La
  • 使用三个指针,分别指向 La、Lb首元结点,Lc 的头结点
  • 比较La、Lb中的每一个结点的 data 域,将较小的结点挂在 Lc 后边。将 pc 指向这个较小的结点,同时将 pc,pa或者pb后移
  • 最后将不为空的链表直接挂载 Lc 的后面。

步骤分析

初始状态下:pc指向pa的头结点,pa、pb分别指向各自的首元结点

image-20230730165748586

比较pa、pb指向结点的data域: pa.data < pb.data

将pa指向的结点挂到pc后边,同时更新pc、pa,具体操作为:

pc.next = pa;
pc= pa;
pa = pa.next;

image-20230730170010489

此时继续循环上面的步骤…直到 La 链表为空,将Lb剩下的结点10、11挂在pc后边,具体的操作为:

// 如果pa不为空挂pa,否则挂pb
pc.next = pa != null ? pa :pb;

image-20230730170233739

代码实现

      // 构建链表LaLinkedList La = new LinkedList();La.insertELe(1,1);La.insertELe(2,7);La.insertELe(3,8);// 构建链表LbLinkedList Lb = new LinkedList();Lb.insertELe(1,2);Lb.insertELe(2,4);Lb.insertELe(3,6);Lb.insertELe(4,8);Lb.insertELe(5,10);Lb.insertELe(6,11);// 初始化指针Node pa = La.dummyHead.next;Node pb = Lb.dummyHead.next;Node pc = La.dummyHead;while(pa != null && pb != null) {if (((Integer) pa.data) < ((Integer) pb.data)) {// pa小,将pa挂载到pc后边pc.next = pa;// 将pc后移pc = pa;// 将pa指向下一个结点pa = pa.next;}else {// pb小,将pb挂载到pc后边pc.next = pb;// 将pc后移pc = pb;// 将pb指向下一个结点pb = pb.next;}}// 最后将不为空的链表,挂载到pc后边// 这个语句的意思: pa!=null将pa挂在到pc后边,否则挂pbpc.next = pa != null ? pa :pb;// 打印La.print();

相关文章:

线性表详细讲解

2.1 线性表的定义和特点2.2 案例引入2.3 线程表的类型定义2.4 线性表的顺序表示和实现2.4.1 线性表的顺序存储表示2.4.2 线性表的结构类型定义2.4.3 顺序表基本操作的实现2.4.4 顺序表总结 2.5 线性表的链式表示和实现2.5.1 线性表的链式存储表示2.5.2 单链表的实现&#xff08…...

代码随想录算法训练营day45

文章目录 Day45爬楼梯题目思路代码 零钱兑换题目思路代码 完全平方数题目思路代码 Day45 爬楼梯 70. 爬楼梯 - 力扣&#xff08;LeetCode&#xff09; 题目 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢…...

机器学习深度学习——softmax回归(上)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——线性回归的简洁实现 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你们有所…...

基于express调用chatgpt文字流输出和有道智云语音合成

express是基于node.js的一个web框架&#xff0c;可以更加简洁的去创建一个后台服务&#xff0c;由于项目的需要&#xff0c;引入和typescript&#xff0c;经过几天的努力实现了chatgpt文字流输出有道智云语音合成的结合&#xff08;略有遗憾&#xff09;&#xff0c;下面我记载…...

(学习笔记-内存管理)内存分段、分页、管理与布局

内存分段 程序是由若干个逻辑分段组成的&#xff0c;比如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的&#xff0c;所以就用分段的形式把这些分段分离出来。 分段机制下&#xff0c;虚拟地址和物理地址是如何映射的&#xff1f; 分段机制下的虚拟地址由…...

PHP使用Redis实战实录1:宝塔环境搭建、6379端口配置、Redis服务启动失败解决方案

宝塔环境搭建、6379端口配置、Redis服务启动失败解决方案 前言一、Redis安装部署1.安装Redis2.php安装Redis扩展3.启动Redis 二、避坑指南1.6379端口配置2.Redis服务启动&#xff08;1&#xff09;Redis服务启动失败&#xff08;2&#xff09;Redis启动日志排查&#xff08;3&a…...

【数据结构】这堆是什么

目录 1.二叉树的顺序结构 2.堆的概念及结构 3.堆的实现 3.1 向上调整算法与向下调整算法 3.2 堆的创建 3.3 建堆的空间复杂度 3.4 堆的插入 3.5 堆的删除 3.6 堆的代码的实现 4.堆的应用 4.1 堆排序 4.2 TOP-K问题 首先&#xff0c;堆是一种数据结构&#xff0c;一种特…...

FFmpeg 音视频开发工具

目录 FFmpeg 下载与安装 ffmpeg 使用快速入门 ffplay 使用快速入门 FFmpeg 全套下载与安装 1、FFmpeg 是处理音频、视频、字幕和相关元数据等多媒体内容的库和工具的集合。一个完整的跨平台解决方案&#xff0c;用于录制、转换和流式传输音频和视频。 官网&#xff1a;http…...

Go 语言 select 都能做什么?

原文链接&#xff1a; Go 语言 select 都能做什么&#xff1f; 在 Go 语言中&#xff0c;select 是一个关键字&#xff0c;用于监听和 channel 有关的 IO 操作。 通过 select 语句&#xff0c;我们可以同时监听多个 channel&#xff0c;并在其中任意一个 channel 就绪时进行相…...

Hive之窗口函数lag()/lead()

一、函数介绍 lag()与lead函数是跟偏移量相关的两个分析函数 通过这两个函数可以在一次查询中取出同一字段的前N行的数据(lag)和后N行的数据(lead)作为独立的列,从而更方便地进行进行数据过滤&#xff0c;该操作可代替表的自联接&#xff0c;且效率更高 lag()/lead() lag(c…...

Vite+Typescript+Vue3学习笔记

ViteTypescriptVue3学习笔记 1、项目搭建 1.1、创建项目(yarn) D:\WebstromProject>yarn create vite yarn create v1.22.19 [1/4] Resolving packages... [2/4] Fetching packages... [3/4] Linking dependencies... [4/4] Building fresh packages...success Installed…...

二、SQL-6.DCL-2).权限控制

*是数据库和表的通配符&#xff0c;出现在数据库位置上表示所有数据库&#xff0c;出现在表名位置上&#xff0c;表示所有表 %是主机名的通配符&#xff0c;表示所有主机。 e.g.所有数据库&#xff08;*&#xff09;的所有表&#xff08;*&#xff09;的所有权限&#xff08;a…...

[OpenStack] GPU透传

GPU透传本质就是PCI设备透传&#xff0c;不算是什么新技术。之前按照网上方法都没啥问题&#xff0c;但是这次测试NVIDIA A100遇到坑了。 首先是禁用nouveau 把intel_iommuon rdblacklistnouveau写入/etc/default/grub的cmdline&#xff0c;然后grub2-mkconfig -o /etc/grub2.c…...

无涯教程-jQuery - Progressbar组件函数

小部件进度条功能可与JqueryUI中的小部件一起使用。一个简单的进度条显示有关进度的信息。一个简单的进度条如下所示。 Progressbar - 语法 $( "#progressbar" ).progressbar({value: 37 }); Progressbar - 示例 以下是显示进度条用法的简单示例- <!doctype …...

[SQL挖掘机] - 窗口函数 - rank

介绍: rank() 是一种常用的窗口函数&#xff0c;它为结果集中的每一行分配一个排名&#xff08;rank&#xff09;。这个排名基于指定的排序顺序&#xff0c;并且在遇到相同的值时&#xff0c;会跳过相同的排名。 用法: rank() 函数的语法如下&#xff1a; rank() over ([pa…...

VBAC多层防火墙技术的研究-状态检测

黑客技术的提升和黑客工具的泛滥,造成大量的企业、机构和个人的电脑系统遭受程度不同的入侵和攻击,或面临随时被攻击的危险。迫使大家不得不加强对自身电脑网络系统的安全防护,根据系统管理者设定的安全规则把守企业网络,提供强大的、应用选通、信息过滤、流量控制、网络侦…...

PHP8的数据类型-PHP8知识详解

在PHP8中&#xff0c;变量不需要事先声明&#xff0c;赋值即声明。 不同的数据类型其实就是所储存数据的不同种类。在PHP8.0、8.1中都有所增加。以下是PHP8的15种数据类型&#xff1a; 1、字符串&#xff08;String&#xff09;&#xff1a;用于存储文本数据&#xff0c;可以使…...

明晚直播:可重构计算芯片的AI创新应用分享!

大模型技术的不断升级及应用落地&#xff0c;正在推动人工智能技术发展进入新的阶段&#xff0c;而智能化快速增长和发展的市场对芯片提出了更高的要求&#xff1a;高算力、高性能、灵活性、安全性。可重构计算区别于传统CPU、GPU&#xff0c;以指令驱动的串行执行方式&#xf…...

flask 点赞系统

dianzan.html页面 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>点赞系统</title> </head> <body><h2>这是一个点赞系统</h2><table border"1"><…...

关于Java的多线程实现

多线程介绍 进程&#xff1a;进程指正在运行的程序。确切的来说&#xff0c;当一个程序进入内存运行&#xff0c;即变成一个进程&#xff0c;进程是处于运行过程中的程序&#xff0c;并且具有一定独立功能。 线程&#xff1a;线程是进程中的一个执行单元&#xff0c;负责当前进…...

如何判断某个视频是深度伪造的?

目录 一、前言 二、仔细检查面部动作 三、声音可以提供线索 四、观察视频中人物的身体姿势 五、小心无意义的词语 深造伪造危险吗&#xff1f; 一、前言 制作深度伪造视频就像在Word文档中编辑文本一样简单。换句话说&#xff0c;您可以拍下任何人的视频&#xff0c;让他…...

ESP32(MicroPython) 四足机器人(一)

最近决定研究一下四足机器人&#xff0c;但市面上的产品&#xff0c;要么性价比低&#xff0c;要么性能达不到要求。本人就另外买了零件&#xff0c;安装到之前的一个麦克纳姆轮底盘的底板上。&#xff08;轮子作为装饰&#xff0c;使用铜柱固定&#xff09; 舵机使用MG996R&a…...

力扣刷题记录---利用python实现链表的基本操作

文章目录 前言一、利用python实现链表的基本操作1.节点的定义使用类实现&#xff1a;1.链表的定义使用类实现&#xff1a;3.判断是否为空函数实现&#xff1a;4.链表长度函数实现&#xff1a;5.遍历链表函数实现&#xff1a;6.头插法函数实现&#xff1a;7.尾插法函数实现&…...

OpenAI重磅官宣ChatGPT安卓版本周发布,现已开启下载预约,附详细预约教程

7月22号&#xff0c;OpenAI 突然宣布&#xff0c;安卓版 ChatGPT 将在下周发布&#xff01;换句话说&#xff0c;本周安卓版 ChatGPT正式上线&#xff01; 最早&#xff0c;ChatGPT仅有网页版。 今年5月&#xff0c;iOS版ChatGPT正式发布&#xff0c;当时OpenAI表示Android版将…...

PHP 支付宝支付、订阅支付(周期扣款)整理汇总

最近项目中需要使用支付宝的周期扣款&#xff0c;整理一下各种封装方法 APP支付&#xff08;服务端&#xff09; /******************************************************* 调用方法******************************************************/function test_pay(){$isSubscri…...

python-pytorch基础之神经网络回归

这里写目录标题 定义数据集定义函数生成数据集 使用Dataloader加载dataset定义神经网络定义实例化查看是否是输出的一个 训练编写trian方法训练并保存模型 测试模型结果构造数据测试结论 定义数据集 import torch import random定义函数 # 生成数据 def get_rancledata():wid…...

linux中通过.desktop文件执行bash命令打开chrome浏览器并传参

.desktop 文件介绍 Ecex 参数介绍 Code 描述 %f %f指向临时文件。用于不了解URL语法的程序。 %F 文件列表。用于可以一次打开多个本地文件的应用程序。每个文件作为单独的参数传递给可执行程序。 %u 单一的URL或者本地文件 %U %u的复数 %i 如果Icon 为空,不应该填写此参数。…...

ChatGPT的应用与发展趋势:解析人工智能的新风口

目录 优势 应用领域 发展趋势 总结 在人工智能技术迅猛发展的时代&#xff0c;自然语言处理系统的提升一直是研究者们追求的目标。作为人工智能领域的重要突破之一&#xff0c;ChatGPT以其出色的语言模型和交互能力&#xff0c;在智能对话领域取得了重要的进展。 ChatGPT是…...

使用maven打jar包时,如何只把依赖的其它jar中的类打进jar包,没有依赖的其它jar包的类文件不打进来?

简介 使用Maven打包时&#xff0c;默认情况下&#xff0c;所有依赖的jar包都会被打包到生成的jar文件中。 如果只想将依赖的其他jar中的类文件打进来&#xff0c;而不包含其它jar包&#xff0c;可以使用Maven的 maven-shade-plugin插件进行配置。 步骤 以下是一个示例配置&…...

arm neon/fpu/mfloat

neon官网介绍: Arm Neon technology is an advanced Single Instruction Multiple Data (SIMD) architecture extension for the A-profile and R-profile processors. Neon technology is a packed SIMD architecture. Neon registers are considered as vectors of elements …...

淘宝网站建设概要/知乎关键词排名

科技实验报告一、定义与作用实验报告&#xff0c;就是在某项科研活动或专业学习中&#xff0c;实验者把实验的目的、方法。步骤、结果等&#xff0c;用简洁的语言写成书面报告。实验报告必须在科学实验的基础上进行。成功的或失败的实验结果的记载&#xff0c;有利于不断积累研…...

建站系统下载 discuz/网络代运营推广

$x array(1,2,3,4,5); var_dump($x);//除去下标在为3底下的值 unset($x[3]); var_dump($x);//除去下标为3的值后面重新排序 $x array_values($x); var_dump($x);...

黄石做企业网站/成人就业技术培训机构

本文转载自News BTC对CROSS的特别报道。以下为翻译的原文&#xff1a; 拍卖是财产权利转让的最古老方式之一。自从人类有了剩余价值的流动性需求&#xff0c;就有了拍卖这种方式。说到捡漏收藏界尽知的当属“明成化斗彩鸡缸杯”了。它的传承过程&#xff0c;似乎就是一部“拣大…...

电子网站建设考试/手机广告推广软件

本文主要讲如何自定义NSOperation&#xff0c;以及自定义NSOperation的一些注意事项&#xff0c;以下载图片为例。 新建一个类&#xff0c;继承于NSOperation。 CustomOperation.h 代码 #import <Foundation/Foundation.h> #import <UIKit/UIKit.h>interface Custo…...

网站推广外链/win10系统优化软件哪个好

部署LNMP和部署LAMP方法是一样的&#xff0c;只不过是WEB服务器软件换了而已&#xff0c;这里使用的WEB服务器软件就是一篇文章中所部署的Nginx&#xff0c;所谓的LNMP也就是LinuxNginxMysqlPHP。之所以使用LNMP是因为它是一个高性能的动态网站平台&#xff0c;在某些领域比LAM…...

网页设计教程下载/金昌网站seo

1. 重复的HTTP请求数量应尽量减少 &#xff08;1&#xff09;减少调用其他页面、文件的数量。 &#xff08;2&#xff09;精灵图 2. 文件资源优化**1**压缩Javascript、CSS代码文件压缩 **2**CDN托管 **3** 缓存使用 **4**文件合并 3. 在文件头部放置css样式的定义 这项设置…...