跳到主要内容数据结构基础:数组、链表、栈与队列详解 | 极客日志C算法
数据结构基础:数组、链表、栈与队列详解
本文介绍数据结构基础,涵盖逻辑结构与存储结构。详细讲解了动态数组、单链表、顺序栈及循环队列的定义、优缺点及 C 语言代码实现,包括初始化、增删改查等操作。内容修正了原文本中的术语错误与代码格式问题,提供了完整的可运行示例。
数据结构基础
1.1 数据结构基础
算法 + 数据结构 = 程序
宏观定义:数据结构是为了高效访问数据而设计出的一种数据的组织和存储方式。
微观定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
数据结构本身和编程语言无关。

内存:由许多存储单元组成,每个存储单元可以存储一个固定大小的数据块,通常以字节 (Byte) 为单位。每个存储单元都有一个唯一的地址,操作系统根据这一地址去访问内存中的数据。
数据结构中的数据元素就保存在内存中,这些数据元素可能存储在连续的内存单元中,或存储在分散的内存单元中。
1.1.1 数据的逻辑结构
- 逻辑结构有四种基本分类:集合结构、线性结构、树形结构、图形结构(或网状结构)。
-
- 线性结构:线性表(顺序表、链表)、栈、队列、字符串、数组、广义表等。
- 非线性结构:各个元素不保持在一个线性序列中。集合结构、树结构、图结构等。
1.1.2 数据的存储结构
-
顺序存储结构
把逻辑上相邻的数据元素存储在物理位置上也相邻的存储单元中,即所有的元素依次存放在一片连续的存储空间中。
- 优点:数据元素存放的地址是连续的,支持下标访问,随机存取表中的元素。
-
链式存储结构
把逻辑是相邻的数据元素存储在物理上可以不相邻的存储空间中,每个元素拥有一个结点,结点中除了存放数据本身以外,还存放指向下一个结点的指针。
- 优点:克服顺序存储结构中需要预知元素个数的缺点。插入、删除灵活(不用移动结点,只要改变结点中的指针)。
- 缺点:需要额外的空间在表达数据之间的逻辑关系。
-
索引存储结构
即目录,索引存储在存储数据元素之外,同时建立数据元素的目录,方便快速检索。
- 优点:用结点的索引号来确定结点存储地址,检索速度快。
- 缺点:增加了附加的索引表,会占用较多的存储空间。在增加和删除数据时要修改索引表。
- 优点:检索、增加或删除结点的操作快。
- 缺点:不支持排序,一般比线性表存储需要更多的空间,并记录的关键字不能重复。
1.1.3 数据的运算
包括:①分配资源、建立结构、释放资源;②插入和删除;③获取和遍历;④修改和排序。
1.2 数组
C 语言中,数组是具有相同类型的数据元素的集合,是最常见、最简单的一种数据结构。
在 C 语言中,数组的长度一旦确定就不可更改了。
1.2.1 数组优缺点
- 查找容易 (通过下标),时间复杂度为 O(1)。
- 通过下标,修改元素快。
- 插入、删除元素难,效率低。
- 插入操作平均需要移动 n/2 个元素。
- 删除操作平均需要移动 (n-1)/2 个元素。
- 扩展繁琐,一方面需要确保能提供更大区域的连续内存空间,另一方面需要将原有数据复制到新的顺序表中。
1.2.2 代码设计
1、定义
定义动态数组的结构体,其中包含指向数组的指针、数组的容量 (可以容纳的最大元素个数)、数组中实际的元素个数。
typedef int element_t;
typedef struct {
element_t* data;
size_t capacity;
size_t length;
} DynamicArray;
2、initDynamicArray 函数
初始化动态数组并设定数组容量,根据传入的 initCapacity 的值计算所需的内存,通过 malloc 给数组 array 分配内存空间,数组 array 的容量即为 initCapacity,同时将数组 array 的长度初始化为 0。
void initDynamicArray(DynamicArray *array, size_t initCapacity) {
array->data = (element_t*)malloc(sizeof(element_t) * initCapacity);
array->capacity = initCapacity;
array->length = 0;
}
3、resizeDynamicArray 函数
调整动态数组的内存大小,将指向数组 array 的指针和要扩大的倍数 (一般为 2 的整数倍) newCapacity 传入函数,先计算所需的内存大小,通过 realloc 函数重新给数组分配内存大小,将 newCapacity 赋给数组的容量参数。
void resizeDynamicArray(DynamicArray *array, size_t newCapacity) {
array->data = (element_t*)realloc(array->data, sizeof(element_t) * newCapacity);
array->capacity = newCapacity;
}
4、insertAt 函数
- 判断需要插入的位置是否合理,若需要插入的位置
index 超过数组的长度 array->length,则不合理。
- 判断数组设定的容量是否足够,若容量不够则需要通过
resizeDynamicArray 函数扩容。
- 插入元素,通过循环从数组最后一个元素开始,每个元素都往自己的后一个元素位置移动,直到循环到下标
index 处时,将要插入的元素 element 放入到 array->data[index] 中。
- 数组长度自增 1。
void insertAt(DynamicArray *array, size_t index, element_t element) {
if (index > array->length) {
printf("%zu 位置不合理!!!\n", index);
return;
}
if (array->length == array->capacity) {
resizeDynamicArray(array, array->capacity * 2);
}
for (size_t i = array->length; i > index; i--) {
array->data[i] = array->data[i - 1];
}
array->data[index] = element;
array->length++;
}
5、insertEnd 函数
在末尾插入元素,则直接调用 insertAt 函数,传入数组的长度,即为数组末尾。
void insertEnd(DynamicArray *array, element_t element) {
insertAt(array, array->length, element);
}
6、getLength 函数
size_t getLength(const DynamicArray *array) {
return array->length;
}
7、deleteAt 函数
- 判断需要删除的位置是否合理,若是需要删除元素的下标
index 大于数组最后一个元素的下标,则删除位置不合理。
- 将需要被删除元素的数据的位置
array->data[index] 赋给指针 *delete_element。
- 利用循环将删除元素的位置,即下标为
index 处开始之后的元素都往前移动一个元素单位。
bool deleteAt(DynamicArray *array, size_t index, element_t *delete_element) {
if (index > array->length - 1) {
return false;
}
*delete_element = array->data[index];
for (; index < array->length - 1; index++) {
array->data[index] = array->data[index + 1];
}
array->length--;
return true;
}
8、deleteEnd 函数
删除末尾的元素并将被删除的元素保存,这直接调用 deleteAt 函数,传入最后一个元素的下标 array->length-1 即可。
bool deleteEnd(DynamicArray *array, element_t *delete_element) {
return deleteAt(array, array->length - 1, delete_element);
}
9、modifyAt 函数
修改指定位置的元素值,首先判断修改位置是否合理,再直接将新的值 element 赋给 array->data[index]。
void modifyAt(DynamicArray *array, size_t index, element_t element) {
if (index >= array->length) {
printf("%zu 位置不合理!!!", index);
return;
}
array->data[index] = element;
}
10、printfDynamicArray 函数
遍历数组,先输出当前数组的容量和长度,再通过 for 循环从头开始遍历数组。
void printfDynamicArray(DynamicArray *array) {
printf("容量:%zu , 长度:%zu, 元素:", array->capacity, array->length);
for (size_t i = 0; i < array->length; i++) {
printf("%d ", array->data[i]);
}
printf("\n\n");
}
11、freeDynamicArray 函数
释放动态数组的内存,直接利用 free 函数释放内存,然后再将数组的参数重置。
void freeDynamicArray(DynamicArray *array) {
free(array->data);
array->data = NULL;
array->capacity = 0;
array->length = 0;
}
12、主函数
int main() {
DynamicArray array;
initDynamicArray(&array, 8);
insertEnd(&array, 100);
insertEnd(&array, 200);
insertEnd(&array, 300);
insertEnd(&array, 400);
insertEnd(&array, 500);
insertEnd(&array, 600);
printfDynamicArray(&array);
insertAt(&array, 2, 250);
printfDynamicArray(&array);
element_t delete_element;
deleteEnd(&array, &delete_element);
printf("%d 被删除了\n", delete_element);
printfDynamicArray(&array);
deleteAt(&array, 2, &delete_element);
printf("%d 被删除了\n", delete_element);
printfDynamicArray(&array);
modifyAt(&array, 1, 20000);
printfDynamicArray(&array);
return 0;
}
1.3 链表
1.3.1 链表结构
- 逻辑结构:链表是一个线性结构,由一系列结点 (Node) 组成,每个结点包含数据元素和指向下一个结点的指针 (Pointer),所有的结点通过指针相连,形成一个链式结构,通常称第一个结点为头结点。
- 物理结构:于数组不同,链表中的结点需要自行组织,向系统申请很多分散在内存各处的结点,每个结点都保存了当前结点的数据和下一个结点的地址 (指针),通过指针将结点串成一串。
1.3.2 链表分类
1.3.3 链表优缺点
-
优点:
- 插入和删除操作效率高。
- 动态扩展性能更好,链表不需要像数组那样预先指定固定的大小,而是可以随时动态的增长或缩小。链表是真正的动态数据结构,不需要处理固定容量的问题。
-
缺点:
- 查找慢。由于链表中的结点不是连续存储的,无法像数组一样根据索引直接计算出每个结点的地址。必须从头结点开始遍历链表,直到找到目标结点,这导致链表的随机访问效率低。
- 额外的存储空间,链表的每一个结点都需要存储指向下一个结点的指针,会占用额外的空间。
1.3.4 代码设计
1、定义
- 定义结点的数据类型。
- 定义结点的结构体
Node 包含结点数据 data 和下一个结点的地址 Node *next。
- 定义链表的结构体
LinkedList 包含头结点地址和链表实际长度。
typedef int element_t;
typedef struct Node {
element_t data;
struct Node* next;
} Node;
typedef struct {
Node *head;
size_t length;
} LinkedList;
2、initLinkedList 函数
初始化链表,即头结点地址为 NULL,链表长度为 0。
void initLinkedList(LinkedList *list) {
list->head = NULL;
list->length = 0;
}
3、getLength 函数
返回链表的长度,直接通过 list->length 访问链表的长度参数。
size_t getLength(const LinkedList *list) {
return list->length;
}
4、getPrevNode 函数
返回指定位置的上一个结点,将链表的头结点 list->head 赋给一个新结点,再利用 for 循环,从链表第二个结点开始一直循环到指定的下标 index 处。
Node *getPrevNode(LinkedList *list, size_t index) {
Node *current_node = list->head;
for (size_t i = 1; i < index; i++) {
current_node = current_node->next;
}
return current_node;
}
5、insertAt 函数
- 判断插入位置是否合理,若插入位置超过链表长度,则不合理。
- 创建一个新的结点,此结点的数据为需要插入的元素
element,指向下一个结点的指针为 NULL。
- 分情况:
- a. 作为头结点插入,则将链表的头结点的地址
list->head 赋给新结点连接下一个结点的指针 new_node->next,把新结点的地址 new_node 赋给头结点地址 list->head。
- b. 作为中间结点插入,则利用
getPrevNode 函数找到需要插入位置的上一个结点的地址,将返回的上一个结点的地址赋给一个 Node *current_node 存储,current_node->next 的下一个结点的参数赋给新结点中指向下一个结点的参数,再将新结点赋给 current_node->next。
- 长度自增。
void insertAt(LinkedList *list, size_t index, element_t element) {
if (index > list->length) {
printf("%zu 位置不合理!!!\n", index);
return;
}
Node *new_node = (Node *)malloc(sizeof(Node));
new_node->data = element;
new_node->next = NULL;
if (index == 0) {
new_node->next = list->head;
list->head = new_node;
} else {
Node *current_node = getPrevNode(list, index);
new_node->next = current_node->next;
current_node->next = new_node;
}
list->length++;
}
6、insertEnd 函数
void insertEnd(LinkedList *list, element_t element) {
insertAt(list, list->length, element);
}
7、deleteAt 函数
- 判断删除位置是否合理。
- 分情况:
- a. 删除头结点,先保存头结点的地址和值,再将头结点的下一个结点的地址赋给头结点。
- b. 删除中间结点,先利用
getPrevNode 函数找到需要插入位置的上一个结点的地址,将返回的上一个结点的地址 current_node,保存被删除结点的地址和值,再将被删除结点的下一个结点的地址 current_node->next->next 赋给被删除结点的上一个结点中指向下一个结点地址的参数 current_node->next。
- 利用
free 函数释放删除的结点内存。
- 长度自减。
bool deleteAt(LinkedList *list, size_t index, element_t *delete_element) {
if (index >= list->length) {
return false;
}
Node *delete_node;
if (index == 0) {
delete_node = list->head;
*delete_element = list->head->data;
list->head = list->head->next;
} else {
Node *current_node = getPrevNode(list, index);
delete_node = current_node->next;
*delete_element = current_node->next->data;
current_node->next = current_node->next->next;
}
free(delete_node);
list->length--;
return true;
}
8、deleteEnd 函数
bool deleteEnd(LinkedList *list, element_t *delete_element) {
return deleteAt(list, list->length - 1, delete_element);
}
9、getValue 函数
- 判断位置是否合理。
- 分情况:
- a. 头结点的值,直接访问头结点的数据值
list->head->data。
- b. 其他结点的值,利用
getPrevNode 函数找到需要插入位置的上一个结点的地址,将返回的结点的地址 prev_node,在访问返回结点的下一个结点的数据值 prev_node->next->data。
void getValue(LinkedList *list, size_t index, element_t *element) {
if (index >= list->length) {
printf("%zu 位置不合理!!! \n", index);
return;
}
if (index == 0) {
*element = list->head->data;
} else {
Node *prev_node = getPrevNode(list, index);
*element = prev_node->next->data;
}
}
10、setValue 函数
- 判断位置是否合理。
- 分情况:
- a. 头结点的值,直接修改头结点的值
list->head->data。
- b. 其他结点的值,利用
getPrevNode 函数找到需要插入位置的上一个结点的地址,将返回的结点的地址 prev_node,再把修改的新值赋给返回的结点的下一个结点的数据参数 prev_node->next->data。
void setValue(LinkedList *list, size_t index, element_t new_value) {
if (index >= list->length) {
printf("%zu 位置不合理!!! \n", index);
return;
}
if (index == 0) {
list->head->data = new_value;
} else {
Node *prev_node = getPrevNode(list, index);
prev_node->next->data = new_value;
}
}
11、freeLinkedList 函数
释放链表内存,利用 while 循环,只要当前结点 current_node 的参数不为空,就保存当前结点的地址 delete_node,再把当前结点的下一个结点的地址 current_node->next 赋给当前结点 current_node,再利用 free 函数释放保存的地址 delete_node,最后重置链表的值和链表的长度。
void freeLinkedList(LinkedList *list) {
Node *current_node = list->head;
Node *delete_node;
while (current_node != NULL) {
delete_node = current_node;
current_node = current_node->next;
free(delete_node);
}
list->head = NULL;
list->length = 0;
}
12、printfLinkedList 函数
打印链表,利用 while 循环,只要当前结点 current_node 的参数不为空,就说明当前结点不是最后一个结点,就输出当前结点的信息。
void printfLinkedList(LinkedList *list) {
printf("长度:%zu \n", list->length);
printf("head: %p \n", list->head);
Node *current_node = list->head;
while (current_node != NULL) {
printf("[当前节点地址:%p , 当前结点的值:%d , 下个结点的地址:%p] \n", current_node, current_node->data, current_node->next);
current_node = current_node->next;
}
printf("\n\n");
}
13、主函数
int main() {
LinkedList list;
initLinkedList(&list);
insertEnd(&list, 100);
insertEnd(&list, 200);
insertEnd(&list, 300);
insertEnd(&list, 400);
insertEnd(&list, 500);
printfLinkedList(&list);
insertAt(&list, 0, 50);
printfLinkedList(&list);
insertAt(&list, 3, 250);
printfLinkedList(&list);
element_t delete_element;
deleteEnd(&list, &delete_element);
printf("%d 被删除了!!! \n", delete_element);
printfLinkedList(&list);
deleteAt(&list, 3, &delete_element);
printf("%d 被删除了!!! \n", delete_element);
printfLinkedList(&list);
element_t element;
getValue(&list, 2, &element);
printf("index = 2 的结点的值:%d \n", element);
getValue(&list, 0, &element);
printf("index = 0 的结点的值:%d \n", element);
setValue(&list, 0, 50000);
setValue(&list, 2, 20000);
printfLinkedList(&list);
freeLinkedList(&list);
return 0;
}
1.4 栈
1.4.1 栈的定义
栈 (stack),是限制在只能在表的一端进行插入和删除操作的线性表。
特点:后进先出 (LIFO,Last In First Out) 或先进后出 (FILO,First In Last Out) 的线性表。
- 栈顶 (Top):允许进行插入、删除操作的一端,又称为表尾。栈顶由一个称为栈顶指针的位置指示器 (变量) 来指示,他是动态变化的。
- 栈底 (Bottom):是固定不变的,不允许进行插入和删除的一端,又称为表头。
- 空栈:不包含任何元素的空表。
- 设栈 S=(a1,a2,…,an ),则 a1 称为栈底元素,an 为栈顶元素,栈中元素按 a1,a2,…,a_n 的次序进栈 (压栈、push),出栈 (弹栈,pop) 的第一个元素应为栈顶元素,出栈顺序为:an,…,a2,a1。
1.4.2 代码设计
1、定义
定义结构体 Stack,其中包括栈元素地址的指针 data、栈容量 capacity、栈长度 length。
typedef int element_t;
typedef struct {
element_t* data;
size_t capacity;
size_t length;
} Stack;
2、initStack 函数
初始化栈,利用 malloc 函数根据传入的初始化容量 initCapacity 计算分配内存的大小,初始化栈容量和栈长度。
void initStack(Stack *stack, size_t initCapacity) {
stack->data = (element_t*)malloc(sizeof(element_t) * initCapacity);
stack->capacity = initCapacity;
stack->length = 0;
}
3、getLength 函数
直接访问栈的长度参数 stack->length。
size_t getLength(const Stack *stack) {
return stack->length;
}
4、resizeStack 函数
利用 realloc 函数根据传入的新的容量 newCapacity 计算分配内存的大小,重新设置栈容量。
void resizeStack(Stack *stack, size_t newCapacity) {
stack->data = (element_t*)realloc(stack->data, sizeof(element_t) * newCapacity);
stack->capacity = newCapacity;
}
5、pushStack 函数
入栈,先判断容量是否足够,不足时调用 resizeStack 函数扩容,再从栈尾入栈。
void pushStack(Stack *stack, element_t element) {
if (stack->length == stack->capacity) {
resizeStack(stack, stack->capacity * 2);
}
stack->data[stack->length] = element;
stack->length++;
}
6、popStack 函数
出栈(删除元素),先将栈尾元素的地址(栈尾元素的下标是栈长度减 1)保存,再将栈长度减一。
bool popStack(Stack *stack, element_t *deleted_element) {
if (stack->length == 0) {
return false;
}
*deleted_element = stack->data[stack->length - 1];
stack->length--;
return true;
}
7、freeStack 函数
void freeStack(Stack *stack) {
free(stack->data);
stack->data = NULL;
stack->capacity = 0;
stack->length = 0;
}
1.5 队列
1.5.1 队列的定义
队列 (Queue):也是操作受限的线性表,限制为仅允许在表的一端进行插入 (入队或进队),在表的另一端进行删除 (出队或离队) 操作。先进先出 (First In First Out,简称 FIFO)。队列中没有元素时,称为空队列。
- 队首 (front):允许进行删除一端。
- 队尾 (rear):允许进行插入的一端。
顺序队列中存在'假溢出'现象:尽管队列中实际元素个数可能远远小于数组大小,但可能由于尾指针 rear 已超出向量空间的上界而不能做入队操作。为充分利用空间,克服上述'假溢出'现象,有两种方法。
- 方法 1:使用数组实现,入队列时添加到队列的最后,出队列时移除第一个元素同时将右侧的元素左移。
- 方法 2:为队列分配的向量空间看成是一个首尾相接的圆环,这种队列称为循环队列。
1.5.2 代码设计
1、定义
定义队列中元素的类型,定义队列结构体,包含队列的内存地址、队列容量、队列长度、队首、队尾等参数。
typedef int element_t;
typedef struct {
element_t* data;
size_t capacity;
size_t length;
size_t front;
size_t rear;
} Queue;
2、initQueue 函数
利用 malloc 函数动态分配队列内存,初始化队列的容量、长度、队首和队尾。
void initQueue(Queue *queue, size_t initCapacity) {
queue->data = (element_t*)malloc(sizeof(element_t) * initCapacity);
queue->capacity = initCapacity;
queue->length = 0;
queue->front = 0;
queue->rear = 0;
}
3、getLength 函数
size_t getLength(const Queue *queue) {
return queue->length;
}
4、enqueueQueue 函数
- 判断队列状态,队列是否满员。
- 入队,将新元素赋给指向队尾的元素的数据。
- 计算下一个队尾的位置,
(队尾+1)% 队列容量这样能够让指向队尾的指针一直根据队列容量循环。
- 队列长度自增。
void enqueueQueue(Queue *queue, element_t element) {
if (queue->length == queue->capacity) {
printf("队列满,%d 入队失败!!!\n", element);
return;
}
queue->data[queue->rear] = element;
queue->rear = (queue->rear + 1) % queue->capacity;
queue->length++;
}
5、dequeueQueue 函数
- 判断是否为空队列,空队列无法出队。
- 将要出队的元素(从队首出队)保存。
- 计算下一个出队的位置
(队首+1)% 队列容量 这样使得指向队首的指针也一直根据队列容量循环。
- 队列长度自减。
bool dequeueQueue(Queue *queue, element_t *deleted_element) {
if (queue->length == 0) {
printf("空队列不允许出队\n");
return false;
}
*deleted_element = queue->data[queue->front];
queue->front = (queue->front + 1) % queue->capacity;
queue->length--;
return true;
}
6、freeQueue 函数
释放队列内存,利用 free 函数释放队列的值,再把队列的值赋 NULL、容量、长度、队首指针、队尾指针都赋 0。
void freeQueue(Queue *queue) {
free(queue->data);
queue->data = NULL;
queue->capacity = 0;
queue->length = 0;
queue->front = 0;
queue->rear = 0;
}
7、printfQueue 函数
void printfQueue(Queue *queue) {
printf("capacity: %zu , length: %zu , front: %zu , rear: %zu \n", queue->capacity, queue->length, queue->front, queue->rear);
printf("元素:");
for (size_t i = 0; i < queue->length; i++) {
size_t index = (queue->front + i) % queue->capacity;
printf("%d ", queue->data[index]);
}
printf("\n\n");
}
8、主函数
int main() {
Queue queue;
initQueue(&queue, 5);
enqueueQueue(&queue, 100);
enqueueQueue(&queue, 200);
enqueueQueue(&queue, 300);
enqueueQueue(&queue, 400);
enqueueQueue(&queue, 500);
enqueueQueue(&queue, 600);
printfQueue(&queue);
element_t delete_element;
dequeueQueue(&queue, &delete_element);
printfQueue(&queue);
dequeueQueue(&queue, &delete_element);
printfQueue(&queue);
freeQueue(&queue);
return 0;
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown 转 HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
- HTML 转 Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online