耗时5天写成的 C语言数据结构:线性表

293 阅读16分钟

目录

一、🚀线性表\color{#0088ca}{一、🚀线性表}
  1.🚢线性表的定义\color{#0088ca} {1.🚢线性表的定义}
  2.🚢线性表的基本操作\color{#0088ca} {2.🚢 线性表的基本操作}
  3.🚢线性表的存储结构\color{#0088ca} {3.🚢 线性表的存储结构}

二、🚀顺序表\color{#0088ca}{二、🚀 顺序表}
  1.🚔顺序表的定义\color{#0088ca} {1.🚔 顺序表的定义}
  2.🚔顺序表的基本实现——静态分配\color{#0088ca} {2.🚔 顺序表的基本实现——静态分配}
  3.🚔顺序表的基本实现——动态分配\color{#0088ca} {3.🚔 顺序表的基本实现——动态分配}
  4.🚔顺序表的操作\color{#0088ca} {4.🚔 顺序表的操作}
    4.1💨插入操作\color{#0088ca} {4.1💨 插入操作}
    4.2💨删除操作\color{#0088ca} {4.2💨 删除操作}
    4.3💨查找操作\color{#0088ca} {4.3💨 查找操作}
    4.4💨逆置操作\color{#0088ca} {4.4💨 逆置操作}

三、🚀链表\color{#0088ca}{三、🚀 链表}
  1.🚔单链表定义\color{#0088ca} {1.🚔 单链表定义}
  2.🚔单链表的操作\color{#0088ca} {2.🚔 单链表的操作}
    2.1💨按位序插入操作——带头结节\color{#0088ca} {2.1💨 按位序插入操作——带头结节}
    2.2💨按位序插入操作——不带头结点\color{#0088ca} {2.2💨 按位序插入操作——不带头结点}
    2.3💨指定位置插入操作——头插\color{#0088ca} {2.3💨 指定位置插入操作——头插}
    2.4💨指定位置插入操作——尾插\color{#0088ca} {2.4💨 指定位置插入操作——尾插}
    2.5💨查找操作(按值)\color{#0088ca} {2.5💨 查找操作(按值)}
    2.6💨查找操作(按位序)\color{#0088ca} {2.6💨 查找操作(按位序)}
    2.7💨删除操作(按位序)\color{#0088ca} {2.7💨 删除操作(按位序)}

  3.⚽双链表\color{#0088ca} {3.⚽ 双链表}
    3.1💨单链表与双链表的区别\color{#0088ca} {3.1 💨 单链表与双链表的区别}

  4.⚽循环链表\color{#0088ca} {4.⚽ 循环链表}
    4.1💨单链表的循环链表\color{#0088ca} {4.1💨 单链表的循环链表}
    4.2💨双链表的循环链表\color{#0088ca} {4.2💨 双链表的循环链表}

  5.⚽静态链表\color{#0088ca} {5.⚽ 静态链表}
  6.⚽顺序表与链表的相同与不同点\color{#0088ca} {6.⚽ 顺序表与链表的相同与不同点}


一、🚀 线性表

1.🚢 线性表的定义\color{#ff991c}{定义}

线性表是具有相同特性数据元素的一个有限序列,数据元素有 n 个,即表长为 n。n = 零时,为空表。L 为表名。a1a_1  被称为表头元素 , ana_n被称为表尾元素 。 表头元素没有直接前驱,但有直接后继 ;表尾元素没有直接后继 , 但有直接前驱。除了表头与表尾元素,其于的都有且只有一个直接前驱和一个直接后继。

L = ( a1a_1 , a2\color{blue}{a_2} , a3\color{red}{a_3} , a4\color{blue}{a_4} ... ,  an1a_{n-1} , ana_n )

以  a3\color{red}{a_3}为例

直接前驱a2\color{blue}{a_2}  就是它的直接前驱。

直接后继a4\color{blue}{a_4}  就是它的直接后继。

前驱元素: a3\color{red}{a_3}前面的所有元素都是  a3\color{red}{a_3} 的前驱元素,所以  a2\color{blue}{a_2} 不仅是  a3\color{red}{a_3} 的前驱元素,同时也是  a3\color{red}{a_3} 的直接前驱。

后继元素: a3\color{red}{a_3} 后面的所有元素都是   a3\color{red}{a_3} 的后继元素,所以  a4\color{blue}{a_4} 不仅是  a3\color{red}{a_3} 的后继元素,同时也是  a3\color{red}{a_3} 的直接后继。

2.🚢 线性表的基本操作

① 创建一个线性表

②  获取线性表的长度

③  在线性表中插入一个节点

④  在线性表的指定位置插入一个节点

⑤  删除线性表一个节点

⑥  查询线性表一个节点

⑦  获取线性表是否为空

3.🚢 线性表的存储结构

线性表的存储结构分为线性存储结构和链式存储结构,线性存储结构称为顺序表,链式存储结构称为链表。链表又分为单链表、双链表、循环链表、静态链表。

二、🚀 顺序表

1.🚔 顺序表的定义

线性表的顺序存储结构称为顺序表,之所以是顺序存储,是因为其逻辑地址上相邻的元素在物理地址上也相邻。由于其物理地址与逻辑地址相对应,所以每个元素上只用存储元素的值,查找元素可以通过索引*数据占用内存空间大小计算得到。

可以用数组来表示。int numList[] = {1, 2, 3};

由于 int 类型一般情况下占用内存为 4 字节,在内存地址中它们的分配空间是相临的,所以上面的 3 个物理地址(16 进制)每个相差为 4。

#include <stdio.h>
int main(void) {
  int numList[] = {1, 2, 3};
  // sizeof 是一个运算符,它用于计算其操作数的大小(以字节为单位)
  //%zu是size_t类型的正确格式说明符,它用于sizeof运算符的结果
  printf("%zu\n", sizeof(int));  // 4

  //%p格式说明符用于打印指针的值,也就是内存地址
  // printf函数期望对应的参数是void *类型。这是因为void
  // *是一个特殊的指针类型,可以指向任何数据类型的对象,而不会丢失信息或引入类型强制
  printf("%p", (void *)&numList[0]);  // 0x7ff7b71d132c
  printf("%p", (void *)&numList[1]);  // 0x7ff7b71d1330
  printf("%p", (void *)&numList[2]);  // 0x7ff7b71d1334
  return 0;
}

结构体类型会将其中的成员占用内存相加

typedef struct {
    int age;
    char *name;
  } Person;

  printf("Person=%zu\n", sizeof(Person));  // Person=8

2.🚔 顺序表的基本实现——静态分配

静态分配的顺序表只适用于知道数据量的情况下,且数据量不会变的情况下。否则,如果数据量变少,则多余存储空间浪费,数量量变多,则空间不够用,会导致程序崩溃。

#define SIZE 40  // 宏定义 最大长度
typedef struct {
  int data[SIZE];  //  整型数组
  int Length;      // 列表的长度,用于跟踪列表的成员数量
} List;

初始化静态分配的顺序表

 // 初始化静态分配的顺序表
void InitList(List *list)
{

 // 不需要为 data 分配内存,因为它是 List 结构体的一部分
    list->Length = 0;
}

int main(void)
{
    List myList;
    InitList(&myList);

    return 0;
}

3.🚔 顺序表的基本实现——动态分配

动态分配是在程序运行过程中,通过动态存储分配语句分配的。当数据存储空间被占满时,将重新分配一个更大的空间,用于替换原来的空间(注意一下:这里是替换,而不是在原有基础上扩充

初始化动态分配的顺序表

#include <stdlib.h>
#define INITSIZE 10
typedef struct {
  int *data;    // 动态数据
  int Length;   // 顺序表的长度(数据量)
  int MaxSize;  // 顺序表的最大容量
} List;

// 初始化顺序表
void InitList(List *list) {
  // 将分配的内存地址赋值给 list->data
  list->data = (int *)malloc(INITSIZE * sizeof(int));

  // 检查内存分配是否成功
  if (list->data == NULL) {
    exit(EXIT_FAILURE);
  }
  list->Length = 0;          // 初始化顺序表的长度
  list->MaxSize = INITSIZE;  // 安化顺序表的最大容量
}

4.🚔 顺序表的操作

注意:以下操作均以 int 值的顺序表为例,如果为 结构体类型,则需要比对里面的成员

4.1💨 插入操作

在顺序表的第 i 个位置插入元素,要保存 i 为顺序表的有效位置 。将 i 的后继元素整体向后移动一个位置,在空出来的位置插入新元素。随后将 Length 加 1 。操作成功返回 true,否则返回 false。

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

#include <stdbool.h>
#include <stdlib.h>
#define INITSIZE 10
typedef struct {
  int *data;    // 动态数据
  int Length;   // 顺序表的长度(数据量)
  int MaxSize;  // 顺序表的最大容量
} List;



/** 插入操作 */
bool InsertList(List *list, int i, int element) {
  // 如果 i 的值 不在顺序表的范围内,则操作失败
  if (i < 0 || i > list->Length) return false;

  // 如果长度不小于最大容量,则插入一条数据后,必然溢出,操作失败
  if (list->Length >= list->MaxSize) return false;

  // 逆向循环,直到i的位置
  for (int j = list->Length; j >= i; j--) {
    // 依次将数据向后移动一格
    list->data[j] = list->data[j - 1];
  }
  //   插入位置是索引,所以要-1
  list->data[i - 1] = element;
  //   长度+1
  list->Length++;
  return true;
}

4.2💨 删除操作

将下标为 i 索引位置删除,并将其后继元素向前移动一个索引。操作成功返回 true,否则返回 false。成功后需要将被删除的元素赋值给 element 。

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

/** 删除操作 */
bool DeleteList(List *list, int i, int *element) {
  // 如果 i 的值 不在顺序表的范围内,则操作失败
  if (i < 0 || i >= list->Length) return false;

  // 将被删除的元素赋值给 element
  element = list->data[i - 1];

  // 从i的位置开始,将后继元素向前移动一格
  for (int j = i - 1; j < list->Length; j++) {
    list->data[j] = list->data[j + 1];
  }
  //   长度-1
  list->Length--;
  return true;
}

4.3💨 查找操作

由于按位查找,只需直接读就可以,就不上代码了,以下是按值查找。

上顺序表中按给定值查找对应元素,找到返回其位序,否则返回-1。

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

#define INITSIZE 8  // 初始化顺序表容量
typedef struct {
  int *data;    // 动态数据
  int MaxSize;  // 最大容量
  int Length;   // 长度(数据量)
} List;


/** 查找操作 */
int FindList(List *list, int element) {
  for (int i = 0; i < list->Length; i++) {
    // 找到元素,返回其位序
    if (element == list->data[i]) {
      return i + 1;
    }
  }
  // 查找失败,返回-1
  return -1;
}

4.4💨 逆置操作

将顺序表的的元素逆序排列。

分两种情况:顺序表元素有奇数个、顺序表元素有偶数个.。

操作情况:将第一个元素和倒数第一个元素调换位置、将第二个元素和倒数第二个元素调换位置...

依次进行。奇数个元素,中间的不需要调换,偶数个元素,两两调换即可。

奇数个(中间的不需要换)

偶数个(两两调换)

#define INITSIZE 10
typedef struct {
  int *data;    // 动态数据
  int Length;   // 顺序表的长度(数据量)
  int MaxSize;  // 顺序表的最大容量
} List;

/** 逆置操作 */
void ReverseList(List *list) {
  int val;
  // 只需循环一半
  for (int i = 0; i < list->Length / 2; i++) {
    // 保存当前索引的值
    val = list->data[i];

    // lastIdx 为 与 i 对应的索引
    int lastIdx = list->Length - i - 1;
    // 将 lastIdx 位置的值 赋值到 i 的位置上
    list->data[i] = list->data[lastIdx];

    // 将 i 位置的值 赋值到 lastIdx 的位置上
    list->data[lastIdx] = val;
  }
}

三、🚀 链表

1.🚔 单链表定义

线性表的链式存储又称单链表,和顺序表不同的是其逻辑地址相邻的元素在物理地址上并不相邻。由于顺序表总要占用一块连续的内存空间,导致很多零散的内存无法被使用。链表中的元素都是单独存储的,存储在内存中也是分散的。这就需要在每个结点上多存储一个信息,用来存放下一个节点的物理地址。

单链表的查找方式比较费时,它不能通过下标直接定位到对应的元素,而是需要从表头依次对比,直到找到对应的元素。

单链表还分为带头节点和不带头节点

相同点:头指针始终指向链表的第一个节点,头指针为 NULL 时,表示是一个空链表。

不同点:不带头节点的第一个节点为真实节点,其中存储着需要的数据。带头节点的头节点一般只存储指向下一个节点的指针,而不存储数据。

2.🚔 单链表的操作

// 定义单链表节点结构体类型
typedef struct LNode {
  int data;     //数据域
  struct LNode* next;   //指针域(指向下一个节点的指针)
}* LinkList;

2.1💨 按位序插入操作——带头结节

判断插入的位置是否合理。

创建链表指针和指针在链表所在的位置。

位置指针,并判断其位置是否是要插入的位置,如果是则进入下一步,否则移动指针至下一个节点。

创建新节点,为其赋值,并将指针指向原位置的下一个节点(a2)。

将原位置(a1)的指针指向新节点。

// 定义单链表节点结构体类型
typedef struct LNode {
  int data;            // 数据域
  struct LNode* next;  // 指针域(指向下一个节点的指针)
} LNode, *LinkList;

/** 按位序插入 */
bool InsertList(LinkList list, int i, int e) {
  // 插入位置超过最小,返回false
  if (i < 1) {
    return false;
  }
  LNode* p = list;  // 链表的指针,移动指向每个节点
  int j = 0;        // 当前指针所在链表的位序

  // 指向的节点的位序不是 i,则进入循环
  while (p != NULL && j < i - 1) {
    p = p->next;  // 指针移向下一位
    j++;
  }

  // 插入位置超过最大,值为空,则返回false
  if (p == NULL) {
    return false;
  }

  // 创建新节点并分配内存
  LNode* t = (LNode*)malloc(sizeof(LNode));
  t->data = e;        // 为新节点赋值
  t->next = p->next;  // 将新节点的指针指向原位序的下一位
  p->next = t;        // 将新节点插入该位序
  return true;
}

2.2💨 按位序插入操作——不带头结点

1、 判断插入的位置是否合理。

2、判断是否是在第一个节点插入,是则进行 5、6 步骤。

3、创建链表指针和指针在链表所在的位置。

4、位置指针,并判断其位置是否是要插入的位置,如果是则进入下一步,否则移动指针至下一个节点。

5、创建新节点,为其赋值,并将指针指向原位置的下一个节点(a2)。

6、将原位置(a1)的指针指向新节点。

// 定义单链表节点结构体类型
typedef struct LNode {
  int data;            // 数据域
  struct LNode* next;  // 指针域(指向下一个节点的指针)
} LNode, *LinkList;

/** 按位序插入(不带头节点) */
bool InsertListNoHead(LinkList list, int i, int e) {
  // 插入位置超过最小,返回false
  if (i < 1) {
    return false;
  }
  // 在第一个位置插入
  if (i == 1) {
    // 创建新节点并分配内存
    LNode* t = (LNode*)malloc(sizeof(LNode));
    t->data = e;
    t->next = list;
    list = t;  // 头指针指向新节点
    return true;
  }

  LNode* p =
      list;  // 链表的指针,移动指向每个节点,此时p指向的是第一个节点,不是头节点
  int j = 1;  // 当前指针所在链表的位序

  // 指向的节点的位序不是 i,则进入循环
  while (p != NULL && j < i - 1) {
    p = p->next;  // 指针移向下一位
    j++;
  }

  // 插入位置超过最大,值为空,则返回false
  if (p == NULL) {
    return false;
  }

  // 创建新节点并分配内存
  LNode* t = (LNode*)malloc(sizeof(LNode));
  t->data = e;        // 为新节点赋值
  t->next = p->next;  // 将新节点的指针指向原位序的下一位
  p->next = t;        // 将新节点插入该位序
  return true;
}

2.3💨 指定位置插入操作——头插

1、创建一个节点 s ,将 s 的指针指向 链表开始节点的物理地址。

2、将链表的开始节点的指针指向 新节点 s。

3、将开始节点的数据 赋值给 节点 s。

4、将 要插入的元素 赋值给 链表的开始节点。

此时,开始节点存储的是新数据,而原先开始节点的数据已经到第二个位置了。

时间复杂度:O(1)\mathcal {O(1)}


// 定义单链表节点结构体类型
typedef struct LNode {
  int data;            // 数据域
  struct LNode* next;  // 指针域(指向下一个节点的指针)
} LNode, *LinkList;

/** 插入操作(头插法) */
bool  HeadInsertList(LinkList list, int e) {
  if (list == NULL) {
    return false;
  }
  // 创建节点,并分配内存
  LNode* s = (LNode*)malloc(sizeof(LNode));
  if (s == NULL) {
    // 内存分配失败
    return false;
  }

  // 将新数据 s 的指针,指向第一个节点的指针,也就是第二个节点的物理地址
  s->next = list->next;

  // 将第一个节点的指针指向 s ,s为第二个节点,s指针将指向第三个节点
  list->next = s;

  // 将 原第一个节点 list 的数据 保存到 s 节点上
  s->data = list->data;

  // 将 e 赋值给开始结点,已将 e 节点插入完成,
  // list为第一个节点,存储着数据 e,而s节点为第二个节点,存储着原 list 的数据
  list->data = e;

  return true;
}

2.4💨 指定位置插入操作——尾插

1、创建新节点。

2、创建链表指针,并移动到最后一个节点。

3、将新节点赋值为新的数据。

4、将链表指针指向新节点。

5、此时新节点为表尾元素。

// 定义单链表节点结构体类型
typedef struct LNode {
  int data;            // 数据域
  struct LNode* next;  // 指针域(指向下一个节点的指针)
} LNode, *LinkList;

/** 插入操作(尾插法) */
bool TailInsertList(LinkList list, int e) {
  // 创建节点,并分配内存
  LNode* r = (LNode*)malloc(sizeof(LNode));
  if (r == NULL) {
    // 内存分配失败
    return false;
  }
  LNode* p = list->next;

  // 移动指针至最后一个节点
  while (p->next != NULL) p = p->next;

  // 此时p为最后一个节点
  // 为新节点赋值
  r->data = e;
  r->next = NULL;

  // r为最后一个节点,p为倒数第二个
  p->next = r;
  return true;
}

2.5💨 查找操作(按值)

先创建一个指针,向链表后移动指针,对比指向的节点是否为要查找的节点,是则返回,否则继续移动指针。如果没有找到则返回 NULL。

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

// 定义单链表节点结构体类型
typedef struct LNode {
  int data;            // 数据域
  struct LNode* next;  // 指针域(指向下一个节点的指针)
} LNode, *LinkList;


// 查找操作
LNode* FindList(LinkList list, int element) {
  // 创建节点指针
  LNode* p = list->next;

  // 若不是查找的节点,则移动指针到下一个节点
  while (p != NULL && p->data != element) {
    // 如果最后没有找到该节点,则下面的语句依然会再执行一遍
    // 此时p->next 为 NULL,则赋值后,p为NULL
    p = p->next;
  }

  // 找到了,则p为该节点,否则p为NULL
  return p;
}

2.6💨 查找操作(按位序)

// 定义单链表节点结构体类型
typedef struct LNode {
  int data;            // 数据域
  struct LNode* next;  // 指针域(指向下一个节点的指针)
} LNode, *LinkList;

/** 查找操作(按位序) */
LNode* FindIdxList(LinkList list, int i) {
  if (i <= 0) return NULL;          // 查找的位序不合理,返回NULL
  LNode* p = list->next;            // 创建指针
  int j = 1;                        // j 为位序
  while (j < i && p != NULL) {
    p = p->next;                    // 向后移动指针
    j++;                            // 移动 j,使其与 i 相等
  }
  return p;                         // 退出循环,要么找到节点,返回节点。要么没找到 p 为 NULL
}

 2.7💨 删除操作(按位序)

1、通过位序找到节点 p。

2、创建新节点,并将其指向 p 的下一个节点(用于临时保存数据)。

3、将 p 的下一个节点的信息(数据和指针)赋值到新节点。

4、释放新节点内存。

// 定义单链表节点结构体类型
typedef struct LNode {
  int data;            // 数据域
  struct LNode* next;  // 指针域(指向下一个节点的指针)
} LNode, *LinkList;

/** 删除操作 */
bool DeleteList(LinkList list, int i) {
  // 找到该位序的节点
  LNode* p = FindIdxList(list, i);

  // 节点不存在,返回false
  if (p == NULL) return false;

  // 创建新节点,为要删除节点的下一个节点
  LNode* s = p->next;

  // 将要删除节点的下一个节点数据 赋值给p,相当于覆盖了p的数据(删除)
  p->data = s->data;
  p->next = s->next;

  // 释放内存
  free(s);
  return true;
}

3.⚽ 双链表

    3.1 💨 单链表与双链表的区别

指针方面:

单链表:只有指向直接后继的指针。

双链表:有指向直接后继、和直接前驱的指针。

存储空间方面:

单链表:需要的空间更少,需要存储数据和指向直接后继的指针。

双链表:需要的空间更多,需要存储数据和指向直接后继和直接前驱的指针。

适用情况:

单链表:适合元素的增加与删除。

双链表:适合元素的查询。

注意:双链表的插入和删除操作,同时要注意其直接前驱和直接后继的指针修改,要不断链!

4.⚽ 循环链表

    4.1💨 单链表的循环链表         

在其表尾元素的 next 上不为 NULL,而是存储头结点的指针。

    4.2💨 双链表的循环链表    

在其表尾元素的 next 上不为 NULL,而是存储头结点的指针。而在头节点也存储一个指向表尾元素的指针。  其尾部操作的时间复杂度要低于 非循环链表。

5.⚽ 静态链表

静态链表与静态顺序表比较相似,其需要预先分配一段连续的内存空间。在表中,每个元素不仅存储着数据,还存储着其逻辑地址的下一个元素的指针,其指针内容为相对于静态链表的表头偏移量。

6.⚽ 顺序表与链表的相同与不同点

存取方式:

顺序表:通过索引(与表头的偏移量)存取,时间复杂度为  O(1)\mathcal O(1) 。

链表:循环链表到要存取的位置,时间复杂度为  O(n)\mathcal O(n) 。

逻辑地址与物理地址对应的方式:

顺序表:在内存中占用一段连续空间,其在逻辑地址相邻的元素,在物理地址上也相邻。

链表:在内存中占用分散的空间,其在逻辑地址上相邻的元素,在物理地址上并不相邻。由于这个特性,想要知道当前元素的下一个元素是什么,就要在当前元素上多存储一个用于指向下一个元素的指针。

空间分配方式:

顺序表:静态顺序表需要先分配内存,但分配大了则浪费,分配小了则内存会溢出,进而导致程序崩溃。动态顺序表可以解决这个问题,但每当其内存满时,则要在内存中开辟一块更大的空间,并将原表中的数据全部移过去,操作效率不高。

链表:使用内存中的零散空间存储,有效的解决了内存中空间浪费的问题。