考研数据结构--第二章:线性表

系列索引👉2023考研王道数据结构知识梳理

文章目录

第二章:线性表

1. 线性表

1.1. 线性表定义

线性表是具有相同数据类型 n ( n ≥ 0 ) n(n\ge0) n(n0)个元素的有限序列,其中 n n n为表长,当 n = 0 n=0 n=0时,该表为空表。

  1. a i a_i ai是线性表中的“第 i i i个”元素线性表中的位序
  2. a 1 a_1 a1表头元素; a n a_n an表尾元素。
  3. 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。

在这里插入图片描述

1.2. 线性表的特点

  1. 表中元素个数有限
  2. 表中元素具有逻辑上的顺序性,在序列中各个元素排序有其先后次序
  3. 表中元素都是数据元素,每个元素都是单个元素。
  4. 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间。
  5. 表中元素具有抽象性,即讨论元素间一对一的逻辑关系,而不考虑元素究竟表示的内容。
  6. 线性表是一种逻辑结构,表示元素之间一对一相邻的关系。

1.3. 线性表的基本操作

  1. InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
  2. DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
  3. ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
  4. ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
  5. LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
  6. GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
    其他常用操作:
  7. Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
    PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
    Empty(L):判空操作。若L为空表,则返回true,否则返回false。

命名要有可读性,最好以上述为标准,否则初始的是否要写上注释


2. 顺序表

2.1. 顺序表的定义

顺序存储的方式实现线性表的顺序储存。把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

2.2. 顺序表的实现

2.2.1. 顺序表的静态分配

有不明白结构体定义方式的可以看看这篇C 结构体,C与C++的结构题的区别请点击C和Cpp在结构体上的区别(不包括面向对象的区别)

#define MaxSize 10 // 定义最大的长度
typedef struct {
    ElemType data[MaxSize]; // ElemType是data数组的类型,用静态的数组来存储数据。
    int length; // 顺序表的当前的长度
}SqList; // 顺序表的类型定义(静态分配方式)

完整的程序

// C语言程序
#include <stdio.h>

/* C语言中,const不是一个真真正正的常量,其代表的含义仅仅是只读。
使用const声明的对象是一个运行时对象,
无法使用其作为某个量的初值、数组的长度、case的值或在类型的情形中使用,
而在C++中可以使用。
*/
#define MaxSize 10 // 定义最大的长度

typedef struct { // 定义一个结构体
    int data[MaxSize];
    int length;
}SqList; 

// 初始化一个顺序表,有些编译器会自动初始化为0,保险起见,我们手动初始化。
void InitList(SqList *L) { // C语言中&表示的不是引用,而是取地址符。C语言引用是“*”
    for (int i = 0; i < MaxSize; i ++)
        L->data[i] = 0; // 构体指针变量一般用”->”,非结构体指针变量,也就是一般结构体变量,一般用”.”
    L->length = 0;
}

int main() {
    SqList L; // 声明一个顺序表
    InitList(&L); // 初始化顺序表
    for (int i = 0; i < MaxSize; i ++) // 打印整个data数组
        printf("data[%d]=%d\n", i, L.data[i]);
    return 0;
}
// C++程序
#include <iostream>

using namespace std;
const int MaxSize = 10; // 定义最大的长度

struct SqList{ // 定义一个结构体
    int data[MaxSize];
    int length;
}L; // 声明一个顺序表

void InitList(SqList &L) { // 初始化一个顺序表
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = 0;
    L.length = 0;
}

int main() {
    InitList(L); // 初始化顺序表
    for (int i = 0; i < MaxSize; i ++) // 打印整个data数组
        printf("data[%d]=%d\n", i, L.data[i]);
    return 0;
}
2.2.1.1. 局限性
  1. 顺序便的表长开始确定后就无法更改,因为存储空间是静态的

2.2.2. 顺序表的动态分配

