线性表详细讲解

news2024/11/13 4:36:40

    • 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 {
    // private
    Object[] 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 + 1
        if (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<=length
        if (index < 1 || index > length) return false;
        // 删除位置的元素,提前赋值给res
        Object 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++;
            // 移动p
            p = 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;
    }

    @Override
    public 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}

    @Override
    public 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;
    }

    @Override
    public 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的后面

单链表代码实现

      // 构建链表La
        LinkedList La = new LinkedList();
        La.insertELe(1,7);
        La.insertELe(2,5);
        La.insertELe(3,3);
        La.insertELe(4,11);

        // 构建链表Lb
        LinkedList Lb = new LinkedList();
        Lb.insertELe(1,2);
        Lb.insertELe(2,6);
        Lb.insertELe(3,3);

        // 将Lb合并到La
        for (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)));
            }
        }

        // 遍历La
        La.print();

2.7.2 有序表的合并

image-20230730153405071

(1)顺序表实现

算法步骤

  • 创建一个空表Lc
  • 依次从 La、Lb中摘取元素比较小的结点,插入到Lc的尾部,直到一个表为空
  • 将不为空的表中的元素全部插入到 Lc 的尾部

        // La
        StructTypeImplementation La = new StructTypeImplementation(3);
        La.insertElem(1,1);
        La.insertElem(7,2);
        La.insertElem(8,3);

        // Lb
        StructTypeImplementation 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);


        // 创建一个空表Lc
        StructTypeImplementation Lc = new StructTypeImplementation(La.getLength() +Lb.getLength());
        // 使用俩个指针,分别指向La、Lb
        int 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尾部的结点插入到Lc
                Lc.insertTail(Lb.getElem(j));
                j++;
            }
        }else {
            while(j <= Lb.getLength()) {
                // 说明是Lb空了,直接将La尾部的结点插入到Lc
                Lc.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

代码实现

      // 构建链表La
        LinkedList La = new LinkedList();
        La.insertELe(1,1);
        La.insertELe(2,7);
        La.insertELe(3,8);

        // 构建链表Lb
        LinkedList 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后边,否则挂pb
        pc.next = pa != null ? pa :pb;

        // 打印
        La.print();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/812054.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ARM裸机-3

1、嵌入式和单片机的区别 1.1、芯片平台 主流的单片机平台&#xff1a;51、PIC、STM32、AVR、MSP430等 主流的嵌入式平台&#xff1a;ARM、PPC、MIPS 1.2、资源、价格、应用领域 单片机片上资源有限、价格低、应用领域多为小家电、终端设备等。 嵌入式系统片上资源丰富、价格…

数据库连接及使用Statement对象完成CRUD

一、数据库连接&#xff1a; 二、使用Statement对象完成CRUD&#xff1a; 1、插入&#xff1a; 2、删除 3、修改 4、查询 三、ORM对象关系映射

数据结构:顺序表详解

数据结构&#xff1a;顺序表详解 一、 线性表二、 顺序表概念及结构1. 静态顺序表&#xff1a;使用定长数组存储元素。2. 动态顺序表&#xff1a;使用动态开辟的数组存储。三、接口实现1. 创建2. 初始化3. 扩容4. 打印5. 销毁6. 尾插7. 尾删8. 头插9. 头删10. 插入任意位置数据…

pytorch 中 view 和reshape的区别

在 PyTorch&#xff08;一个流行的深度学习框架&#xff09;中&#xff0c; reshape 和 view 都是用于改变张量&#xff08;tensor&#xff09;形状的方法&#xff0c;但它们在实现方式和使用上有一些区别。下面是它们之间的主要区别&#xff1a; 实现方式&#xff1a; reshap…

13年测试经验,性能测试-压力测试指标分析总结,看这篇就够了...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 一般推荐&#xf…

Jmeter环境变量配置及测试

上图是Windows版本的测试结果。 Windows系统&#xff1a; win11&#xff1a;“此电脑”——鼠标右键“属性”——“高级系统设置”——“环境变量” 1.1 新建“系统变量”&#xff1a;JMETER_HOME JMETER_HOME变量值为解压后的jmeter路径&#xff0c;如&#xff1a; D:\apach…

AD21原理图的高级应用(三)原理图多通道的应用

&#xff08;三&#xff09;原理图多通道的应用 在很多大型的设计过程中&#xff0c;我们可能会遇到需要重复使用某个图纸&#xff0c;如果使用常规的复制粘贴&#xff0c;虽然可以达到设计要求,但原理图的数量将会变得庞大而烦琐。Altium Designer 支持多通道设计。 多通道设…

数字图像处理(番外)图像增强

图像增强 图像增强的方法是通过一定手段对原图像附加一些信息或变换数据&#xff0c;有选择地突出图像中感兴趣的特征或者抑制(掩盖)图像中某些不需要的特征&#xff0c;使图像与视觉响应特性相匹配。 图像对比度 图像对比度计算方式如下&#xff1a; C ∑ δ δ ( i , j …

数学学习——最优化问题引入、凸集、凸函数、凸优化、梯度、Jacobi矩阵、Hessian矩阵

文章目录 最优化问题引入凸集凸函数凸优化梯度Jacobi矩阵Hessian矩阵 最优化问题引入 例如&#xff1a;有一根绳子&#xff0c;长度一定的情况下&#xff0c;需要如何围成一个面积最大的图像&#xff1f;这就是一个最优化的问题。就是我们高中数学中最常见的最值问题。 最优化…

【C++进阶:哈希--unordered系列的容器及封装】

本课涉及到的所有代码都见以下链接&#xff0c;欢迎参考指正&#xff01; practice: 课程代码练习 - Gitee.comhttps://gitee.com/ace-zhe/practice/tree/master/Hash unordered系列关联式容器 在C98中&#xff0c;STL提供了底层为红黑树结构的一系列关联式容器&#xff0c;在…

React井字棋游戏官方示例

在本篇技术博客中&#xff0c;我们将介绍一个React官方示例&#xff1a;井字棋游戏。我们将逐步讲解代码实现&#xff0c;包括游戏的组件结构、状态管理、胜者判定以及历史记录功能。让我们一起开始吧&#xff01; 项目概览 在这个井字棋游戏中&#xff0c;我们有以下组件&am…

交叉编译工具链的安装、配置、使用

一、交叉编译的概念 交叉编译是在一个平台上生成另一个平台上的可执行代码。 编译&#xff1a;一个平台上生成在该平台上的可执行文件。 例如&#xff1a;我们的Windows上面编写的C51代码&#xff0c;并编译成可执行的代码&#xff0c;如xx.hex.在C51上面运行。 我们在Ubunt…

jellyfin搭建服务器后,快解析端口映射让外网访问

Jellyfin是一款相对知名的影音服务器&#xff0c;是一套多媒体应用程序软件套装&#xff0c;可以有效的组织管理和共享数字媒体文件&#xff0c;不少伙伴喜欢用jellyin在本地自己主机上搭建自己的服务器。当本地搭建服务器后&#xff0c;面对动态IP和无公网IP环境困境下&#x…

【javaSE】面向对象程序三大特性之封装

目录 封装的概念 访问限定符 说明 访问private所修饰的变量的方法 封装扩展之包 包的概念 导入包中的类 注意事项 自定义包 基本规则 操作步骤 步骤一 ​编辑步骤二 ​编辑 步骤三 步骤四 步骤五 包的访问权限控制举例 常见的包 static成员 再谈学生类 s…

Vue中导入并读取Excel数据

在工作中遇到需要前端上传excel文件获取到相应数据处理之后传给后端并且展示上传文件的数据. 一、引入依赖 npm install -S file-saver xlsxnpm install -D script-loadernpm install xlsx二、在main.js中引入 import XLSX from xlsx三、创建vue文件 <div><el-uplo…

Aduino中eps环境搭建

这里只记录Arduino2.0以后版本&#xff1a;如果有外网环境&#xff0c;那么可以轻松搜到ESP32开发板环境并安装&#xff0c;如果没有&#xff0c;那就见下面操作&#xff1a; 进入首选项&#xff0c;将esp8266的国内镜像地址填入&#xff0c;然后保存&#xff0c;在开发板中查…

[STL]stack和queue使用介绍

[STL]stack和queue使用介绍 文章目录 [STL]stack和queue使用介绍stack使用介绍stack介绍构造函数empty函数push函数top函数size函数pop函数 queue使用介绍queue介绍构造函数empty函数push函数front函数back函数size函数pop函数 deque介绍 stack使用介绍 stack介绍 stack是一种…

C++中的static修饰类的成员变量和成员函数

回顾一下C语言中static的描述&#xff0c;我们知道&#xff1a; 当static修饰局部变量时&#xff0c;使局部变量的生命周期延长.static修饰全局变量时&#xff0c;将外部链接属性变成了内部链接属性&#xff0c;使全局变量的作用域只能在该源文件中执行.static修饰函数时&#…

时序预测 | Python实现NARX-DNN空气质量预测

时序预测 | Python实现NARX-DNN空气质量预测 目录 时序预测 | Python实现NARX-DNN空气质量预测效果一览基本介绍研究内容程序设计参考资料效果一览 基本介绍 时序预测 | Python实现NARX-DNN空气质量预测 研究内容 Python实现NARX-DNN空气质量预测,使用深度神经网络对比利时空气…

西安市未央区地方财政支出绩效管理研究_kaic

摘 要 目前传统的地方财政绩效管理研究普遍上主要集中在有关收入研究方面上&#xff0c;而对其支出的规模以及各类结构的研究较少。我国大部分地方财政政府的财政收入低下&#xff0c;财政支出效率有限&#xff0c;不能很好的为其地方经济提供较为稳定的社会支撑和经济保障。造…