一.线性表的定义
线性表:零个或多个元素的有限序列
首先它是一个序列,其中的元素是有顺序的,即第一个元素无前驱,最后一个元素无后继,剩下的每个都有且只有一个前驱和后继,线性表的元素个数n叫表的长度,n=0时,线性表称作空表。其中每个元素的下标称作该元素在线性表中的位序。
在较为复杂的线性表中,一个数据元素可以由若干个数据项组成。
二.线性表的抽象数据类型(ADT)
2.1.数据对象
D={ai|ai属于elemset,(i=1,2,...,n,n大于等于0)}
2.2.数据关系
R={<ai-1,ai>|ai-1,ai属于D}(序偶关系,一组有序的数)
2.3.基本操作
-
初始化线性表
- 操作:
InitList()
- 初始条件: 无
- 操作结果: 构造一个空的线性表。
- 操作:
-
销毁线性表
- 操作:
DestroyList()
- 初始条件: 线性表已存在
- 操作结果: 销毁线性表。
- 操作:
-
清空线性表
- 操作:
ClearList()
- 初始条件: 线性表已存在
- 操作结果: 将线性表重置为空表。
- 操作:
-
判断线性表是否为空
- 操作:
ListEmpty()
- 初始条件: 线性表已存在
- 操作结果: 若线性表为空,返回
true
;否则返回false
。
- 操作:
-
获取线性表的长度
- 操作:
ListLength()
- 初始条件: 线性表已存在
- 操作结果: 返回线性表中元素的个数。
- 操作:
-
获取指定位置的元素
- 操作:
GetElem(L,i.*e)
- 初始条件: 线性表已存在,且 1 ≤ i ≤ 线性表长度
- 操作结果: 返回线性表中第 i 个位置的元素。
- 操作:
-
元素的位置
- 操作:
LocateElem(e)
- 初始条件: 线性表已存在
- 操作结果: 查找线性表中第一个值为 e 的元素,返回其位置;若不存在,返回 0。
- 操作:
-
在指定位置插入元素
- 操作:
ListInsert(L,i, e)
- 初始条件: 线性表已存在,且 1 ≤ i ≤ 线性表长度+1
- 操作结果: 在线性表的第 i 个位置插入新元素 e,线性表长度加1。
- 操作:
-
删除指定位置的元素
- 操作:
ListDelete(*L,i,*e)
- 初始条件: 线性表已存在,且 1 ≤ i ≤ 线性表长度
- 操作结果: 删除线性表的第 i 个位置的元素,线性表长度减1。
- 操作:
-
遍历线性表
- 操作:
ListTraverse(L)
- 初始条件: 线性表已存在
- 操作结果: 从第一个元素到最后一个元素,依次对线性表中的每个数据元素做某种操作。
- 操作:
三.线性表的顺序存储结构
3.1.定义
线性表的顺序存储结构,指的是用一段地址连续(元素间不能有空)的存储单元依次存储线性表的数据元素(逻辑上相邻的元素在存储中也相邻)
3.2.方式
由于顺序表具有,地址连续,依次存放,随机存取,类型相同的特点。所以用一堆数组来存储线性表中的元素。
代码如下
#include <iostream>
using namespace std;
template <typename T>
class SeqList {
private:
T* data; // 存储数据的数组
int maxSize; // 顺序表的最大容量
int length; // 当前顺序表的长度
我们可以发现顺序存储结构的三个属性
- 存储空间的起始位置:数组data,他的存储位置就存储空间的存储位置
- 线性表的最大存储容量:数组长度maxSize
- 线性表当前的长度:length
注意:数组长度指的是存储空间的长度,一般是不变的(有编程手段可以实现动态数组,但会带来性能上的损耗),线性表长度则是看线性表中数据元素的个数,是可以变化的,就好比要装一堆衣服,先去拿了箱子,再把衣服一件件放进去,所以数组长度是要大于等于线性表长度的
3.3.地址计算方法
地址:存储器中的每个存储单元都有自己的编号,这个编号就叫地址
假设每个数据元素占用c个存储单元,loc(a1)为基地址,则有一个很简单的数学公式来计算地址
loc(ai)=loc(a1)+(i-1)*c
那么根据这个公式,我们可以观察出,对于计算机来说,在线性表中每个位置存入或取出数据所花的时间都是相等的,也就是一个常数,则这个操作的时间复杂度为O(1),一般把具有这种特点的结构叫随机存储结构
3.4.具体操作
3.4.1.获取元素操作
int getElement(int index) {
if (index < 0 || index >= length) {
cout << "参数无效" << endl;
return -1; // 返回一个无效值表示出错
}
return data[index];
}
3.4.2.元素插入操作
void insertElement(int index, int element) {
if (index < 0 || index > length) {
cout << "参数无效" << endl;
return;
}
if (length >= MAXSIZE) {
cout << "线性表已满" << endl;
return;
}
for (int i = length; i > index; --i) {
data[i] = data[i - 1];
}
data[index] = element;
length++;
}
思路
- 如果插入位置不合理,抛出异常
- 如果线性表长度大于等于数组长度,抛出异常或动态增加容量
- 从最后一个位置开始向前遍历到第i个位置,分别将他们向后移一个位置
- 把元素插入空位
- 表长加1
3.4.3元素删除操作
void deleteElement(int index) {
if (index < 0 || index >= length) {
cout << "参数无效" << endl;
return;
}
for (int i = index; i < length - 1; ++i) {
data[i] = data[i + 1];
}
length--;
}
思路
- 删除位置不合理,抛出异常
- 取出删除元素
- 从删除元素开始向后遍历到最后一个元素的位置,分别将它们向前移一个位置
- 表长减1
我们可以观察出,获取元素的时间复杂度是0(1),而插入和删除最坏情况要遍历整个表,则时间复杂度是0(n)
3.4.4.完整代码
#include <iostream>
using namespace std;
const int MAXSIZE = 100;
class LinearList {
private:
int data[MAXSIZE];
int length;
public:
// 构造函数
LinearList() {
length = 0;
}
// 创建线性表
void createList(int arr[], int size) {
if (size > MAXSIZE) {
cout << "线性表创建成功" << endl;
return;
}
for (int i = 0; i < size; ++i) {
data[i] = arr[i];
}
length = size;
}
// 获取元素
int getElement(int index) {
if (index < 0 || index >= length) {
cout << "参数无效" << endl;
return -1; // 返回一个无效值表示出错
}
return data[index];
}
// 插入元素
void insertElement(int index, int element) {
if (index < 0 || index > length) {
cout << "参数无效" << endl;
return;
}
if (length >= MAXSIZE) {
cout << "线性表已满" << endl;
return;
}
for (int i = length; i > index; --i) {
data[i] = data[i - 1];
}
data[index] = element;
length++;
}
// 删除元素
void deleteElement(int index) {
if (index < 0 || index >= length) {
cout << "参数无效" << endl;
return;
}
for (int i = index; i < length - 1; ++i) {
data[i] = data[i + 1];
}
length--;
}
// 显示线性表
void displayList() {
for (int i = 0; i < length; ++i) {
cout << data[i] << " ";
}
cout << endl;
}
};
int main() {
LinearList list;
int arr[] = { 1, 2, 3, 4, 5 };
list.createList(arr, 5);
cout << "表 ";
list.displayList();
cout << "2的元素为 " << list.getElement(2) << endl;
list.insertElement(2, 10);
cout << "把2插入10 ";
list.displayList();
list.deleteElement(3);
cout << "删除3 ";
list.displayList();
cin.get();
return 0;
}
3.5.线性表顺序结构的优缺点
3.5.1.优点
- 无须为表中元素之间的逻辑关系的增加而增加额外的存储空间
- 可以快速的获取表中任意位置的元素
3.5.2.缺点
- 插入和删除要移动大量元素
- 长度变化较大时难以确定存储空间的容量
- 造成存储空间“碎片”
四.线性表的链式存储结构
4.1.基础概念
我们思考顺序存储结构的缺点,发现顺序是因为数据元素间没有空隙,所以需要移动大量元素,十分耗费时间。
让元素间预留出一个空位如何,但是不能解决同一位置多次插入的问题
那提前分析并预留空位如何,这么搞效率太低,而且空间上损耗太大
现在要让元素间留有一定空位,干脆所有元素都不考虑相邻位置的问题,哪有空位就去哪,只需要在每个元素上做好标记,让我们可以通过标记找到它的下一个元素的位置(内存地址)。于是我们得到了链式存储结构,用任意的一组存储单元存储线性表的数据元素
而对于数据ai来说不同于顺序存储结构的是,它还需要存储一个指示其直接后继的信息
我们将存储元素信息的域称作数据域,把存储直接后继位置的域称作指针域(其中信息称作指针或链),这两部分组成数据元素的存储映像,称作结点(最后一个结点指向空),n个结点链结成一个链表,就是线性表的链式存储结构
如果一个链表的每个结点只包含一个指针域,它就是单链表
单链表一般会在头一个结点前附设一个结点,称作头结点,头结点可以不存储任何信息,也可以存链表长度之类的
4.2.头指针和头结点的区别
4.2.1.头指针
- 头指针是指向第一个结点的指针,如果链表有头结点,那头指针要指向头结点
- 头指针有标志作用,所以一般用头指针冠以链表的名字
- 头指针是链表的必要元素,不论链表是否为空
4.2.2头结点
- 头结点是为了方便设立的,其内容一般无意义
- 是链表的非必要元素
4.3单链表
4.3.1创建
#include <stdio.h>
#include <stdlib.h>
// 定义节点结构
typedef struct Node {
int data; // 数据域
struct Node* next; // 指针域,指向下一个节点
} Node;
4.3.2插入
一般有三种插入方法,分别为
- 头部插入
- 尾部插入
- 指定位置插入
头部插入
void insertAtHead(int data) {
// 创建新节点
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = head; // 新节点的next指向当前头节点
head = newNode; // 更新头节点为新节点
}
尾部插入,要先遍历链表找到最后一个结点
void insertAtTail(int data) {
// 创建新节点
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL; // 新节点的next为NULL
if (head == NULL) { // 如果链表为空
head = newNode; // 新节点为头节点
return;
}
Node* current = head;
while (current->next != NULL) { // 找到最后一个节点
current = current->next;
}
current->next = newNode; // 将最后一个节点的next指向新节点
}
指定位置插入
void insertAtPosition(int data, int position) {
// 创建新节点
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
if (position == 0) { // 如果插入位置为0,相当于头部插入
newNode->next = head;
head = newNode;
return;
}
Node* current = head;
int count = 0;
// 找到目标位置的前一个节点
while (current != NULL && count < position - 1) {
current = current->next;
count++;
}
if (current == NULL) { // 如果位置超出链表长度
printf("Position out of bounds.\n");
free(newNode);
return;
}
newNode->next = current->next; // 插入节点
current->next = newNode;
}
4.3.3删除
删除分为两种,按值删除和按位置删除
按值删除
void deleteByValue(int data) {
if (head == NULL) { // 如果链表为空
printf("List is empty.\n");
return;
}
if (head->data == data) { // 如果头节点是要删除的节点
Node* temp = head;
head = head->next;
free(temp);
return;
}
Node* current = head;
while (current->next != NULL && current->next->data != data) {
current = current->next;
}
if (current->next == NULL) { // 如果未找到数据
printf("Value not found in the list.\n");
return;
}
Node* temp = current->next;
current->next = current->next->next; // 删除节点
free(temp);
}
按位置删除
void deleteByPosition(int position) {
if (head == NULL) { // 如果链表为空
printf("List is empty.\n");
return;
}
if (position == 0) { // 如果要删除的是头节点
Node* temp = head;
head = head->next;
free(temp);
return;
}
Node* current = head;
int count = 0;
// 找到目标位置的前一个节点
while (current != NULL && count < position - 1) {
current = current->next;
count++;
}
if (current == NULL || current->next == NULL) { // 如果位置超出链表长度
printf("Position out of bounds.\n");
return;
}
Node* temp = current->next;
current->next = current->next->next; // 删除节点
free(temp);
}
4.4双链表
在定义中加入一个指向前驱结点的指针
#include <stdio.h>
#include <stdlib.h>
// 定义双链表节点
typedef struct Node {
int data;
struct Node* next;
struct Node* prev;
} Node;
// 插入节点到双链表末尾
void insert(Node** head_ref, int new_data) {
Node* new_node = (Node*)malloc(sizeof(Node));
Node* last = *head_ref;
new_node->data = new_data;
new_node->next = NULL;
// 如果链表为空,将新节点设为头节点
if (*head_ref == NULL) {
new_node->prev = NULL;
*head_ref = new_node;
return;
}
// 否则,找到链表的最后一个节点
while (last->next != NULL) {
last = last->next;
}
last->next = new_node;
new_node->prev = last;
}
// 删除指定数据的节点
void deleteNode(Node** head_ref, int key) {
Node* temp = *head_ref;
// 找到需要删除的节点
while (temp != NULL && temp->data != key) {
temp = temp->next;
}
// 如果没有找到节点,直接返回
if (temp == NULL) {
return;
}
// 如果要删除的节点是头节点
if (*head_ref == temp) {
*head_ref = temp->next;
}
// 修改前驱节点的 next 指针
if (temp->next != NULL) {
temp->next->prev = temp->prev;
}
// 修改后继节点的 prev 指针
if (temp->prev != NULL) {
temp->prev->next = temp->next;
}
free(temp);
}
// 遍历双链表
void printList(Node* node) {
Node* last;
printf("Forward traversal: ");
while (node != NULL) {
printf("%d ", node->data);
last = node;
node = node->next;
}
printf("\n");
}
int main() {
Node* head = NULL;
insert(&head, 1);
insert(&head, 2);
insert(&head, 3);
insert(&head, 4);
printf("Original doubly linked list:\n");
printList(head);
deleteNode(&head, 3);
printf("After deleting node with data 3:\n");
printList(head);
system("pause");
return 0;
}
4.5循环链表
循环链表是一种特殊的链表形式,链表的最后一个节点的指针指向头节点,从而形成一个环。循环链表可以是单向的(单向循环链表)或双向的(双向循环链表)。
优点:
- 从任意节点开始遍历,最终都会返回到该节点,适合需要循环访问的场景。
- 更加节省空间,因为不需要额外的节点来指示链表的结尾。
缺点:
- 可能会造成无限循环,需要额外的判断来避免死循环。
- 操作相对普通链表更为复杂,需要注意指针操作。
#include <stdio.h>
#include <stdlib.h>
// 定义循环链表节点
typedef struct Node {
int data;
struct Node* next;
} Node;
// 插入节点到循环链表
void insert(Node** head_ref, int new_data) {
Node* new_node = (Node*)malloc(sizeof(Node));
Node* last = *head_ref;
new_node->data = new_data;
new_node->next = *head_ref;
if (*head_ref == NULL) {
new_node->next = new_node;
*head_ref = new_node;
return;
}
while (last->next != *head_ref) {
last = last->next;
}
last->next = new_node;
}
// 删除指定数据的节点
void deleteNode(Node** head_ref, int key) {
if (*head_ref == NULL) {
return;
}
Node *current = *head_ref, *prev;
if (current->data == key && current->next == *head_ref) {
free(current);
*head_ref = NULL;
return;
}
while (current->next != *head_ref && current->data != key) {
prev = current;
current = current->next;
}
if (current->data == key) {
prev->next = current->next;
free(current);
}
}
// 遍历循环链表
void printList(Node* head) {
Node* temp = head;
if (head != NULL) {
do {
printf("%d ", temp->data);
temp = temp->next;
} while (temp != head);
}
printf("\n");
}
int main() {
Node* head = NULL;
insert(&head, 1);
insert(&head, 2);
insert(&head, 3);
insert(&head, 4);
printf("Original circular linked list:\n");
printList(head);
deleteNode(&head, 3);
printf("After deleting node with data 3:\n");
printList(head);
return 0;
}
4.6静态链表
静态链表是用数组来实现的链表,虽然使用的是数组,但数组中的每个元素存储的内容类似于链表的节点。每个数组元素通常由数据部分和指针部分组成,其中指针部分存储的是下一个元素的索引,而不是指针。
优点:
- 可以避免动态内存分配的开销,提高性能。
- 在不支持动态内存分配的环境中使用。
缺点:
- 大小固定,无法动态扩展。
- 插入和删除操作复杂度较高,尤其是在数组大小较大时。
#include <stdio.h>
#define SIZE 100
// 静态链表节点定义
typedef struct {
int data;
int next;
} Node;
// 初始化静态链表
void initializeList(Node list[], int* head) {
*head = -1;
for (int i = 0; i < SIZE - 1; i++) {
list[i].next = i + 1;
}
list[SIZE - 1].next = -1;
}
// 插入数据
void insert(Node list[], int* head, int* free, int data) {
if (*free == -1) {
printf("List is full!\n");
return;
}
int new_index = *free;
*free = list[*free].next;
list[new_index].data = data;
list[new_index].next = *head;
*head = new_index;
}
// 删除数据
void deleteNode(Node list[], int* head, int* free, int key) {
int prev = -1, current = *head;
while (current != -1 && list[current].data != key) {
prev = current;
current = list[current].next;
}
if (current == -1) {
printf("Node with data %d not found.\n", key);
return;
}
if (prev == -1) {
*head = list[current].next;
} else {
list[prev].next = list[current].next;
}
list[current].next = *free;
*free = current;
}
// 遍历静态链表
void printList(Node list[], int head) {
int current = head;
while (current != -1) {
printf("%d ", list[current].data);
current = list[current].next;
}
printf("\n");
}
int main() {
Node list[SIZE];
int head, free;
initializeList(list, &head);
free = 0;
insert(list, &head, &free, 1);
insert(list, &head, &free, 2);
insert(list, &head, &free, 3);
printf("Original static linked list:\n");
printList(list, head);
deleteNode(list, &head, &free, 2);
printf("After deleting node with data 2:\n");
printList(list, head);
return 0;
}