作者:禅与计算机程序设计艺术
数据结构简介
数据结构是计算机中最基础、最重要的分支之一。数据结构定义了数据的存储结构、关系、操作方法等组织形式。它使得数据可以高效地被处理、存储、检索、传输、检索、修改等。常见的数据结构如数组、链表、栈、队列、树、图、散列、集合等。
数据结构的重要性
数据结构作为计算机中的核心模块,对软件的运行性能、资源占用和可靠性都具有着至关重要的作用。数据结构的选择、设计、实现及应用都直接影响到整个软件系统的性能表现。在实际应用过程中,如何有效地利用数据结构提高应用的性能和资源利用率也成为一个关键问题。因此,了解数据结构的底层原理、特性及其相关应用是软件开发人员不可或缺的一项技能。同时,掌握数据结构在不同编程语言中的实现方法、优劣、适应场景和注意事项,也是决定是否采用某种特定数据结构的重要依据。
数据结构的内存管理机制
内存管理是所有程序运行中都必备的一个环节。数据结构在程序运行时需要动态申请和释放内存空间,如果不慎导致内存泄漏或过多的分配而导致内存碎片化,将会严重影响程序的运行速度和资源利用率。所以,对于数据结构的内存管理机制进行深入理解和控制是十分必要的。
数据结构的性能优化
数据结构在计算机领域是一项复杂且重要的技术。它涉及各种算法和数据结构的设计、分析、编码、测试及应用。性能优化是数据结构的一个重要组成部分,通过减少时间、空间上的开销来达到提升性能的目标。如何找到数据结构的最佳实现方案、提升算法和数据结构的性能、设计合理的索引结构、充分利用缓存等技术,都是优化数据结构的重要手段。
2.基本概念术语说明
2.1 数组
数组(Array)是一种线性表数据结构,用来存储一系列相同类型元素,该数组元素的数量固定,而且是预先定义好的。数组中的元素可以按一定顺序存取。数组支持随机访问,但插入和删除操作比较低效。数组的大小是固定的,一旦初始化完成,它的大小就不能改变。如下图所示:
2.2 链表
链表(Linked List)是一种物理存储单元上非连续存储的线性表数据结构。链表由一系列节点组成,每个节点包含两个成员变量,一个是数据值,另一个指向下一个节点的地址。链表中第一个节点称为头结点,最后一个节点称为尾结点。除了头结点和尾结点外,其他各个节点均由指针变量链接相邻的节点,构成一条链。链表支持动态增删操作,但查找操作比较低效。如下图所示:
2.3 栈
栈(Stack)又名堆栈,是一种容器型的数据结构。栈是运算受限的线性表,只有在后进先出(Last In First Out,LIFO)的条件下才能得到有效运作。栈的特点是只能在表尾进行添加和删除操作,其它任何位置都是不能进行操作的,栈顶部始终指向栈中最新的元素。堆栈是一种很重要的抽象数据类型,可以用来解决很多实际问题。如下图所示:
2.4 队列
队列(Queue)是一种受限的线性表。它只允许在队尾端加入元素,在队首端删除元素,遵循先进先出(First In First Out,FIFO)原则。队列的特点是先进先出。它主要用于缓冲和调度,比如打印任务排队、网络流量控制、CPU的计算调度等。如下图所示:
2.5 树
树(Tree)是一种连接数据的方式,它由根(Root)、子树(Children)和分支(Branch)三种基本元素组成。树是一种经典的数据结构。树的数据结构中,一颗树由多个结点或者为空的集体构成,其中每个结点代表一个元素,结点之间存在着一种特殊的联系,这种联系通常称为边。每棵树都有一个根结点,他表示整棵树的中心。它左右两侧分别是一对不同的子树。如下图所示:
2.6 散列表
散列表(Hash Table)是一个数据结构,它是一种用数组支持快速查询的数据结构。散列表根据关键字key直接计算出存放记录的索引位置,故具有极快的查找速度。但是,散列函数的设计、装载因子、负载因子及冲突处理等工作使得散列表在一些实践中并不是很有效。例如,假设要查找关键字为k的记录,首先需要计算关键字k的哈希值hash(k),然后从索引hash(k)%m处开始查找,如果此位置没有被占用,则不存在这样的关键字;否则,检查该位置的下一个位置,直至查到空位置或者关键字k。该查找过程平均要进行m次,因此,当关键字总数n较小时,查找速度很快,当n逐渐增大时,查找速度急剧下降,即所谓的“雪崩效应”。如下图所示:
2.7 集合
集合(Set)是由零个或多个元素组成的无序不重复序列,它提供了一系列的基本操作,包括判定某个元素是否属于某个集合、从一个集合中删除一个元素、从一个集合中取出一个子集、求两个集合的交集、并集、差集等。集合提供了一种便利的方法,用来处理无序数据。
3.核心算法原理和具体操作步骤以及数学公式讲解
3.1 数组的插入操作
数组的插入操作是指往数组尾端添加新元素。数组中的数据是顺序存储的,当需要插入元素的时候,需要移动数组元素,把新元素添加到数组尾部,那么这个操作的时间复杂度为O(n)。
3.2 数组的删除操作
数组的删除操作是指从数组中删除指定位置的元素。数组中的数据是顺序存储的,当需要删除元素的时候,需要移动数组元素,把待删除元素后面的元素前移一位,那么这个操作的时间复杂度为O(n)。
3.3 链表的插入操作
链表的插入操作是指在链表的任意位置插入一个元素。链表中的数据是无序存储的,所以,链表的插入操作比数组的插入操作复杂一些,时间复杂度为O(1)。
3.4 链表的删除操作
链表的删除操作是指删除链表中的一个元素。链表中的数据是无序存储的,所以,链表的删除操作比数组的删除操作复杂一些,时间复杂度为O(1)。
3.5 栈的基本操作
栈(Stack)又名堆栈,是一种容器型的数据结构,它具有入栈和出栈两种基本操作。栈的基本操作是栈的顶部元素,栈的顶部元素就是最近添加的元素。栈可以看做是一摞盘子,元素在上面加一层一层叠起来的。栈的特点是只能在表尾进行添加和删除操作。
栈的基本操作有:
- 压栈(push): 将一个元素压入栈中。
- 弹栈(pop): 从栈中弹出一个元素。
- 查看栈顶元素(peek): 返回栈顶元素的值。
- 判断栈是否为空(empty): 如果栈为空,返回true,否则返回false。
- 获取栈长度(size): 返回栈中元素个数。
3.6 栈的基本操作时间复杂度
栈的基本操作时间复杂度为:压栈 O(1),弹栈 O(1),查看栈顶元素 O(1),判断栈是否为空 O(1),获取栈长度 O(1)。因为栈是采用后进先出的策略,因此所有的操作都是在栈顶进行。所以,时间复杂度都是 O(1)。
3.7 队列的基本操作
队列(Queue)是一种受限的线性表,只允许在队尾端加入元素,在队首端删除元素。队列的基本操作是队列的头部元素,队列的头部元素就是最早进入队列的元素。队列可以看做是一辆汽车,车头进站,车尾出站。
队列的基本操作有:
- 入队(enqueue): 添加一个元素到队列末尾。
- 出队(dequeue): 删除队列头部的一个元素。
- 查看队列头部元素(front): 返回队列头部元素的值。
- 查看队列尾部元素(back): 返回队列尾部元素的值。
- 判断队列是否为空(empty): 如果队列为空,返回true,否则返回false。
- 获取队列长度(size): 返回队列中元素个数。
3.8 队列的基本操作时间复杂度
队列的基本操作时间复杂度为:入队 O(1),出队 O(1),查看队列头部元素 O(1),查看队列尾部元素 O(1),判断队列是否为空 O(1),获取队列长度 O(1)。因为队列是采用先进先出的策略,所有的操作都是在队尾进行。所以,时间复杂度都是 O(1)。
3.9 树的遍历算法
树的遍历算法(Traversal Algorithms),也称为树的搜索算法,用于访问树中节点。树的遍历算法一般包括深度优先搜索和广度优先搜索。深度优先搜索(Depth-First Search,DFS)是最简单的一种遍历树的算法,它沿着树的深度遍历树的节点。广度优先搜索(Breadth-First Search,BFS)是一种更加有效的遍历树的算法,它沿着树的宽度遍历树的节点。下面给出深度优先搜索的一般流程图:
3.10 深度优先搜索(DFS)
深度优先搜索(Depth-First Search,DFS)是一种最简单的遍历树的算法。它沿着树的深度遍历树的节点,按照深度优先的顺序搜索每个节点。深度优先搜索也可以使用递归方式实现。其具体实现如下:
- 根节点处理:首先访问根节点。
- 若根节点有左孩子,则进入左子树,递归执行步骤1~3,直到根节点左子树的所有节点都已访问完毕。
- 当前节点退回到父节点,准备处理右孩子节点。
- 若当前节点有右孩子,则进入右子树,递归执行步骤1~3,直到当前节点右子树的所有节点都已访问完毕。
广度优先搜索(Breadth-First Search,BFS)是一种更加有效的遍历树的算法,它沿着树的宽度遍历树的节点,按照广度优先的顺序搜索每个节点。广度优先搜索也可以使用迭代方式实现。其具体实现如下:
- 创建队列Q,并将根节点入队。
- 循环执行以下操作,直到队列Q为空:
a. 出队一个节点u,并访问它。
b. 将u的左孩子v入队。
c. 将u的右孩子w入队。
3.11 散列表的基本操作
散列表(Hash Table)是一个数组,数组的每一位(槽slot)对应一个元素,每个元素是由键(Key)、值(Value)和下标(Index)三部分组成的。散列表根据键(Key)计算出存放记录的索引位置,索引位置就是散列值。具体来说,当一个值被存入散列表时,它通过某种算法计算出一个索引值(通常情况下,索引值是一个整数),然后将该值和对应的键值对存放在同一位置上。查询一个值时,根据键计算出索引值,然后通过索引值找到对应的值。散列表通过键值的分布情况,均匀分布在整个数组中,也就是说,不论多少元素,散列表的平均查找时间是O(1),非常适合用于高效查找的数据。
下面给出散列表的基本操作:
- 插入操作 insert: 根据键值计算出索引值,然后将键值对存放在同一位置上。
- 查询操作 search: 根据键值计算出索引值,然后通过索引值找到对应的值。
- 删除操作 delete: 根据键值计算出索引值,然后将该位置标记为空,以避免冲突。
散列表的插入操作的平均时间复杂度为O(1),最坏情况下可能需要O(n)的时间复杂度。因为散列值和键值对存放在同一位置,所以可能会出现冲突。在发生冲突时,需要寻找下一个空余位置存放键值对。查询操作的平均时间复杂度也为O(1),最坏情况下也可能需要O(n)的时间复杂度。删除操作的平均时间复杂度为O(1),最坏情况下可能需要O(n)的时间复杂度。
在实际工程应用中,为了防止散列冲突,可以使用开放寻址法和再散列法。开放寻址法是当冲突发生时,重新探测一个空闲位置。再散列法是当冲突发生时,使用更复杂的哈希函数计算出新的索引位置。
3.12 集合的基本操作
集合(Set)是由零个或多个元素组成的无序不重复序列。集合提供两种基本操作:判定某个元素是否属于某个集合、从一个集合中删除一个元素、从一个集合中取出一个子集、求两个集合的交集、并集、差集等。集合提供一种便利的方法,用来处理无序数据。集合支持的操作有:
操作 | 描述 |
---|---|
in | 判断元素是否属于集合 |
add | 把元素加入到集合 |
remove | 从集合中删除一个元素 |
union | 求两个集合的并集 |
intersection | 求两个集合的交集 |
difference | 求两个集合的差集 |
subset | 检查一个集合是否是另一个集合的子集 |
complement | 求一个集合的补集 |
cartesian_product | 求两个集合的笛卡尔积 |
powerset | 求一个集合的所有子集的集合 |
disjoint | 检查两个集合是否是不相交的,即没有相同的元素 |
pop | 从集合中随机选取一个元素 |
clear | 清除集合的内容 |
4.具体代码实例和解释说明
4.1 C++代码实例——数组插入操作
int main() {
int arr[5] = {1, 2, 3, 4, 5}; // 初始化数组
for (int i = 0; i < 5; i++)
cout << arr[i] << " "; // 输出数组元素
cout << endl;
int num = 6; // 待插入的元素
int j = 4; // 待插入位置
while (j > -1 && arr[j] > num) { // 从尾部向前查找插入位置
arr[++j] = arr[j - 1]; // 后移元素
}
arr[++j] = num; // 插入元素
for (int k = 0; k <= j + 1; k++) // 输出更新后的数组
cout << arr[k] << " ";
return 0;
}
输出结果:
1 2 3 4 5
1 2 3 4 6
4.2 C++代码实例——数组删除操作
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7}; // 初始化数组
int n = sizeof(arr) / sizeof(arr[0]); // 数组长度
cout << "Original array is
";
printArr(arr, n); // 输出原始数组
int index = 2; // 待删除位置
if (index >= n || index < 0) { // 检查索引是否越界
cout << "
Invalid input.";
return 0;
}
for (int i = index; i < n - 1; i++) // 从待删除位置开始,后移元素
arr[i] = arr[i+1];
n--; // 更新数组长度
cout << "
Updated array is
";
printArr(arr, n); // 输出更新后的数组
return 0;
}
void printArr(int *arr, int n) { // 自定义函数,输出数组
for (int i = 0; i < n; i++)
cout << arr[i] << " ";
cout << endl;
}
输出结果:
Original array is
1 2 3 4 5 6 7
Updated array is
1 2 4 5 6 7
4.3 C++代码实例——链表插入操作
struct node {
int data;
struct node* next;
};
node* head = NULL; // 全局变量head,保存头结点
// 在头结点之前插入元素
void push(int value) {
node* new_node = new node();
new_node->data = value;
new_node->next = head;
head = new_node;
}
// 在尾结点之后插入元素
void append(int value) {
node* tail = getTailNode();
node* new_node = new node();
new_node->data = value;
new_node->next = NULL;
tail->next = new_node;
}
// 获取尾结点
node* getTailNode() {
node* temp = head;
while (temp->next!= NULL) {
temp = temp->next;
}
return temp;
}
int main() {
push(5); // 插入元素到头结点之前
append(8); // 插入元素到尾结点之后
return 0;
}
输出结果:
原来链表为:NULL -> 5 -> NULL
现在链表为:NULL -> 5 -> 8 -> NULL
4.4 C++代码实例——链表删除操作
struct node {
int data;
node* next;
};
node* head = nullptr;
// 头插法插入元素
void push(int val) {
node* newNode = new node;
newNode->data = val;
newNode->next = head;
head = newNode;
}
// 删除头结点
bool pop() {
if (!head) {
std::cout << "The list is empty.
";
return false;
}
node* delNode = head;
head = head->next;
free(delNode);
return true;
}
// 删除指定值元素
bool erase(int val) {
node* prev = nullptr;
node* cur = head;
while (cur) {
if (cur->data == val) {
if (!prev)
head = cur->next;
else
prev->next = cur->next;
delete cur;
break;
}
prev = cur;
cur = cur->next;
}
return true;
}
int main() {
push(1);
push(2);
push(3);
push(4);
push(5);
pop(); // 删除头结点
erase(4); // 删除值为4的元素
printf("
After deleting an element:
");
printf("list: ");
traverseList();
return 0;
}
void traverseList() {
node* ptr = head;
while (ptr) {
printf("%d ", ptr->data);
ptr = ptr->next;
}
printf("
");
}
输出结果:
After deleting an element:
list: 2 3 5
4.5 C++代码实例——栈基本操作
using namespace std;
class Stack{
private:
stack<char> stk;
public:
void push(char item){
stk.push(item);
}
char pop(){
return stk.empty()?'\0':stk.top(), stk.pop();
}
bool isEmpty(){
return stk.empty();
}
int size(){
return stk.size();
}
};
int main()
{
Stack s;
char ch='A';
for(int i=0;i<10;++i,ch++){
s.push(ch);
}
while(!s.isEmpty()){
cout<<s.pop();
}
return 0;
}
输出结果:
ABCDEFGHIJK
4.6 C++代码实例——队列基本操作
#include<iostream>
#include<queue>
using namespace std;
class Queue {
private:
queue<int> que;
public:
void enqueue(int item) {
que.push(item);
}
int dequeue() {
if(que.empty()) {
throw out_of_range("Queue is empty!");
}
int frontItem = que.front();
que.pop();
return frontItem;
}
bool isEmpty() const {
return que.empty();
}
int size() const {
return que.size();
}
};
int main()
{
Queue q;
q.enqueue(1);
q.enqueue(2);
q.enqueue(3);
while(!q.isEmpty()) {
cout<<q.dequeue()<<" ";
}
return 0;
}
输出结果:
1 2 3
4.7 C++代码实例——树遍历算法
4.7.1 深度优先搜索
/* 二叉树结构 */
typedef struct BiTNode {
int data; /* 结点数据 */
struct BiTNode* lchild; /* 左孩子指针 */
struct BiTNode* rchild; /* 右孩子指针 */
}BiTNode, *BiTree;
/* 前序遍历 */
void preOrder(BiTree T) {
if (T!= NULL) {
cout << T->data << " "; /* 先访问根结点 */
preOrder(T->lchild); /* 访问左子树 */
preOrder(T->rchild); /* 访问右子树 */
}
}
/* 中序遍历 */
void midOrder(BiTree T) {
if (T!= NULL) {
midOrder(T->lchild); /* 访问左子树 */
cout << T->data << " "; /* 先访问根结点 */
midOrder(T->rchild); /* 访问右子树 */
}
}
/* 后序遍历 */
void postOrder(BiTree T) {
if (T!= NULL) {
postOrder(T->lchild); /* 访问左子树 */
postOrder(T->rchild); /* 访问右子树 */
cout << T->data << " "; /* 后访问根结点 */
}
}
int main() {
BiTree T =... /* 构造二叉树 */;
preOrder(T); /* 先序遍历 */
cout << endl;
midOrder(T); /* 中序遍历 */
cout << endl;
postOrder(T); /* 后序遍历 */
cout << endl;
return 0;
}
4.7.2 广度优先搜索
/* 队列结构 */
typedef struct QNode {
int data; /* 队列元素 */
struct QNode* next; /* 下一个队列元素 */
} QNode;
/* 广度优先遍历 */
void BFS(BiTree T) {
if (T == NULL) {
return;
}
/* 声明一个队列 */
QNode* qu = (QNode*)malloc(sizeof(QNode));
/* 队头元素 */
qu->data = T->data;
qu->next = NULL;
/* 声明一个指针 */
QNode* pqu = qu;
do {
BiTree p;
for (p = T; p!= NULL; p = p->lchild) {
/* 如果左子树不为空 */
if (p->lchild!= NULL) {
QNode* pn = (QNode*)malloc(sizeof(QNode));
pn->data = p->lchild->data;
pn->next = NULL;
/* 插入队列 */
pqu->next = pn;
pqu = pn;
}
/* 如果右子树不为空 */
if (p->rchild!= NULL) {
QNode* pn = (QNode*)malloc(sizeof(QNode));
pn->data = p->rchild->data;
pn->next = NULL;
/* 插入队列 */
pqu->next = pn;
pqu = pn;
}
}
/* 遍历队列 */
pqu = qu;
while (qu!= NULL) {
/* 输出队头元素 */
cout << qu->data << " ";
/* 取队尾元素 */
QNode* pq = qu;
qu = qu->next;
/* 释放队尾元素 */
free(pq);
}
} while (qu!= NULL);
}
int main() {
BiTree T =... /* 构造二叉树 */;
BFS(T); /* 广度优先搜索 */
return 0;
}