数据结构与算法笔记2 线性表

145 阅读9分钟

1. 定义:线性表 Linear List

线性表是具有相同数据类型\color{red}相同数据类型n(n0) n (n\ge 0) 个数据元素的有限序列,其中 n n 为表长,当 n=0 n = 0 时线性表是一个空表。若用L L命名线性表,则其一般表示为:

L=a1,a2,...,aj,aj+1,...anL=(a_{1},a_{2}, ..., a_{j},a_{j+1},...a_{n})

除第一个元素外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继。

2. 线性表基本操作

  1. 初始化表。 InitList(&L)
  2. 销毁操作。 Destroy(&L)
  3. 按位插入操作。ListInsert(&L,i,e)
  4. 按位删除操作。ListDelete(&L,i,&e)
  5. 按值查找操作。LocateEle(L,e)
  6. 按位查找操作。GetEle(L,i)
  7. 求表长。Len(L)
  8. 输出操作。PrintList(L)
  9. 判空操作。IsEmpty(L)

**对数据的操作(记忆思路):创销,增删改查

3. 顺序表:用顺序存储方式实现的线性表

image.png

3.1 顺序表的实现

3.1.1 顺序表的实现:静态分配,即用数组实现

在初始化顺序表的时候,把初始长度设为0是必须要做的。且如果开始没有初始化数组各元素,内存中会有遗留的脏数据。

静态分配的存储空间是静态的,顺序表的表长刚开始确定后就无所更改。 存满了就满了。存在局限性\color{red} 存在局限性

image.png

3.1.2 顺序表的实现:动态分配,需要定义一个指针指向顺序表中第一个数据元素

image.png

关键:动态申请和释放内存空间:缺点时间开销大。

C语言中使用malloc和free函数
C++中可以使用newdelete函数

L.data = (ElemType *)malloc(sizeof(ElemType)*InitSize);

需要增加动态增加数组长度的方法。

3.2 顺序表的特点

    1. 随机访问,即可以在 O(1) O(1) 时间内找到第i个元素。
    1. 存储密度高,每个节点只存储数据元素。
    1. 扩展容量不方便,(即便采用动态分配的方式实现,扩展长度的时间复杂度也比较高)。
    1. 插入、删除操作不方便,需要移动大量元素。

image.png

3.3 顺序表的基本操作

3.3.1 顺序表的基本操作:插入

image.png

3.3.2 顺序表的基本操作:删除

image.png

3.3.3 顺序表的基本操作:查找

1) 按位查找:获取表L中第i个元素的值

image.png

2)按值查找:获取表L中针对e值的次序

image.png

4. 链表:用链式存储结构实现的线性表

链表可以分为单链表、双链表、循环链表和静态链表。

4.1 单链表

单链表有带头节点和不带头节点的

4.1.1 顺序存储和链式存储比较

顺序存储

优点:可随机存取,存储密度高。
缺点:要求大片连续空间,改变容量不方便。

链式存储

优点:不要求大片连续空间,改变容量方便。
缺点:不可随机存取,要耗费一定空间存放指针。

image.png

4.1.2 用代码表示单链表

image.png

4.1.3 初始化单链表

带头结点写代码更方便,不带头节点头指针直接指向数据节点,带头节点的头指针指向头节点。

1) 不带头节点的单链表初始化,空表判断

image.png

2) 带头结点的单链表初始化,空表判断

image.png

4.1.4 单链表的基本操作:插入

1) 按位序插入带头结点

平均时间复杂度是 O(n) O(n) image.png

2) 按位序插入不带头结点

不带头结点的情况,需要在 i=1i=1 时,专门创建有数据的第一个结点,要特殊考虑。平均时间复杂度也是 O(n) O(n)

image.png

3) 指定结点的后插操作:在p结点后插入元素e

其实就是1) 2)算法中后半部分的算法封装,此处算法时间复杂度为 O(1) O(1) ,在本封装中考虑到了malloc失败的情况。如果内存满了,可能分配内存失败。

image.png

4)指定结点的前插操作:在p结点前插入元素e

两种思路。

    1. 循环查找p的前驱节点q,再对q后插,时间复杂度为 O(n)O(n)
    1. 申请一个新的结点作为p的后插节点,后插节点存取p的原有元素,p存取要插入的新元素。时间复杂度 O(1) O (1)

image.png

4.1.5 单链表的基本操作:删除(带头结点)

1) 按位序删除

最好情况下时间复杂度 O(1) O(1) 最坏和平均情况下时间复杂度 O(n) O(n) image.png

2) 指定结点删除

将p的后继结点q的数据域赋值到p结点,然后在p结点和q结点的后继结点之间建立关系,释放q结点。时间复杂度为 O(1) O(1),如果p是最后一个结点该代码会有bug,需要从头找p的前驱,时间复杂度为 O(n)O(n)image.png

4.1.6 单链表的基本操作:查找(带头结点)

1) 按位查找

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

image.png

2)按值查找

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

image.png

4.1.7 单链表的基本操作:求表的长度

时间复杂度: O(n) O(n)

image.png

4.2 单链表的建立

当有很多个数据元素,要存到一个单链表里如何实现。

  1. 初始化一个单链表;
  2. 每次取一个数据元素,插入到表尾/表头

