一、线性表介绍
1.1、基本概念
线性表(Linear List)是数据结构中最基本、最简单、也是最常用的一种数据结构。它是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n=0时,线性表是一个空表。
若用L命名线性表,则其一般表示为
L=(a1 , a2,a3,......,ai,ai+1,......,an,)
a1是唯一的"第一个"元素,又称表头元素 (head)。
an是唯一的"最后一个元素",又称表尾元素 (tail)。
1.2、线性表的特征
(1)唯一的第一个元素:线性表中必存在唯一的一个"第一个元素"。
(2)唯一的最后一个元素:线性表中必存在唯一的一个"最后一个元素"。
(3)前驱和后继关系:除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。
(4)线性排列:线性表中的元素按线性顺序排列,每个元素都有唯一的前驱和后继。
(5)线性表中的元素是有限的,且每个元素的数据类型相同(C语言中因为线性表在内存中是连续存储的,使用相同的数据类型可以确保每个元素占用相同的内存空间,从而提高访问和操作的效率。但Python允许线性表中的元素具有不同的数据类型)。
二、线性表的实现
2.1、实现方式
(1)线性表主要的两种存储结构
顺序存储结构(顺序表): 用一组地址连续的存储单元依次存储线性表的数据元素。
**链式存储结构(链表):**用一组任意的存储单元存储线性表中的数据元素,并通过指针域存储直接后继的存储位置。
(2)线性表的基本操作
|---------------------------|--------------------------------|
| InitList(&L) | 初始化表,构造一个空的线性表。 |
| DestroyList(&L) | 销毁操作,销毁线性表,并释放线性表L所占用的内存空间。 |
| ListInsert(&L,i,e) | 按位插入,i是表L中的第i个位置,e是插入元素。 |
| ListDelete(&L,i,&e) | 按位删除,i是表L中的第i个位置,e用于返回删除元素的值。 |
| LocateElem(L,e) | 按值查找,e是要查找的值,没找到也要考虑。 |
| GetElem(L,i) | 按位查找,i是表L中的第i个位置,从而获取第i个位置的值 |
| Length(L) | 求线性表的长度,即表中数据元素的个数。 |
| PrintList(L) | 打印线性表,按顺序查看表的内容。 |
| Empty(L) | 判断表是否为空,根据返回结构判断,也可在其他操作中直接判断。 |
总结 :增删改 要用指向指针的指针,而查要用指针。具体说明如下:
结构体:
cpp
struct Node{
int data;
struct Node* next;
};
查 需要定义struct Node* head(这是一个指向 struct Node 类型的指针。它指向一个 Node 结构体的实例。例如,可以通过 head->a 访问 head 所指向的 Node 结构体中的成员 data)
增删改 需要定义 struct Node** head(这是一个指向 struct Node* 类型的指针,它是一个指向指针的指针,通常用于需要修改指针本身的情况)
2.2、顺序表的定义
(1)顺序表的概念
顺序表是一种基本的数据结构,它是用一段连续的存储单元依次存储数据元素的线性结构。顺序表通常采用数组来实现,因此也被称为动态数组或线性数组。
(2)顺序表的特点
**连续存储:**顺序表中的元素在内存中是连续存储的,这使得访问元素的时间复杂度为O(1),即可以通过下标直接访问元素。
**固定大小或动态扩展:**顺序表可以是固定大小的(静态顺序表),也可以是动态扩展的(动态顺序表)。动态顺序表可以根据需要自动扩展存储空间。
**高效的随机访问:**由于顺序表的元素是连续存储的,因此可以高效地进行随机访问和修改。
(3)基本操作代码实现
cpp
#include <stdio.h>
#define MaxSize 10
typedef struct {
int data[MaxSize];
int length;
} SqList;
// 初始化顺序表
void initSqList(SqList *list) {
list->length = 0;
}
// 添加元素
void addElement(SqList *list, int element) {
if (list->length >= MaxSize) {
printf("List is full\n");
return;
}
list->data[list->length++] = element;
}
// 删除指定位置的元素
void removeElement(SqList *list, int index) {
if (index < 0 || index >= list->length) {
printf("Index out of bounds\n");
return;
}
for (int i = index; i < list->length - 1; i++) {
list->data[i] = list->data[i + 1];
}
list->length--;
}
// 获取指定位置的元素
int getElement(SqList *list, int index) {
if (index < 0 || index >= list->length) {
printf("Index out of bounds\n");
return -1;
}
return list->data[index];
}
// 打印顺序表
void printSqList(SqList *list) {
for (int i = 0; i < list->length; i++) {
printf("%d ", list->data[i]);
}
printf("\n");
}
int main() {
SqList list;
initSqList(&list);
for(int i=1;i<=10;i++){
addElement(&list, i);
}
printSqList(&list);
removeElement(&list, 1);
printSqList(&list);
printf("查找第2个元素的值是: %d\n", getElement(&list, 1));
return 0;
}
2.3、链表的定义
(1)链表的概念
链表是一种常见的基础数据结构,用于存储一系列数据元素。与数组不同,链表的存储单元在物理上并不连续,而是通过指针链接在一起。每个链表节点包含两个部分:一个是存储数据元素的数据域,另一个是存储下一个节点地址的指针域。
(2)链表的种类型
**单链表:**每个节点只包含一个指向下一个节点的指针。
**双链表:**每个节点包含两个指针,分别指向前一个节点和后一个节点。
**循环单链表:**链表的最后一个节点指向第一个节点,形成一个环。
**循环双链表:**结合了双链表和循环链表的特点,形成一个双向循环的结构。
**静态链表:**使用数组来实现链表结构,每个数组元素包含数据和一个指向下一个元素的索引。
(3)链表的优缺点
**主要优点:**动态分配内存,节省空间。插入和删除操作较为方便,时间复杂度为O(1)。
**主要缺点:**访问特定元素时需要从头开始遍历,时间复杂度为O(n)。由于需要存储指针,空间开销较大。
(4)链表设计
链表在设计时可以选择是否带有头节点。头节点是一个特殊的节点,它通常不存储实际数据,而是作为链表的起始点。带头节点的链表和不带头节点的链表各有优缺点, 但多数链表设计以带头结点为主。
**带头节点的链表:**优点是统一了链表的操作,简化了代码。例如,插入和删除操作不需要处理特殊情况(如在链表头部插入或删除)。头节点始终存在,链表的第一个节点总是可以通过头节点访问。缺点是需要额外的存储空间来存储头节点。
**不带头节点的链表:**优点是节省了一个节点的存储空间。缺点是在进行插入和删除操作时,需要处理特殊情况,代码可能会稍微复杂一些。
(5)单链表实现
单链表的结构体中只带了一个指向下一个结点的指针(好处是增删操作简单,坏处是只能从头节点开始,逐个访问到链表的末尾),基本操作实现的代码如下:
cpp
#include <stdio.h>
#include <stdlib.h>
// 定义节点结构
typedef struct LNode {
int data;
struct LNode* next;
} LNode, *LinkList;
// 创建新节点
LNode* createNode(int data) {
LNode* newNode = (LNode*)malloc(sizeof(LNode));
newNode->data = data;
newNode->next = NULL;
return newNode;
}
// 在链表头部插入节点
void insertAtHead(LinkList* head, int data) {
LNode* newNode = createNode(data);
newNode->next = *head;
*head = newNode;
}
// 在链表尾部插入节点
void insertAtTail(LinkList* head, int data) {
LNode* newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
return;
}
LNode* temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newNode;
}
// 打印链表
void printList(LinkList head) {
printf("链表内容: ");
LNode* temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}
// 删除链表中指定内容的节点
void deleteNode(LinkList* head, int key) {
LNode* temp = *head;
LNode* prev = NULL;
if (temp != NULL && temp->data == key) {
*head = temp->next;
free(temp);
return;
}
while (temp != NULL && temp->data != key) {
prev = temp;
temp = temp->next;
}
if (temp == NULL) return;
prev->next = temp->next;
free(temp);
}
//指定位置插入结点
bool postioninsert(LinkList* head,int station,int b){
if(station<1) return false;
int k=0;
LNode* p=*head;
LNode* newnode=createNode(b);
while(p!=NULL&& k<station-1){
p=p->next;
k++;
}
newnode->next=p->next;
p->next=newnode;
return true;
}
//查找指定位置的结点
int find(LinkList head,int station){
LinkList temp=head;
int k=0;
while(temp!=NULL){
if(k==station){
return temp->data;
}else{
temp=temp->next;
k++;
}
}
return -1;
}
//释放内存
void freeList(LinkList head){
LinkList temp;
while(head!=NULL){
temp=head;
head=head->next;
free(temp);
}
}
int main() {
LinkList head = NULL;
LinkList tail=NULL;
for(int i=0;i<=10;i++){
insertAtHead(&head,i);
}
printList(head);
for(int i=0;i<=10;i++){
insertAtTail(&tail,i);
}
printList(tail);
postioninsert(&tail,3,16);
printList(tail);
deleteNode(&head, 6);
printList(head);
int result=find(tail,2);
printf("查找结果:%d",result);
freeList(head);
freeList(tail);
return 0;
}
(6)双链表实现
双链表的结构体中带了一个指向前一个结点的指针和一个指向下一个结点的指针(好处是可以从任意节点开始向前或向后遍历链表,坏处是增删操作复杂)。基本操作实现的代码如下:
cpp
#include <stdio.h>
#include <stdlib.h>
// 定义双链表节点结构
typedef struct LNode {
int data;
struct LNode* prev;
struct LNode* next;
} LNode, *LinkList;
// 创建新节点
LNode* createNode(int data) {
LNode* newNode = (LNode*)malloc(sizeof(LNode));
newNode->data = data;
newNode->prev = NULL;
newNode->next = NULL;
return newNode;
}
// 插入节点到链表头部
void insertAtHead(LinkList* head, int data) {
LNode* newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
} else {
newNode->next = *head;
(*head)->prev = newNode;
*head = newNode;
}
}
// 插入节点到链表尾部
void insertAtTail(LinkList* head, int data) {
LNode* newNode = createNode(data);
if (*head == NULL) {
*head = newNode;
} else {
LNode* temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = newNode;
newNode->prev = temp;
}
}
// 删除链表中指定元素的节点
void deleteNode(LinkList* head, int key) {
LNode* temp = *head;
while (temp != NULL && temp->data != key) {
temp = temp->next;
}
if (temp == NULL) return; // 节点不存在
if (temp->prev != NULL) temp->prev->next = temp->next;
if (temp->next != NULL) temp->next->prev = temp->prev;
if (temp == *head) *head = temp->next;
free(temp);
}
// 删除指定位置的节点
void deleteNodeAtPosition(LinkList* head, int position) {
if (*head == NULL || position < 0) return; // 链表为空或位置无效
LNode* temp = *head;
for (int i = 0; temp != NULL && i < position; i++) {
temp = temp->next;
}
if (temp == NULL) return; // 位置超出链表长度
if (temp->prev != NULL) temp->prev->next = temp->next;
if (temp->next != NULL) temp->next->prev = temp->prev;
if (temp == *head) *head = temp->next;
free(temp);
}
// 遍历链表并打印节点数据
void printList(LinkList head) {
LNode* temp = head;
printf("链表内容: ");
while (temp != NULL) {
printf("%d ", temp->data);
temp = temp->next;
}
printf("\n");
}
int main() {
LinkList head = NULL;
LinkList tail=NULL;
for(int i=1;i<=10;i++){
insertAtHead(&head,i);
}
printList(head);
for(int i=1;i<=10;i++){
insertAtTail(&tail,i);
}
printList(tail);
deleteNode(&head, 20);
printList(head);
deleteNodeAtPosition(&head, 1);
printList(head);
return 0;
}
(7)循环单链表实现
循环单链表是一种特殊的链表数据结构。它的特点是链表的最后一个节点指向第一个节点(而不是指向NULL),从而形成一个环。 需要初始化开始时指向自己,具体代码如下:
cpp
#include <stdio.h>
#include <stdlib.h>
// 定义节点结构体
typedef struct LNode {
int data;
struct LNode* next;
} LNode, *LinkList;
// 初始化循环单链表
LinkList initList() {
LinkList head = (LinkList)malloc(sizeof(LNode));
if (head == NULL) {
printf("Memory allocation failed\n");
return NULL;
}
head->next = head; // 指向自己,形成循环
return head;
}
// 插入节点
void insertNode(LinkList head, int data) {
LNode* newNode = (LNode*)malloc(sizeof(LNode));
if (newNode == NULL) {
printf("Memory allocation failed\n");
return;
}
newNode->data = data;
newNode->next = head->next;
head->next = newNode;
}
// 删除节点
void deleteNode(LinkList head, int data) {
LNode* p = head;
while (p->next != head && p->next->data != data) {
p = p->next;
}
if (p->next == head) {
printf("Node not found\n");
return;
}
LNode* temp = p->next;
p->next = temp->next;
free(temp);
}
// 遍历循环单链表
void traverseList(LinkList head) {
LNode* p = head->next;
printf("链表内容:");
while (p != head) {
printf("%d -> ", p->data);
p = p->next;
}
printf("HEAD\n");
}
int main() {
LinkList list = initList();
for(int i=1;i<=10;i++){
insertNode(list, i);
}
traverseList(list);
deleteNode(list, 7);
traverseList(list);
return 0;
}
(8)循环双链表实现
循环双链表是一种特殊的双向链表数据结构。它的特点是链表的最后一个节点指向第一个节点,而第一个节点的前驱指向最后一个节点,从而形成一个环。 需要初始化开始时指向自己,具体代码如下:
cpp
#include <stdio.h>
#include <stdlib.h>
// 定义节点结构体
typedef struct LNode {
int data;
struct LNode* prev;
struct LNode* next;
} LNode, *LinkList;
// 创建新节点
LNode* createNode(int data) {
LNode* newNode = (LNode*)malloc(sizeof(LNode));
newNode->data = data;
newNode->prev = newNode->next = NULL;
return newNode;
}
// 初始化循环双链表
LinkList initList() {
LinkList head = createNode(0); // 创建头节点
head->prev = head->next = head; // 头节点的前驱和后继都指向自己
return head;
}
// 插入节点到循环双链表
void insertNode(LinkList head, int data) {
LNode* newNode = createNode(data);
LNode* tail = head->prev; // 获取尾节点
tail->next = newNode;
newNode->prev = tail;
newNode->next = head;
head->prev = newNode;
}
// 根据位置删除节点
void deleteNode(LinkList head, int position) {
if (position < 1) {
printf("位置无效\n");
return;
}
LNode* current = head->next;
int count = 1;
while (current != head && count < position) {
current = current->next;
count++;
}
if (current == head) {
printf("位置超出链表长度\n");
return;
}
current->prev->next = current->next;
current->next->prev = current->prev;
free(current);
}
// 打印循环双链表
void printList(LinkList head) {
LNode* current = head->next; // 从头节点的下一个节点开始
printf("链表内容: ");
while (current != head) {
printf("%d ", current->data);
current = current->next;
}
printf("\n");
}
int main() {
LinkList list = initList();
for(int i=1;i<=10;i++){
insertNode(list, i);
}
printList(list);
deleteNode(list, 2); // 删除位置2的节点
printList(list);
return 0;
}
(9)静态链表实现
静态链表是一种使用数组来实现链表的数据结构。它在逻辑结构上相邻的数据元素存储在指定的一块内存空间中,但这些数据元素可以在这块内存空间中随机存放。增改改查操作方便,时间复杂度低,但灵活性较低,需要提前分配较大的空间。
cpp
#include <stdio.h>
#define MAX_SIZE 100 // 定义静态链表的最大容量
// 定义结点结构
typedef struct {
int data; // 数据域
int next; // 指针域,存储下一个结点的下标
} Node;
// 定义静态链表结构
typedef struct {
Node nodes[MAX_SIZE]; // 结点数组
int head; // 头结点下标
int free; // 空闲结点下标
} StaticLinkedList;
// 初始化静态链表
void initList(StaticLinkedList *list) {
list->head = -1; // 初始化头结点为-1,表示链表为空
list->free = 0; // 初始化空闲结点为0
for (int i = 0; i < MAX_SIZE - 1; i++) {
list->nodes[i].next = i + 1; // 将每个结点的next指向下一个结点
}
list->nodes[MAX_SIZE - 1].next = -1; // 最后一个结点的next为-1,表示没有下一个结点
}
// 分配一个空闲结点
int allocateNode(StaticLinkedList *list) {
if (list->free == -1) return -1; // 如果没有空闲结点,返回-1
int newNode = list->free; // 获取一个空闲结点
list->free = list->nodes[newNode].next; // 更新空闲结点的下标
return newNode; // 返回分配的结点下标
}
// 释放一个结点
void freeNode(StaticLinkedList *list, int node) {
list->nodes[node].next = list->free; // 将释放的结点的next指向当前空闲结点
list->free = node; // 更新空闲结点的下标
}
// 插入元素
void insert(StaticLinkedList *list, int data) {
int newNode = allocateNode(list); // 分配一个空闲结点
if (newNode == -1) {
printf("没有空闲结点.\n");
return;
}
list->nodes[newNode].data = data; // 将数据存储到新结点
list->nodes[newNode].next = list->head; // 将新结点的next指向当前头结点
list->head = newNode; // 更新头结点为新结点
}
// 删除元素
void deleteNode(StaticLinkedList *list, int data) {
int prev = -1; // 前驱结点下标
int curr = list->head; // 当前结点下标
while (curr != -1 && list->nodes[curr].data != data) {
prev = curr; // 更新前驱结点
curr = list->nodes[curr].next; // 更新当前结点
}
if (curr == -1) {
printf("没有找到元素.\n");
return;
}
if (prev == -1) {
list->head = list->nodes[curr].next; // 如果删除的是头结点,更新头结点
} else {
list->nodes[prev].next = list->nodes[curr].next; // 否则,更新前驱结点的next
}
freeNode(list, curr); // 释放当前结点
}
// 遍历链表并打印元素
void display(StaticLinkedList *list) {
printf("链表内容:");
int curr = list->head; // 当前结点下标
while (curr != -1) {
printf("%d -> ", list->nodes[curr].data); // 打印当前结点的数据
curr = list->nodes[curr].next; // 更新当前结点
}
printf("NULL\n"); // 打印链表结束标志
}
int main() {
StaticLinkedList list;
initList(&list); // 初始化链表
for(int i=1;i<=10;i++){
insert(&list, i);
}
display(&list);
deleteNode(&list, 6);
display(&list);
return 0;
}
三、线性表的应用
线性表在计算机和数学及算法中中有广泛的应用。以下是一些常见的应用场景:
(1)数组排序 :线性表可以用于实现各种排序算法,如冒泡排序、选择排序和插入排序等。通过比较和交换元素的位置,可以将无序的线性表转换为有序的线性表。
(2)动态内存分配 :线性表可用于实现动态内存分配。通过动态地添加或删除节点,可以有效地管理内存资源。
(3)多项式处理: 在数学计算中,线性表可以用于表示和处理多项式。例如,将多项式的系数存储在线性表中,方便进行多项式的加法、减法等操作。
(4)约瑟夫问题: 线性表可以用于解决约瑟夫问题,这是一种经典的算法问题,涉及循环链表的使用。
(5)文本处理: 在处理文本文件时,可以将文件内容存储为线性表,方便进行字符串的查找、替换等操作。
(6)数据库系统: 线性表可以用于表示数据库表格中的行数据,方便进行数据的插入、删除和查找。
**(7)操作系统:**线性表可以用于实现进程的内存管理,帮助操作系统有效地分配和回收内存。