#define InitSize 10 // 顺序表的初始长度
typedef struct {
	ElemType *data; // 动态分配数组的指针
	int MaxSize; // 顺序表的最大容量
	int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义(动态分配方式)
2.2.2.1. 动态申请和释放内存空间

C语言

mallocfree函数

两个函数的用法请点击malloc和free函数使用注意事项,C语言malloc和free使用详解

// 开辟空间
// malloc函数返回一个指针,需要强行转换为定义的ElemType数据类型的指针
L.data = (ElemType*) malloc(sizeof(ElemType) * InitSize);
// sizeof(ElemType) * InitSize是指要分配InitSize大小的一整片连续内存空间


// 释放空间
free(L.data);

C++

newdelete关键字

两个关键字的用法请点击c++中new和delete的使用方法

// 开辟空间
/*
格式1:指针变量名=new 类型标识符;
格式2:指针变量名=new 类型标识符(初始值);
格式3:指针变量名=new 类型标识符[内存单元个数];
*/
L.data = new ElemType[InitSize];

// 释放空间
delete [] L.data; // 不加[]释放的是第一个元素的指针

完整的程序

#include <stdio.h>
#include <stdlib.h> // free, malloc库函数的头文件

#define InitSize 10 // 默认的最大的长度

typedef struct {
	int *data; // 动态分配数组的指针
	int MaxSize; // 顺序表的最大容量
	int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义(动态分配方式)

void InitList(SqList *L) {
    L->data = (int *)malloc(sizeof(int) * InitSize);  // 用malloc函数申请一片连续的存储空间
    L->MaxSize = InitSize; // 将顺序表的最大容量初始化为默认的最大的长度
    L->length = 0; // 将顺序表的当前长度默认为0
}

void IncreaseSize(SqList *L, int len) { // 动态增加数组的长度
    int *p = L->data; // 用指针p指向L.data数组
    L->data = (int *)malloc(sizeof(int) * (L->MaxSize + len)); // 开辟一片新的连续的存储空间
    for (int i = 0; i < L->length; i ++) // 将数据复制到新的区域,这样时间开销很大
        L->data[i] = p[i];
    L->MaxSize += len; // 更新顺序表可以容纳的最大的长度
    free(p); // 释放旧的存储空间
}

// 还可以用realloc函数
// 建议初学者使用 malloc 和 free 更能理解背后过程
void IncreaseSize(SqList *L, int len) { 
    /*
    会先判断当前的指针所指向的内存是否有足够的连续空间,
    如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;
    如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,
    而后释放原来的内存区域
    malloc与realloc均不会初始化
    */
    L->data = (int *)realloc(L->data, sizeof(int) * 5); // 不写L->data参数和malloc一样
    L->MaxSize += len;
}

int main() {
    SqList L; // 声明一个顺序表
    InitList(&L); // 初始化顺序表
    for (int i = 0; i < L.MaxSize; i ++)  // 赋值,注意要L.length要++
        L.data[i] = i, L.length ++;
    IncreaseSize(&L, 5); // 将顺序表扩容5个单位
    for (int i = 0; i < L.MaxSize; i ++) // 打印
        printf("L[%d]=%d\n", i, L.data[i]);
    return 0;
}

mallocrealloc的区别请点击malloc与realloc的区别

// C++程序
#include <iostream>

using namespace std;
const int InitSize = 10; // 默认的最大的长度

struct SqList {
    int *data;
    int MaxSize;
    int length;
}L; // 声明一个叫L的结构体

void InitList(SqList &L) {
    L.data = new int[InitSize]; // 开辟一个大小为InitSize的连续的存储空间
    L.MaxSize = InitSize;
    L.length = 0;
}

void IncreaseSize(SqList &L, int len) {
    int *p = L.data;
    // 开辟一个新的大小为InitSize + len的连续的存储空间
    L.data = new int[L.MaxSize + len];
    for (int i = 0; i < L.length; i ++)
        L.data[i] = p[i];
    L.MaxSize += len;
    delete [] p; // 释放旧的存储空间
}

int main() {
    InitList(L);
    for (int i = 0; i < L.MaxSize; i ++)
        L.data[i] = i, L.length ++;
    IncreaseSize(L, 5);
    for (int i = 0; i < L.MaxSize; i ++)
        printf("L[%d]=%d\n", i, L.data[i]);
    return 0;
}

newmalloc的区别请点击new和malloc有何区别?

2.3. 顺序表的特点

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

2.4. 顺序表的基本操作

2.4.1. 插入

ListInsert(&L, i, e)在表L中的第i个位置上插入指定元素e,这里指的是位序,从1开始

2.4.1.1. 静态分配实现
#define MaxSize 10 // 定义最大长度
typedef struct {
    ElemType data[MaxSize]; // 用静态数组存放数据
    int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义

完整的程序

// C语言程序
#include <stdio.h>
#include <stdbool.h> // 操作布尔类型

#define MaxSize 10

typedef struct {
    int data[MaxSize];
    int length;
}SqList;

// 初始化一个顺序表
void InitList(SqList *L) {
    for (int i = 0; i < MaxSize; i ++)
        L->data[i] = 0;
    L->length = 0;
}

// 在顺序表的第i个位置插入元素e
bool ListInsert(SqList *L, int i, int e) {
    // 判断i是否有效,i的有效范围[1, length + 1]
    if (i < 1 || i > L->length + 1)
        return false;
    if (L->length >= MaxSize)  // 判断是否越界
        return false;
    for (int j = L->length; j >= i; j --)
        L->data[j] = L->data[j - 1]; // 将第i个元素及后的元素往后移动1个单位
    L->data[i - 1] = e; // 在i处放上元素e
    L->length ++;
    return true;
}

int main() {
    SqList L;
    InitList(&L);
    for (int i = 0; i < MaxSize - 1; i ++) // 为前9个赋值数据
        L.data[i] = i, L.length ++;
    if (ListInsert(&L, 10, 3)) // 在第10个位置插入数据7
        puts("Insert success!!!");
    else puts("Insert Fatal!!!");
    for (int i = 0 ; i < MaxSize; i ++) // 打印
        printf("L[%d]=%d\n", i, L.data[i]);
    return 0;
}
// C++程序
#include <iostream>

using namespace std;
const int MaxSize = 10;

struct SqList {
    int data[MaxSize];
    int length;
}L;

// 初始化一个顺序表
void InitList(SqList &L) {
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = 0;
    L.length = 0;
}

// 在顺序表的第i个位置插入元素e
bool ListInsert(SqList &L, int i, int e) {
    // 判断i是否有效,i的有效范围[1, length + 1]
    if (i < 1 || i > L.length + 1)
        return false;
    if (L.length >= MaxSize)
        return false;
    for (int j = L.length; j >= i; j --) // 将第i个元素及后的元素往后移动1个单位
        L.data[j] = L.data[j - 1];
    L.data[i - 1] = e; // 在i处放上元素e
    return true;
}

int main() {
    InitList(L);
    for (int i = 0; i < MaxSize - 1; i ++) // 为前9个赋值数据
        L.data[i] = i, L.length ++;
    if (ListInsert(L, 10, 7)) // 在第10个位置插入数据7
        puts("Insert Success!!!");
    else puts("Insert Fatal!!!");
    for (int i = 0; i < MaxSize; i ++) // 打印
        printf("L[%d]=%d\n", i, L.data[i]);
    return 0;
}
2.4.1.2. 动态分配实现

并实现了数组越界后的自动扩容

// C语言程序
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define InitSize 10

typedef struct {
    int *data; // 动态分配数组的指针
    int MaxSize; // 顺序表的最大容量
    int length; // 顺序表的当前长度
}SqList; // 顺序表的类型定义(动态分配方式)

void InitList(SqList *L) {
    L->data = (int *)malloc(sizeof(int) * InitSize); // 用malloc函数申请一片连续的存储空间
    L->MaxSize = InitSize; // 将顺序表的最大容量初始化为默认的最大的长度
    L->length = 0; // 将顺序表的当前长度默认为0
}

void IncreaseSize(SqList *L, int len) { // 动态增加数组的长度
    int *p = L->data; // 用指针p指向L.data数组
    L->data = (int *)malloc(sizeof(int) * (L->MaxSize + len)); // 开辟一片新的连续的存储空间
    for (int i = 0; i < L->length; i ++) // 将数据复制到新的区域,这样时间开销很大
        L->data[i] = p[i];
    L->MaxSize += len; // 更新顺序表可以容纳的最大的长度
    free(p); // 释放旧的存储空间
}

bool ListInsert(SqList *L, int i, int e) {
    // 判断i是否有效,i的有效范围[1, length + 1]
    if (i < 1 || i > L->length + 1)
        return false;
    if (L->length >= L->MaxSize) // 判断是否越界
        IncreaseSize(L, L->length - L->MaxSize + 1); // 越界后动态开辟空间
    for (int j = L->length; j >= i; j --) // 将第i个元素及后的元素往后移动1个单位
        L->data[j] = L->data[j - 1];
    L->data[i - 1] = e; // 在i处放上元素e
    L->length ++; 
    return true;
}

int main() {
    SqList L;
    InitList(&L);
    for (int i = 0; i < L.MaxSize; i ++) // 赋值数据
        L.data[i] = i, L.length ++;
    if (ListInsert(&L, L.MaxSize + 1, 7)) // 在第MaxSize + 1个位置插入数据7
        puts("Insert Success!!!");
    else puts("Insert Fatal!!!");
    for (int i = 0; i < L.MaxSize; i ++) // 打印
        printf("L[%d]=%d\n", i, L.data[i]);
    return 0;
}
// C++程序
#include <iostream>

using namespace std;
const int InitSize = 10; // 默认的最大的长度

struct SqList {
    int *data;
    int MaxSize;
    int length;
}L; // 声明一个叫L的结构体

void InitList(SqList &L) {
    // 开辟一个大小为InitSize的连续的存储空间
    L.data = new int[InitSize];
    L.MaxSize = InitSize;
    L.length = 0;
}

void IncreaseSize(SqList &L, int len) {
    int *p = L.data;
    // 开辟一个新的大小为InitSize + len的连续的存储空间
    L.data = new int[L.MaxSize + len];
    for (int i = 0; i < L.length; i ++)
        L.data[i] = p[i];
    L.MaxSize += len;
    delete [] p; // 释放旧的存储空间
}

bool ListInsert(SqList &L, int i, int e) {
    if (i < 1 || i > L.length + 1)
        return false;
    if (L.length >= L.MaxSize)
        IncreaseSize(L, L.length - L.MaxSize + 1);
    for (int j = L.length; j >= i; j --)
        L.data[j] = L.data[j - 1];
    L.data[i - 1] = e;
    L.length ++;
    return true;
}

int main() {
    InitList(L);
    for (int i = 0; i < L.MaxSize; i ++) // 赋值数据
        L.data[i] = i, L.length ++;
    if (ListInsert(L, L.MaxSize + 1, 7)) // 在第MaxSize + 1个位置插入数据7
        puts("Insert Success!!!");
    else puts("Insert Fatal!!!");
    for (int i = 0; i < L.MaxSize; i ++) // 打印
        printf("L[%d]=%d\n", i, L.data[i]);
    return 0;
}
2.4.1.3. 小结
  1. 要注意的一点就是在所有线性表的插入操作中,所有的插入都是前插,也就是第 i 个位置的前一个位置进行插入,这是一个小的规定。
2.4.1.3.1. 时间复杂度
  1. 最好情况:新元素插入到表尾,不需要移动元素 i = n + 1 i=n+1 i=n+1,循环0次;最好时间复杂度 O ( 1 ) O(1) O(1)
  2. 最坏情况:新元素插入到表头,需要将原有的 n n n个元素全都向后移动 i = 1 i=1 i=1,循环 n n n次,最坏时间复杂度 O ( n ) O(n) O(n)
  3. 平均情况:假设新元素插入到任何一个位置的概率都相同,概率都是 p = 1 n + 1 p=\frac{1}{n+1} p=n+11 i = 1 i=1 i=1,循环 n n n次, i = 2 i=2 i=2,循环 n − 1 n-1 n1次… i = n + 1 i=n+1 i=n+1时,循环0次。平均循环次数= n p + ( n − 1 ) p + ( n − 2 ) p + ⋯ 1 ⋅ p = n ( n + 1 ) 2 1 ( n + 1 ) = n 2 np+\left( n-1 \right) p+\left( n-2 \right) p+\cdots 1·p=\frac{n\left( n+1 \right)}{2}\frac{1}{\left( n+1 \right)}=\frac{n}{2} np+(n1)p+(n2)p+1p=2n(n+1)(n+1)1=2n平均时间复杂度 O ( n ) O(n) O(n)

2.4.2. 删除

ListDelete(&L, i, &e)删除表 L L L中的第 i i i个位置的元素,并用 e e e返回删除元素的值

完整程序

// C语言程序
#include <stdio.h>
#include <stdbool.h>

#define MaxSize 10

typedef struct {
    int data[MaxSize];
    int length;
}SqList;

void InitList(SqList *L) {
    for (int i = 0; i < MaxSize; i ++)
        L->data[i] = 0;
    L->length = 0;
}

bool ListDelete(SqList *L, int i, int *e) {
    // 判断i是否有效,i的有效范围[1, length]
    if (i < 1 || i > L->length)
        return false;
    *e = L->data[i - 1]; // 将被删除的元素赋值给e
    for (int j = i; j < L->length; j ++)
        L->data[j - 1] = L->data[j];
    L->data[L->length - 1] = 0; // 将length位置的元素赋值为0;
    L->length --;
    return true;
}

int main() {
    SqList L;
    InitList(&L);
    int e = -1;
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = i, L.length ++;
    if (ListDelete(&L, 5, &e))
        printf("Delete %d Success!!!\n", e);
    else puts("Delete Fatal!!!");
    for (int i = 0; i < MaxSize; i ++)
        printf("L[%d]=%d\n", i, L.data[i]);
    return 0;
}
// C++程序
#include <iostream>

using namespace std;
const int MaxSize = 10;

struct SqList {
    int data[MaxSize];
    int length;
}L;

void InitList(SqList &L) {
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = 0;
    L.length = 0;
}

bool ListDelete(SqList &L, int i, int &e) {
    // 判断i是否有效,i的有效范围[1, length]
    if (i < 1 || i > L.length)
        return false;
    e = L.data[i - 1]; // 将被删除的元素赋值给e
    for (int j = i; j < L.length; j ++)
        L.data[j - 1] = L.data[j];
    L.data[L.length - 1] = 0; // 将length位置的元素赋值为0;
    L.length --;
    return true;
}

int main() {
    InitList(L);
    int e;
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = i, L.length ++;
    if (ListDelete(L, 5, e))
        printf("Delete %d Success!!!\n", e);
    else puts("Delete Fatal!!!");
    for (int i = 0; i < MaxSize; i ++)
        printf("L[%d]=%d\n", i, L.data[i]);
    return 0;
}
2.4.2.1. 小结
  1. 要注意的一点就是在所有线性表的删除操作中,所有的插入都是前删,也就是第 i 个位置的前一个位置进行删除,这是一个小的规定。
2.4.2.1.1. 时间复杂度
  1. 最好情况:删除表尾元素,不需要移动其他元素 i = n i = n i=n,循环 0 次;最好时间复杂度 O ( 1 ) O(1) O(1)
  2. 最坏情况:删除表头元素,需要将后续的 n − 1 n-1 n1个元素全都向前移动 1 1 1个单位,循环 n − 1 n-1 n1次;最坏时间复杂度 O ( n ) O(n) O(n);
  3. 平均情况:假设删除任何一个元素的概率相同,概率都是 p = 1 n p=\frac{1}{n} p=n1 i = 1 i=1 i=1,循环 n n n次, i = 2 i=2 i=2,循环 n − 1 n-1 n1次… i = n i=n i=n时,循环0次。平均循环次数= ( n − 1 ) p + ( n − 2 ) p + ⋯ 1 ⋅ p = n ( n − 1 ) 2 1 n = n − 1 2 \left( n-1 \right) p+\left( n-2 \right) p+\cdots 1·p=\frac{n\left( n - 1 \right)}{2}\frac{1}{ n}=\frac{n - 1}{2} (n1)p+(n2)p+1p=2n(n1)n1=2n1平均时间复杂度 O ( n ) O(n) O(n)

2.4.3. 查找

2.4.3.1. 按位查找

GetElem(L, i)获取表 L L L中第 i i i个位置的元素的值

#define MaxSize 10
typedef struct {
    ElemType data[MaxSize];
    int length;
}SqList;

ElemType GetElem(SqList &L, int i) {
    return L.data[i - 1];
}

完整的程序

// C语言程序
#include <stdio.h>

#define MaxSize 10

typedef struct {
    int data[MaxSize];
    int length;
}SqList;

void InitList(SqList *L) {
    for (int i = 0; i < MaxSize; i ++)
        L->data[i] = 0;
    L->length = 0;
}

int GetElem(SqList *L, int i) {
    return L->data[i - 1]; // 和普通数组访问一样
}

int main() {
    SqList L;
    InitList(&L);
    for (int i = 0; i < MaxSize;i ++)
        L.data[i] = i, L.length ++;
    printf("%d\n", GetElem(&L, 3));
    return 0;
}
// C++程序
#include <iostream>

const int MaxSize = 10;
struct SqList {
    int data[MaxSize];
    int length;
}L;

void InitList(SqList &L) {
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = 0;
    L.length = 0;
}

int GetElem(SqList &L, int i) {
    return L.data[i - 1]; // 和普通数组访问一样
}

int main() {
    InitList(L);
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = i, L.length ++;
    printf("%d\n", GetElem(L, 5));
    return 0;
}
2.4.3.1.1. 时间复杂度

由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第 i 个元素——“随机存取”特性。即时间复杂度为 O ( 1 ) O(1) O(1)

2.4.3.2. 按值查找

LocateElem(L, e)在表 L L L中查找具有给定关键字值的元素。

#define MaxSize 10
typedef struct {
    ElemType data[MaxSize];
    int length;
}SqList;

int LocateElem(SqList &L, ElemType e) {
    for (int i = 0; i < L.length; i ++)
        if (L.data[i] == e)
            return i + 1;
    return 0;
}

完整的程序

// C语言程序
#include <stdio.h>

#define MaxSize 10

typedef struct {
    int data[MaxSize];
    int length;
}SqList;

void InitList(SqList *L) {
    for (int i = 0; i < MaxSize; i ++)
        L->data[i] = 0;
    L->length = 0;
}

int LocateElem(SqList *L, int e) {
    for (int i = 0; i < L->length; i ++)
        if (L->data[i] == e)
            return i + 1; // 数组下标为i的元素值等于e,返回其位序i+1
    return 0; // 查询失败返回0
}

int main() {
    SqList L;
    InitList(&L);
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = i, L.length ++;
    printf("%d\n", LocateElem(&L, 5));
    return 0;
}
// C++程序
#include <iostream>

using namespace std;
const int MaxSize = 10;

struct SqList {
    int data[MaxSize];
    int length;
}L;

void InitList(SqList &L) {
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = 0;
    L.length = 0;
}

int LocateElem(SqList &L, int e) {
    for (int i = 0; i < L.length; i ++)
        if (L.data[i] == e)
            return i + 1; // 数组下标为i的元素值等于e,返回其位序i+1
    return 0; // 查询失败返回0
}

int main() {
    InitList(L);
    for (int i = 0; i < MaxSize; i ++)
        L.data[i] = i, L.length ++;
    printf("%d\n", LocateElem(L, 5));
    return 0;
}
2.4.3.2.1. 时间复杂度
  1. 最好情况:目标元素在表头循环1次;最好时间复杂度 O ( 1 ) O(1) O(1)
  2. 最坏情况:目标元素在表尾循环 n n n次;最坏时间复杂度 O ( n ) O(n) O(n)
  3. 平均情况:假设目标元素出现在任何一个位置的概率相同,都是 1 n \frac{1}{n} n1目标元素在第1位,循环1次;在第2位,循环2次;…… ;在第 n n n位,循环 n n n次,平均循环次数为: 1 ⋅ 1 n + 2 ⋅ 1 n + 3 ⋅ 1 n + ⋯ n ⋅ 1 n = n ( n + 1 ) 2 1 n = n + 1 2 1·\frac{1}{n}+2·\frac{1}{n}+3·\frac{1}{n}+\cdots n·\frac{1}{n}=\frac{n\left( n+1 \right)}{2}\frac{1}{n}=\frac{n+1}{2} 1n1+2n1+3n1+nn1=2n(n+1)n1=2n+1平均时间复杂度为 O ( n ) O(n) O(n)
2.4.3.3. 小结
  1. 《数据结构》考研初试中,手写代码可以直接用“==”,无论 ElemType 是基本数据类型还是
    结构类型
  2. 手写代码主要考察学生是否能理解算法思想,不会严格要求代码完全可运行
  3. 有的学校考《C语言程序设计》,那么…也许就要语法严格一些

3. 链表

3.1. 链表的基本概念

定义:链式存储︰用一组任意的存储单元存储线性表中的数据元素。用这种方法存储的线性表简称线性链表。每一个结点只包含一个指针域的链表称为单链表

结点的组成

  1. 数据域:存放数据元素

  2. 指针域:存放后继结点的地址

首元结点:链表中存储第一个数据元素 a 1 a_1 a1的结点

头指针:指向链表中第一个结点的指针,通常用用来标识一个单链表

头结点:在链表的首元结点之前附加的一个结点,指针域指向线性表的首元结点,头结点的数据域可以不设任何信息,也可以只放空表标志和表长等信息,但此结点不能计入链表长度值

头指针与头结点的区分:不管是否带有头结点,头指针均指向链表的首元结点,而头结点是带头结点的链表中的首元结点,结点内通常不存储信息

不带头结点VS带头结点

  1. 由于首元结点的位置被存储在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无需特殊处理
  2. 无论链表是否为空,头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就统一了

在这里插入图片描述

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

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

循环链表:首尾相接的链表称为循环链表

3.2. 链表的优缺点

3.2.1. 优点

  1. 数据元素的个数可以自由扩充
  2. 插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高

3.2.2. 缺点

  1. 存储密度小
  2. 存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问

3.3. 单链表

3.3.1. 单链表的实现

typedef struct { // 定义单链表结点类型
    ElemType val; // 每个结点存放一个数据元素,数据域
    struct LNode *next; // 指针指向下一个结点,指针域
}LNode, *LinkList; 
// LNode *强调的是一个结点,LinkList强调的是一个链表

3.3.2. 单链表的基本操作

3.3.2.1. 头插法建立单链表

先入后出,先插入的在后面

3.3.2.1.1. 不带头结点

在这里插入图片描述

核心代码

// C语言程序
// 头插法,逆向建立单链表
ListNode *ListHeadInsert(LinkList head) {
    // 头插法读入与输出相反
    for (int i = 1; i < 10; i ++) { // 生成一个链表
        ListNode *p = (ListNode *)malloc(sizeof(ListNode));
        /*
        头插法与尾插法的不同之处主要在此
        新结点p的指针域指向头指针指向的区域
        */
        p->next = head;
        p->val = i;
        // 令头指针指向新结点
        head = p;
    }
    return head;
}
// C++代码
// 头插法,逆向建立单链表
ListNode *ListHeadInsert(ListNode *head) {
    // 头插法读入与输出相反
    for (int i = 1; i < 10; i ++) { // 生成一个链表
        ListNode *p = new ListNode(i);
        /*
        头插法与尾插法的不同之处主要在此
        新结点p的指针域指向头指针指向的区域
        */
        p->next = head; 
        // 另头指针指向新结点
        head = p;
    }
    return head;
}
3.3.2.1.2. 带头结点

在这里插入图片描述

核心代码

// C语言代码
// 头插法,逆向建立单链表
ListNode *ListHeadInsert(LinkList head) {
    // 头插法读入与输出相反
    for (int i = 1; i < 10; i ++) { // 生成一个链表
        ListNode *p = (ListNode *)malloc(sizeof(ListNode));
        /*
        头插法与尾插法的不同之处主要在此
        新结点p的指针域指向头指针指向的区域
        */
        p->next = head->next;
        p->val = i;
        // 令头指针指向新结点
        head->next = p;
    }
    return head;
}
// C++代码
// 头插法,逆向建立单链表
ListNode *ListHeadInsert(ListNode *head) {
    // 头插法读入与输出相反
    for (int i = 1; i < 10; i ++) { // 生成一个链表
        ListNode *p = new ListNode(i);
        /*
        头插法与尾插法的不同之处主要在此
        新结点p的指针域指向头指针指向的区域
        */
        p->next = head->next;
        // 另头指针指向新结点
        head->next = p;
    }
    return head;
}
3.3.2.2. 尾插法建立单链表
3.3.2.2.1. 不带头结点

核心代码

// C语言程序
// 尾插法,正向建立单链表
ListNode *ListTailInsert(LinkList head) {
    ListNode *r = head;
    for (int i = 1; i < 10; i ++) { // 生成一个链表
        ListNode *p = (ListNode *)malloc(sizeof(ListNode));
        /*
        头插法与尾插法的不同之处主要在此
        */
        p->val = i;
        if (!head) { // 特判下head初始化后指向NULL
            head = p;
            r = p;
            continue;
        }
        // 令尾指针的next指针指向新结点
        r->next = p;
        r = p; // 尾指针指向新结点
    }
    r->next = NULL; // 将尾指针置空
    return head;
}
// C++代码
// 尾插法,正向建立单链表
ListNode *ListHeadInsert(ListNode *head) {
    ListNode *r = head;
    for (int i = 1; i < 10; i ++) { // 生成一个链表
        ListNode *p = new ListNode(i);
        /*
        头插法与尾插法的不同之处主要在此
        */
        if (!head) {
            head = p;
            r = p;
            continue;
        }
        r->next = p;
        r = p;
    }
    r->next = NULL;
    return head;
}
3.3.2.2.2. 带头结点

核心代码

// C语言代码
// 尾插法,正向建立单链表
ListNode *ListHeadInsert(LinkList head) {
    LinkList r = head;
    for (int i = 1; i < 10; i ++) { // 生成一个链表
        ListNode *p = (ListNode *)malloc(sizeof(ListNode));
        /*
        头插法与尾插法的不同之处主要在此
        */
        p->val = i;
        r->next = p;
        r = p;
    }
    r->next = NULL;
    return head;
}
// C++代码
// 尾插法,正向建立单链表
ListNode *ListTailInsert(ListNode *head) {
    ListNode *r = head;
    for (int i = 1; i < 10; i ++) { // 生成一个链表
        ListNode *p = new ListNode(i);
        /*
        头插法与尾插法的不同之处主要在此
        */
        r->next = p;
        r = p;
    }
    r->next = NULL;
    return head;
}
3.3.2.3. 单链表的查找
3.3.2.3.1. 按序号查找结点值

GetElem(L, i)按位查找,获取表 L L L中第 i i i个位置的元素的值

3.3.2.3.1.1. 带头结点

核心代码

// C语言程序
ListNode *GetElem(LinkList head, int i) {
    if (i < 0)
        return NULL;
    ListNode *p; // 指针p指向当前扫描到的结点
    int j = 0; // 当前指针p指向的第几个结点
    p = head; // 头结点是第0个结点不存储数据
    while (p && j < i) // 循环找到第i个结点
        p = p->next, j ++;
    return p;
}
// C++程序
ListNode *GetElem(ListNode *head, int i) {
    if (i < 0)
        return NULL;
    ListNode *p; // 指针p指向当前扫描到的结点
    int j = 0; // 当前指针p指向的第几个结点
    p = head; // 头结点是第0个结点不存储数据
    while (p && j < i) // 循环找到第i个结点
        p = p->next, j ++;
    return p;
}
3.3.2.3.2. 按值查找表结点
3.3.2.3.2.1. 带头结点

核心代码

// C语言程序
ListNode *LocateElem(LinkList head, int e) {
    ListNode *p = head->next;
    while (p && p->data != e)
        p = p->next; 
    return p;
}
// C++程序
ListNode *LocateElem(ListNode *head, int e) {
    ListNode *p = head->next;
    while (p && p->val != e)
        p = p->next;
    return p;
}
3.3.2.4. 按结点插入
3.3.2.4.1. 按指定结点的前插操作

InsertPriorNode(p e):在指定结点 p p p之前插入元素 e e e

核心代码

// C语言程序
bool InsertPriorNode(ListNode *p, int e) {
    if (!p)
        return false;
    ListNode *q = (ListNode *)malloc(sizeof(ListNode));
    if (!q)
        return false;
    q->next = p->next;
    p->next = q;
    q->val = p->val; // 将指定结点p中的val赋值给新结点的val
    p->val = e; // 将新结点的val赋值e
    return true;
}
// C++程序
bool InsertPriorNode(ListNode *p, int e) {
    if (!p)
        return false;
    ListNode *q = new ListNode(e);
    if (!q)
        return false;
    q->next = p->next;
    p->next = q;
    q->val = p->val; // 将指定结点p中的val赋值给新结点的val
    p->val = e; // 将新结点的val赋值e
    return true;
}
3.3.2.4.2. 按指定结点的后插操作

InsertNextNode(p, e):在结点 p p p后面插入指定元素 e e e

核心代码

// C语言程序
bool InsertNextNode(ListNode *p, int e) {
    if (!p)
        return false;
    ListNode *q = (ListNode *)malloc(sizeof(ListNode));
    if (!q)
        return false;
    q->val = e; // 用结点q保存数据e
    q->next = p->next;
    p->next = q;
    return true;
}
// C++程序
bool InsertNextNode(ListNode *p, int e) {
    if (!p)
        return false;
    ListNode *q = new ListNode(e);
    if (!q)
        return false;
    q->next = p->next;
    p->next = q;
    return true;
}
3.3.2.5. 按位序插入

ListInsert(&L, i, e):在表 L L L的第 i i i个位置插入指定元素e

3.3.2.5.1. 不带头结点

不存在 “第0个”结点,因此 i=1 时需要特殊处理

核心代码

// C语言程序
// 按位序插入(不带头结点)
bool ListInsert(LinkList *head, int i, int e) {
    if (i < 1)
        return false;
    if (i == 1) { // 插入第1个结点的操作与其他结点不同,需要特殊处理
        ListNode *p = (ListNode *)malloc(sizeof(ListNode));
        p->val = e;
        p->next = *head;
        *head = p; // 头指针指向新结点
        return true;
    }
    ListNode *p; // 指针指向当前扫描到的结点
    int j = 1; // 当前p指针指向的第几个结点
    p = *head; // p指向第1个结点(首元结点,不是头结点)
    while (p && j < i - 1)
        p = p->next, j ++;
    InsertNextNode(p, e);
}
// C++代码
// 按位序插入(不带头结点)
bool ListInsert(ListNode *&head, int i, int e) {
    if (i < 1)
        return false;
    if (i == 1) {
        ListNode *p = new ListNode(e);
        p->next = head;
        head = p;
        return true;
    }
    ListNode *p; // 指针指向扫描到的结点
    int j = 1; // 当前指针p指向的是第几个结点
    p = head; // p指向第一个结点(首元结点)不是头结点
    while (p && j < i - 1)
        p = p->next, j ++;
    InsertNextNode(p, e);
}
3.3.2.5.2. 带头结点

头结点可以看作是“第0个”结点

核心代码

// C语言代码
// 按位序插入(带头结点)
bool ListInsert(LinkList head, int i, int e) {
    if (i < 1)
        return false;
    ListNode *p = GetElem(head, i - 1);
    InsertNextNode(p, e);
}
// C++代码
// 按位序插入(带头结点)
bool ListInsert(ListNode *head, int i, int e) {
    if (i < 1)
        return false;
    ListNode *p = GetElem(head, i - 1);
    InsertNextNode(p, e);
}
3.3.2.6. 单链表的删除
3.3.2.6.1. 按位序删除

ListDelete(&L, i, &e)删除操作,删除表 L L L中第 i i i个位置的元素,并用 e e e返回删除元素的值

3.3.2.6.1.1. 带头结点

核心代码

// C语言程序
bool ListDelete(LinkList head, int i, int *e) {
    if (i < 1)
        return false;
    ListNode *p = GetElem(head, i - 1); // 找到第i个结点
    if (!p || !p->next)
        return false;
    ListNode *q = p->next; // 令指针q指向被删除的结点
    e = q->val; // 用e返回被删除的值
    p->next = q->next; // 将被删除的结点从链中断开
    free(q) // 释放被删除的结点
    return true;
}
// C++程序
bool ListDelete(ListNode *head, int i, int &e) {
    if (i < 1)
        return false;
    ListNode *p = GetElem(head, i - 1); // 找到第i个结点
    if (!p || !p->next)
        return false;
    ListNode *q = p->next; // 令指针q指向被删除的结点
    p->next = q->next; // 将被删除的结点从链中断开
    delete q; // 释放被删除的结点
    return true;
}
3.3.2.6.2. 按指定结点的删除

DeleteNode(&p)删除指定结点p

方法一:传入链表头指针,循环遍历找到该结点的前一个结点
方法二:偷天换日,交换val,但是当要删除最后一个结点的时候只能使用方法一

核心代码

// C语言程序
bool DeleteNode(ListNode *p) {
    if (!p)
        return false;
    ListNode *q = p->next; // 令q指针指向p的后继结点
    p->val = q->val; // 和后继结点交换数据
    p->next = q->next; // 将结点q从链表中断开
    free(q); // 释放结点q
    return true;
}
// C++程序
bool DeleteNode(ListNode *p) {
    if (!p)
        return false;
    ListNode *q = p->next;
    p->val = q->val;
    p->next = q->next;
    delete q;
    return true;
}
3.3.2.7. 求表长
3.3.2.7.1. 带头结点

核心代码

// C语言程序
int Length(LinkList head) {
    int len = 0;
    ListNode *p = head;
    while (p->next)
        p = p->next, len ++;
    return len;
}
// C++程序
int Length(ListNode *head) {
    int len = 0;
    ListNode *p = head;
    while (p->next)
        p = p->next, len ++;
    return len;
}

3.3.3. 单链表的完整程序

单链表的完整程序👉单链表基本操作的完整程序

3.4. 双链表

只介绍初始化、插入、删除、销毁。按值查找,和按位查找均与单链表相同

typedef struct DListNode {
    ElemType val;
    struct DListNode *prior, *next;
}DListNode, *DLinkList;

3.4.1. 带头结点

3.4.1.1. 初始化

核心代码

// C语言程序
bool DInitList(DLinkList *head) {
    DListNode *p = (DListNode *)malloc(sizeof(DListNode));
    if (!p)
        return false;
    p->prior = NULL; // 头结点的prior永远指向NULL
    p->next = NULL;
    *head = p;
    return true;
}
// C++程序
bool DInitList(DListNode *&head) {
    DListNode *p = new DListNode();
    if (!p)
        return false;
    p->prior = NULL; // 头结点的prior永远指向NULL
    p->next = NULL;
    head = p;
    return true;
}
3.4.1.2. 插入

核心代码

// C语言程序和C++程序相同
// 在p结点之后插入s结点
bool InsertDListNode(DListNode *p, DListNode *s) {
    if (!p || !s)
        return false;
    s->next = p->next;
    if (!p->next)
        p->next->prior = s;
    s->prior = p;
    p->next = s;
    return true;
}
3.4.1.3. 删除

核心代码

// C语言程序和C++程序相同
// 删除p的后继结点
bool DeleteNextDListNode(DListNode *p) {
    if (!p || !p->next)
        return false;
    DListNode *q = p->next;
    p->next = q->next;
    if (!q->next)
        q->next->prior = p;
    free(q);
    // delete q // C++可以写这个
    return true;
}
3.4.1.4. 销毁

核心代码

// C语言程序
// 销毁双链表
void DestoryList(DLinkList *head) {
    // 循环释放每个结点
    while ((*head)->next)
        DeleteNextDListNode(*head);
    free(*head); // 释放头结点
    *head = NULL; // 头指针指向NULL
}
// C++程序
// 销毁双链表
void DestoryList(DListNode *&head) {
    // 循环释放每个结点
    while (head->next)
        DeleteNextDListNode(head);
    delete head; // 释放头结点
    head = NULL; // 头指针指向NULL
}

3.5. 循环链表

3.5.1. 循环单链表

核心代码

// C语言程序
// 初始化一个循环单链表
bool InitList(LinkList *head) {
    ListNode *p = (ListNode *)malloc(sizeof(ListNode));
    if (!p)
        return false;
    head = p;
    head->next = head; // 头结点next的指针指向头结点
    return true;
}
// C++程序
bool InitList(ListNode *&head) {
    ListNode *p = new ListNode();
    if (!p)
        return false;
    head = p;
    head = p;
    head->next = head;
    return false;
}

3.5.2. 循环双链表

初始化

核心代码

// C语言程序
bool InitDLinkList(DLinkList *head) {
    ListNode *p = (ListNode *)malloc(sizeof(ListNode));
    if (!p)
        return false;
    head = p;
    head->prior = head;
    head->next = head;
}
// C++程序
bool InitDLinkList(DListNode *&head) {
    ListNode *p = new ListNode();
    if (!p)
        return false;
    head = p;
    head->prior = head;
    head->next = head;
    return true;
}
插入

核心代码

// C语言程序
bool InsertNextDListNode(DListNode *p, DListNode *s) {
    s->next = p->next;
    p->next->prior = s;
    s->prior = p;
    p->next = s;
}
// C++程序

3.6. 顺序表VS链表

3.6.1. 存储读写方式

顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。例如在第 i i i个位置上执行存或取的操作,顺序表仅需一次访问,而链表则需从表头开始依次访问 i i i次。

3.6.2. 逻辑结构与物理结构

采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的。

3.6.3. 查找、插入和删除操作

对于按值查找,顺序表无序时,两者的时间复杂度均为 O ( n ) O(n) O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为 O ( l o g n ) O(logn) O(logn)

对于按序号查找,顺序表支持随机访问,时间复杂度仅为 O ( 1 ) O(1) O(1),而链表的平均时间复杂度为 O ( n ) O(n) O(n)。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需修改相关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大。

3.6.4. 空间分配

顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置;预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效

如何选取存储结构

  1. 基于存储方面:长度或规模难以估计,不宜选择顺序表,链表不用事先估计存储规模,但是链表的存储密度比较低,显然链式存储结构的存储密度是小于1的

  2. 基于运算的考虑:顺序表是开辟一大片连续空间存储,可以随机存取,按序号访问的时间复杂度为 O ( 1 ) O(1) O(1)。而链表在物理逻辑上不相邻,所以访问元素的时间复杂度为 O ( n ) O(n) O(n),因此,经常做的运算是按序号访问数据元素,显然顺序表优于链表。在顺序表中插入删除操作的时候,平均移动一半的元素,当数据元素量较大的时候,速度会变得很慢,在链表中插入,删除的时候,虽然也要找插入的位置,但是主要的是比较操作,显然链表优于顺序表

  3. 顺序表容易实现,任何高级语言都有数组类型,而链表的操作是基于指针,显然顺序表实现较链表简单

在这里插入图片描述


注:如果您通过阅读本文解决了问题,恳请您留下一个宝贵的赞再离开,如果不嫌弃的话,点个关注再走吧,非常欢迎您能够在讨论区指出此文的不足处,博主会及时对文章加以修正 !如果有任何问题,欢迎评论,非常乐意为您解答!( •̀ ω •́ )✧

  • 34
    点赞
  • 114
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值