头插法、尾插法:核心就是初始化操作、指定结点的后插操作。尾插法需要设置一个指向表尾结点的指针。

4.2.1 尾插法

1) 直接使用按位序插入的方法:时间复杂度 O(n2) O(n^{2})

  1. 初始化单链表

  2. 实则之变量length记录链表长度

  3. 调用按位序插入的方法

     while循环
     {
         每次取一个数据元素e;
         ListInsert(L,length+1,e)插到尾部;
         length++;
     }
    

2) 对尾指针进行后插操作的方法:时间复杂度 O(n) O(n)

设置一个尾指针,这个指针指向表尾的最后一个数据节点,对该节点进行后插操作。

image.png

4.2.2 头插法

初始化单链表,用while循环每取一个数据元素对其进行后插操作。 头插法一个重要的应用可以实现链表的逆置。

image.png

4.3 双链表

双链表就是在单链表的基础上,再增加一个指针,指向其前驱结点,解决了逆向检索的问题。且存储密度更低。

4.3.1 双链表结点类型定义

typedef struct DNode //定义双链表结点类型
{
	int data;  //数据域
	struct DNode* prior, * next; //前驱和后继指针
} DNode, * DLinkList;

4.3.2 双链表头结点的初始化及判断是否为空

image.png

4.3.3 双链表的插入

此处是后插操作,由于双链表特性,我们可以较方便的找到给定节点的前驱节点,再对前驱节点进行后插操作。 image.png

4.3.4 双链表的删除和销毁

image.png

4.3.5 双链表的遍历

双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度 O(n) O(n)

1) 后向遍历

while(p != NULL)
    //对结点p做相应处理,如打印
    p = p->next;

2) 前向遍历

while(p != NULL)
{
    //对结点p做相应处理
    p = p->prior;
}

3) 前向遍历(跳过头结点)

while (p->prior != NULL)
{
    //对结点p做相应处理
    p = p->prior;
}

4.4 循环链表

循环单链表和循环双链表都是在相应的单链表和双链表的基础上稍作修改。

4.4.1 循环单链表

循环单链表是单链表尾节点的next指向头结点而成。

特性

  1. 在初始化循环单链表时,头结点的next指向头结点。

  2. 判断循环单链表为空的标志

     L->next == L;
    
  3. 判断结点p为循环单链表尾结点的标志

     p->next == L;
    

image.png

优势

单链表从一个结点出发只能找到后续的各个结点。

  1. 循环单链表从一个结点出发可以找到其他任何一个结点。
  2. 循环单链表可以通过让指针指向尾结点来快速实现尾部和头部插入,时间复杂度为 O(1) O(1)

image.png

4.4.2 循环双链表

循环双链表即双链表表头结点的prior指向表尾结点;表尾结点的next指向头结点。

特性

  1. 在初始化循环双链表时,头结点的next和prior都指向头结点自身。

  2. 判断循环双链表为空的标志

     L->next == L;
    
  3. 判断结点p为循环双链表尾结点的标志

     p->next == L;
    

image.png

区别

可以简化插入和删除的代码中的对是否是最后一个结点的条件判断。

4.4.3 链表代码中需要关注的几个问题

  1. 如何判空
  2. 如何判断结点p是否是表尾、表头结点,这是后向/前向遍历的实现核心
  3. 如何在表头、表中、表尾插入/删除一个结点(插入、删除操作的不易错思路)

4.5 静态链表:用数组方式实现的链表

单链表:各个结点在内存中散乱分布。 静态链表:分配一整片连续的内存空间,各个结点集中安置。

image.png

优点

增、删操作不需要大量移动元素

缺点

不能随机存取,只能从头结点开始依次往后查找;容量固定不可变。

适用场景

  1. 不支持指针的低级语言;
  2. 数据元素数量固定不变的场景(如操作系统的文件分配表FAT)。

4.5.1 定义静态链表

image.png

4.5.2 静态链表的基本操作实现

1)查找

如果要在静态链表中找到某一个位序的节点,需要从头节点出发挨个往后遍历结点。时间复杂度为 O(n) O(n)

2) 插入位序为 i 的结点

  1. 从头找到一个空的结点,存入数据元素(可以通过初始化的时候把空结点的next设为一个特殊值来表示结点空闲,如-2);
  2. 从头结点出发找到位序为i-1的结点;
  3. 修改新结点的next;
  4. 修改i-1号结点的next。

3)删除某个结点

  1. 从头结点出发找到前驱结点;
  2. 修改前驱结点的游标;
  3. 被删除结点next设为-2。

5. 顺序表与链表对比

5.1 逻辑结构

都属于线性表,都是线性结构。

5.2 存储结构

顺序表

顺序存储

优点:支持随机存取、存储密度高

缺点:大片连续空间分配不方便,改变容量不方便

链表

链式存储

优点:离散的小空间分配方便,改变容量方便

缺点:不可随机存取,存储密度低

5.3 基本操作

5.3.1 基本操作:创

image.png

5.3.2 基本操作:销

image.png

5.3.3 基本操作:增、删

image.png

5.3.4 基本操作:查

image.png

表长难以预估、经常要增加/删除元素 用链表

表长可预估、查询(搜索)操作较多 用顺序表

6. Tips:开放式问题 用框架式思路答题

image.png