- 一、线性表
- 1.1 概念与特点
- 1.2 线性表的存储结构
- 1.3 常见操作
- 1.4 应用场景
- 二、链表
- 2.1 链表简介
- 2.2 单向链表(单链表)
- 2.21 基本概念
- 2.22 单链表基本操作
- 2.23 C语言实现
- 2.3 双向链表
- 2.4 循环链表
一、线性表
线性表是一种最基本、最简单的数据结构,数据元素之间仅具有单一的前驱和后继关系,它提供了简单而强大的方法来处理和操作数据集合。
1.1 概念与特点
- 定义:线性表是由n(n≥0)个数据元素组成的有限序列,其中每个元素只有一个前驱和一个后继(除了第一个和最后一个元素)。
- 特点:
- 线性结构:线性表中的元素之间存在线性关系,每个元素仅有一个直接前驱和一个直接后继。
- 有序性:线性表中的元素按照其在序列中的位置依次排列,位置由线性表的首尾确定。
- 可变性:线性表的长度可以动态地增加或减少。
线性表的数据元素具有抽象(即不确定)的数据类型,在实际问题中,数据元素的抽象类
型将被具体的数据类型所取代。(常见整形和结构体)
1.2 线性表的存储结构
线性表的存储结构有两种常见的实现方式:顺序存储和链式存储。
(1)顺序存储结构:
顺序存储结构使用一段连续的内存空间来存储线性表中的元素。在内存中,线性表的元素按照其在序列中的位置依次存放。其中,首元素存放在起始地址,后续元素紧接着存放在连续的内存单元中。
- 优点:
- 随机访问:由于元素在内存中的连续存储,可以通过元素的下标快速直接访问元素。
- 简单高效:存取元素的时间复杂度为O(1),适用于频繁访问和随机访问元素的场景。
- 缺点:
- 大小固定:顺序存储结构需要预先分配一段连续的内存空间,导致大小固定,不易扩展。
- 插入和删除操作效率低:在插入和删除元素时,需要移动其他元素的位置,导致效率较低。
(2)链式存储结构:
链式存储结构通过节点之间的指针连接来存储线性表中的元素。每个节点包含两部分信息:数据域用于存储元素值,指针域用于指向下一个节点的地址。
-
优点:
- 灵活扩展:链式存储结构可以动态地分配内存,便于根据实际需求进行大小的调整。
- 插入和删除操作效率高:在插入和删除元素时,只需调整相邻节点的指针,效率较高。
-
缺点:
- 随机访问困难:由于元素之间不是连续存储的,无法通过下标直接访问元素,需要从头节点开始依次遍历至目标位置。
- 需要额外的指针空间:链式存储结构需要额外的指针空间来存储节点之间的连接关系。
总结:
顺序存储结构适用于频繁访问和随机访问元素的场景,而链式存储结构适用于动态大小和频繁插入、删除操作的场景。
1.3 常见操作
- 插入操作:向线性表的指定位置插入一个新元素,需要移动后续元素的位置。
- 删除操作:从线性表中删除指定位置的元素,需要移动后续元素的位置。
- 查找操作:根据元素的值或位置在线性表中进行查找,可以获取指定元素或元素所在位置的信息。
- 修改操作:修改线性表中指定位置的元素的值。
- 获取长度:获取线性表中元素的个数。
1.4 应用场景
线性表作为一种基本的数据结构,广泛应用于各个领域,包括但不限于以下几个方面:
- 数组:数组是线性表的一种实现方式,用于存储具有相同类型的数据集合,广泛用于数据存储和处理、图像处理、数值计算等领域。
- 链表:链表是线性表的另一种实现方式,通过指针将元素连接起来,常用于动态内存管理、数据库系统、操作系统等领域。
- 栈:栈是一种特殊的线性表,遵循"先进后出"(LIFO)的原则,常用于函数调用、表达式求值、撤销操作等场景。
- 队列:队列是一种特殊的线性表,遵循"先进先出"(FIFO)的原则,常用于任务调度、消息传递、缓冲区管理等场景。
二、链表
2.1 链表简介
链表是一种常见且重要的数据结构,用于组织和存储数据。相比于顺序存储结构,链表通过节点之间的指针连接实现元素的存储和访问。本节将详细介绍链表的概念、特点、常见的类型以及操作和应用场景。
(1)概念与特点:
- 定义:链表是由一系列节点组成的数据结构,每个节点包含一个数据元素和一个指向下一个节点的指针。
- 特点:
- 链式结构:链表中的节点通过指针相互连接,形成链式结构。
- 无序性:链表中的节点可以按任意顺序排列。
- 可变性:链表的长度可以动态地增加或减少,不需要预先分配固定大小的内存空间。
(2)常见类型:
- 单向链表:每个节点只包含一个指向下一个节点的指针,最后一个节点指向空值。
- 双向链表:每个节点同时包含指向前一个节点和后一个节点的指针,使得可以在链表中进行双向遍历。
- 循环链表:最后一个节点的指针指向头节点,形成一个闭环的链表结构。
(3)常见操作:
- 插入操作:在链表的指定位置插入一个新节点,需要调整相邻节点的指针。
- 删除操作:从链表中删除指定位置的节点,需要调整相邻节点的指针。
- 查找操作:根据节点的值或位置在链表中进行查找,可以获取指定节点或节点所在位置的信息。
- 修改操作:修改链表中指定节点的值。
- 获取长度:获取链表中节点的个数。
(4)应用场景:
链表作为一种灵活的数据结构,广泛应用于各个领域,特别适用于以下几个方面:
- 动态数据集合:由于链表的可变性,适用于需要频繁插入和删除操作的场景,如实时数据流处理、编辑器的撤销操作等。
- 内存管理:链表的灵活性使其成为动态内存分配的基础,用于管理堆内存中的空闲块或垃圾回收算法。
- 数据结构的实现:链表是许多其他高级数据结构的基础,如栈、队列和图等,提供了灵活性和效率的支持。
2.2 单向链表(单链表)
2.21 基本概念
单链表(singly linked list)是用一组任意的存储单元存放线性表的元素,这组存储单元可以连续也可以不连续,甚至可以零散分布在内存中的任意位置。
为了能正确表示元素之间的逻辑关系,每个存储单元在存储数据元素的同时,还必须存储其后继元素所在的地址信息,即指针,这两部分组成了数据元素的存储映像,称为结点(node)。
单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起的,由于每个结点只有一个指针域,故称为单链表。
单链表中每个结点的存储地址存放在其前驱结点的 指针域中,而第一个元素无前驱,所以设头指针(head pointer)指向第一个元素所在结点(称为开始结点),整个单链表的存取必须从头指针开始进行,因而头指针具有标识一个单链表的作用;由于最后一个元素无后继,故最后一个元素所在结点(称为终端结点)的指针域为空,即NULL
(图中用“∧”表示),也称尾标志(tail mark)。
上面的表述中:元素从第一个节点开始存储,看起来没什么问题。但是当链表为空、在最前面插入和删除元素、遍历链表时会更加麻烦。所以,通常在第一个元素的节点前面添加一个头结点。(在后面的介绍中,可以想象没有头结点应该如何操作)
2.22 单链表基本操作
细介绍单链表的各种操作:
(1)创建链表:
创建一个空的单链表需要初始化一个头指针,并将头指针指向空值。
(2)插入操作:
- 头部插入:创建一个新节点,将新节点的指针指向原头节点,再将头指针指向新节点。
- 尾部插入:遍历链表,找到最后一个节点,将最后一个节点的指针指向新节点,再将新节点的指针指向空值。
- 中间插入:找到插入位置的前一个节点,将新节点的指针指向前一个节点的下一个节点,再将前一个节点的指针指向新节点。
(3)删除操作:
- 删除头节点:将头指针指向头节点的下一个节点,并释放原头节点的内存。
- 删除尾节点:遍历链表,找到倒数第二个节点,将该节点的指针指向空值,并释放最后一个节点的内存。
- 删除指定位置的节点:找到要删除位置的前一个节点,将前一个节点的指针指向要删除节点的下一个节点,并释放要删除节点的内存。
(4)查找操作:
- 根据值查找:从头节点开始遍历链表,比较每个节点的值与目标值,直到找到匹配的节点或遍历到链表末尾。
- 根据位置查找:从头节点开始遍历链表,按照位置依次查找,直到找到目标位置的节点或遍历到链表末尾。
-
修改操作:
根据指定位置或节点,将节点的数据域修改为新的值。 -
获取链表长度:
从头节点开始遍历链表,累计节点的个数,直到遍历到链表末尾。 -
遍历链表:
从头节点开始依次遍历每个节点,可以输出节点的值或进行其他操作。
需要注意的是,在进行插入和删除操作时,要确保链表的指针关系正确,避免出现内存泄漏或指针丢失的情况。
单链表的操作灵活性较高,尤其适用于频繁插入和删除操作的场景。然而,由于无法直接访问前一个节点,某些操作可能需要从头节点开始遍历整个链表,因此在使用时需注意操作的效率。
2.23 C语言实现
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构
typedef struct Node {
int data;
struct Node* next;
} Node;
// 创建一个新节点
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
if (newNode != NULL) {
newNode->data = data;
newNode->next = NULL;
}
return newNode;
}
// 在链表头部插入新节点
void insertAtHead(Node** head, int data) {
Node* newNode = createNode(data);
if (newNode != NULL) {
newNode->next = *head;
*head = newNode;
}
}
// 在链表尾部插入新节点
void insertAtTail(Node** head, int data) {
Node* newNode = createNode(data);
if (newNode != NULL) {
if (*head == NULL) {
*head = newNode;
} else {
Node* current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = newNode;
}
}
}
// 在指定位置插入新节点
void insertAtPosition(Node** head, int data, int position) {
if (position <= 0) {
insertAtHead(head, data);
return;
}
Node* newNode = createNode(data);
if (newNode != NULL) {
Node* current = *head;
int currentPosition = 1;
while (current != NULL && currentPosition < position) {
current = current->next;
currentPosition++;
}
if (current != NULL) {
newNode->next = current->next;
current->next = newNode;
} else {
free(newNode); // 释放未使用的节点
printf("Invalid position.\n");
}
}
}
// 删除头节点
void deleteAtHead(Node** head) {
if (*head != NULL) {
Node* temp = *head;
*head = (*head)->next;
free(temp);
}
}
// 删除尾节点
void deleteAtTail(Node** head) {
if (*head != NULL) {
if ((*head)->next == NULL) {
free(*head);
*head = NULL;
} else {
Node* previous = *head;
Node* current = (*head)->next;
while (current->next != NULL) {
previous = current;
current = current->next;
}
previous->next = NULL;
free(current);
}
}
}
// 删除指定位置的节点
void deleteAtPosition(Node** head, int position) {
if (position <= 0) {
deleteAtHead(head);
return;
}
Node* current = *head;
Node* previous = NULL;
int currentPosition = 1;
while (current != NULL && currentPosition < position) {
previous = current;
current = current->next;
currentPosition++;
}
if (current != NULL) {
if (previous != NULL) {
previous->next = current->next;
} else {
*head = current->next;
}
free(current);
} else {
printf("Invalid position.\n");
}
}
// 查找节点是否存在
int search(Node* head, int key) {
Node* current = head;
while (current != NULL) {
if (current->data == key) {
return 1; // 节点存在
}
current = current->next;
}
return 0; // 节点不存在
}
// 修改指定位置的节点值
void modify(Node* head, int position, int newData) {
Node* current = head;
int currentPosition = 1;
while (current != NULL && currentPosition < position) {
current = current->next;
currentPosition++;
}
if (current != NULL) {
current->data = newData;
} else {
printf("Invalid position.\n");
}
}
// 获取链表长度
int getLength(Node* head) {
int length = 0;
Node* current = head;
while (current != NULL) {
length++;
current = current->next;
}
return length;
}
// 遍历链表并打印节点值
void display(Node* head) {
Node* current = head;
while (current != NULL) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
// 主函数测试链表操作
int main() {
Node* head = NULL;
insertAtHead(&head, 3);
insertAtHead(&head, 2);
insertAtHead(&head, 1);
display(head); // 输出:1 2 3
insertAtTail(&head, 4);
insertAtTail(&head, 5);
display(head); // 输出:1 2 3 4 5
insertAtPosition(&head, 10, 3);
display(head); // 输出:1 2 10 3 4 5
deleteAtHead(&head);
display(head); // 输出:2 10 3 4 5
deleteAtTail(&head);
display(head); // 输出:2 10 3 4
deleteAtPosition(&head, 2);
display(head); // 输出:2 3 4
int key = 3;
if (search(head, key)) {
printf("%d exists in the list.\n", key);
} else {
printf("%d does not exist in the list.\n", key);
}
modify(head, 2, 8);
display(head); // 输出:2 8 4
printf("Length of the list: %d\n", getLength(head)); // 输出:3
return 0;
}
2.3 双向链表
吃宵夜去了,待续…