1. 引言
在软件设计师考试中,数据结构是核心考察内容之一。掌握数据结构对于理解计算机算法、优化程序性能以及解决复杂问题至关重要。本章将介绍数据结构的重要性和在软件设计师考试中的考察目标。
1.1 数据结构的重要性
数据结构是计算机存储、组织数据的方式,它直接影响到程序的运行效率。合理的数据结构可以提高数据处理的效率,节省存储空间,简化算法设计。在软件开发中,选择合适的数据结构对于提升软件性能和质量具有重要意义。
1.2 软件设计师考试中数据结构的考察目标
软件设计师考试旨在评估考生对数据结构基本概念、原理和应用的理解和掌握程度。考试内容通常包括:
- 数据结构的基本概念和术语
- 线性结构(数组、链表、栈、队列)
- 树形结构(二叉树、平衡树)
- 图形结构(图的存储和遍历)
- 查找和排序算法
- 算法分析
- 数据结构的应用
通过这些内容的考察,测试考生是否具备设计、实现和优化数据结构的能力,以及能否将数据结构应用于解决实际问题。
2. 基本概念和术语
理解数据结构的基本概念和术语是掌握数据结构的前提。本章将介绍数据结构的定义、算法与数据结构的关系以及抽象数据类型(ADT)。
2.1 数据结构的定义
数据结构是计算机中存储、组织数据的方式,它能够使数据组织得更加有结构,以便可以更高效地存取和修改数据。数据结构包括数据元素之间的关系和数据元素的存储方式。
2.2 算法和数据结构的关系
算法是解决问题的方法,而数据结构是算法操作的对象。算法的效率很大程度上取决于所使用的数据结构。良好的数据结构可以提高算法的效率,简化算法的设计。反之,不当的数据结构可能导致算法效率低下,甚至无法解决问题。
示例:
- 使用数组实现的排序算法通常比使用链表实现的更高效,因为数组支持随机访问,而链表只支持顺序访问。
2.3 抽象数据类型(ADT)
抽象数据类型(Abstract Data Type,ADT)是数据结构的一种数学模型,它定义了一组数据和一组操作这些数据的操作,而无需指定数据的存储细节。ADT强调数据的逻辑结构和行为特性,而不是物理结构。
常见的抽象数据类型包括:
- 栈(Stack):后进先出(LIFO)的数据结构。
- 队列(Queue):先进先出(FIFO)的数据结构。
- 列表(List):有序的数据元素集合。
- 树(Tree):层次结构的数据结构。
- 图(Graph):由顶点和边组成的数据结构。
ADT的优点在于它提供了一种抽象层,使得我们可以在不关心具体实现细节的情况下,使用数据结构和算法。这种抽象性有助于提高代码的可读性、可维护性和可重用性。
示例:
// 栈的抽象数据类型定义
class Stack {
public:
void push(int value); // 入栈操作
int pop(); // 出栈操作
bool isEmpty(); // 判断栈是否为空
};
在本章中,我们介绍了数据结构的基本概念和术语,包括数据结构的定义、算法与数据结构的关系以及抽象数据类型(ADT)。理解这些基本概念对于深入学习数据结构至关重要。在下一章中,我们将探讨线性结构,包括数组、链表、栈和队列。
3. 线性结构
线性结构是数据结构中最基础的部分,其中的元素之间存在一对一的线性关系。本章将详细介绍线性结构中的数组、链表、栈和队列。
3.1 数组
数组是一种最基本的线性结构,它由相同类型的元素组成,这些元素在内存中是连续存放的。
3.1.1 数组的定义和特点
- 定义:数组是由具有相同类型的若干元素组成的有序集合。
- 特点:
- 随机访问:可以通过索引快速访问任意元素。
- 固定大小:数组的大小在创建时确定,之后不能改变。
- 内存连续:数组元素在内存中是连续存放的。
3.1.2 数组的存储结构
数组在内存中是一块连续的存储空间,每个元素的存储地址可以通过基地址加上偏移量计算得到。
示例代码(C++):
int arr[5] = {10, 20, 30, 40, 50};
cout << "Element at index 2: " << arr[2] << endl; // 输出:30
3.1.3 数组的优缺点
- 优点:
- 访问速度快:可以通过索引直接访问任意元素。
- 内存利用率高:元素在内存中连续存放,没有额外的存储开销。
- 缺点:
- 大小固定:数组的大小在创建时确定,之后不能改变。
- 插入和删除效率低:在数组中插入或删除元素需要移动大量元素。
3.2 链表
链表是一种由一系列节点组成的线性结构,每个节点包含数据域和指针域,指针域指向下一个节点。
3.2.1 链表的定义和特点
- 定义:链表是由一系列节点组成的有序集合,每个节点包含数据域和指针域。
- 特点:
- 动态大小:链表的大小可以根据需要动态变化。
- 非连续存储:链表元素在内存中不一定是连续存放的。
3.2.2 单链表
单链表是链表的一种,每个节点包含一个指针,指向下一个节点。
示例代码(C++):
struct Node {
int data;
Node* next;
};
Node* head = new Node{10, nullptr};
head->next = new Node{20, nullptr};
head->next->next = new Node{30, nullptr};
3.2.3 双向链表
双向链表是链表的一种,每个节点包含两个指针,分别指向前一个节点和后一个节点。
示例代码(C++):
struct Node {
int data;
Node* prev;
Node* next;
};
Node* head = new Node{10, nullptr, nullptr};
head->next = new Node{20, head, nullptr};
head->next->next = new Node{30, head->next, nullptr};
3.2.4 循环链表
循环链表是链表的一种,最后一个节点的指针指向第一个节点,形成一个环。
示例代码(C++):
struct Node {
int data;
Node* next;
};
Node* head = new Node{10, nullptr};
head->next = new Node{20, nullptr};
head->next->next = new Node{30, head}; // 形成环
3.2.5 链表的优缺点
- 优点:
- 动态大小:链表的大小可以根据需要动态变化。
- 插入和删除效率高:在链表中插入或删除元素只需要改变指针。
- 缺点:
- 访问速度慢:不能直接访问任意元素,需要从头节点开始遍历。
- 内存利用率低:每个节点需要额外的存储空间来存放指针。
在本章中,我们详细介绍了线性结构中的数组和链表。数组是一种基础的线性结构,具有随机访问的特点,但大小固定,插入和删除效率低。链表是一种动态的线性结构,插入和删除效率高,但访问速度慢。在下一章中,我们将探讨栈和队列。
4. 栈和队列
栈和队列是两种重要的线性结构,它们在数据的存取方式上有各自的特点和应用场景。
4.1 栈
4.1.1 栈的定义和特点
- 定义:栈是一种遵循后进先出(Last In First Out, LIFO)原则的线性结构。
- 特点:
- 只能在一端进行插入和删除操作,这一端被称为栈顶。
- 插入操作称为入栈(push),删除操作称为出栈(pop)。
4.1.2 栈的实现方法
栈可以用数组或链表来实现。数组实现的栈具有随机访问的特点,而链表实现的栈则具有更好的灵活性。
示例代码(C++ 使用数组实现栈):
#include <iostream>
#include <stack>
int main() {
std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
while (!s.empty()) {
std::cout << s.top() << " ";
s.pop();
}
return 0;
}
4.1.3 栈的应用
栈在许多算法和程序设计中都有应用,如表达式求值、括号匹配、函数调用等。
示例:括号匹配
#include <iostream>
#include <stack>
#include <string>
bool isBalanced(const std::string& expr) {
std::stack<char> s;
for (char c : expr) {
if (c == '(') {
s.push(c);
} else if (c == ')') {
if (s.empty() || s.top() != '(') {
return false;
}
s.pop();
}
}
return s.empty();
}
int main() {
std::string expr = "((a+b)*(c-d))";
if (isBalanced(expr)) {
std::cout << "The expression is balanced." << std::endl;
} else {
std::cout << "The expression is not balanced." << std::endl;
}
return 0;
}
4.2 队列
4.2.1 队列的定义和特点
- 定义:队列是一种遵循先进先出(First In First Out, FIFO)原则的线性结构。
- 特点:
- 插入操作通常在一端进行,称为队尾。
- 删除操作通常在另一端进行,称为队头。
4.2.2 队列的实现方法
队列可以用数组或链表来实现。数组实现的队列需要处理循环队列的问题,而链表实现的队列则更加灵活。
示例代码(C++ 使用链表实现队列):
#include <iostream>
#include <list>
template<typename T>
class Queue {
private:
std::list<T> elements;
public:
void enqueue(const T& element) {
elements.push_back(element);
}
T dequeue() {
if (elements.empty()) {
throw std::runtime_error("Queue is empty");
}
T frontElement = elements.front();
elements.pop_front();
return frontElement;
}
bool isEmpty() const {
return elements.empty();
}
};
int main() {
Queue<int> q;
q.enqueue(1);
q.enqueue(2);
q.enqueue(3);
while (!q.isEmpty()) {
std::cout << q.dequeue() << " ";
}
return 0;
}
4.2.3 循环队列
循环队列是一种特殊的队列,它使用固定大小的数组来存储元素,并使用两个指针(通常称为头指针和尾指针)来指示队列的前端和后端。
示例代码(C++ 实现循环队列):
#include <iostream>
#include <vector>
class CircularQueue {
private:
std::vector<int> data;
int head;
int tail;
int capacity;
public:
CircularQueue(int size) : capacity(size), head(0), tail(0) {
data.resize(capacity);
}
bool enqueue(int value) {
if ((tail + 1) % capacity == head) {
return false; // Queue is full
}
data[tail] = value;
tail = (tail + 1) % capacity;
return true;
}
int dequeue() {
if (head == tail) {
throw std::runtime_error("Queue is empty");
}
int value = data[head];
head = (head + 1) % capacity;
return value;
}
bool isEmpty() const {
return head == tail;
}
};
int main() {
CircularQueue q(3);
q.enqueue(1);
q.enqueue(2);
std::cout << q.dequeue() << " "; // 输出 1
q.enqueue(3);
std::cout << q.dequeue() << " "; // 输出 2
std::cout << q.dequeue() << " "; // 输出 3
return 0;
}
4.2.4 队列的应用
队列在许多算法和程序设计中都有应用,如任务调度、缓冲处理、广度优先搜索等。
在本章中,我们详细介绍了栈和队列的定义、特点、实现方法和应用。栈是一种后进先出的数据结构,常用于处理递归、回溯等问题;队列是一种先进先出的数据结构,常用于任务调度、缓冲处理等场景。在下一章中,我们将探讨树形结构,包括二叉树、平衡树等。
5. 树形结构
树形结构是一类重要的非线性数据结构,它模拟了树的层次关系,具有丰富的应用场景。
5.1 树的基本概念
5.1.1 树的定义
树是一种由节点(Node)组成的数据结构,具有以下特点:
- 有且仅有一个特定的称为根(Root)的节点。
- 除根节点外,其余节点可分为若干个互不相交的子树(Subtree)。
- 每个节点最多有一个前驱节点(即父节点),可以有零个或多个后继节点(即子节点)。
5.1.2 树的术语
- 节点的度(Degree):节点拥有的子节点数目。
- 树的度:树中所有节点的度的最大值。
- 叶子节点(Leaf):度为0的节点。
- 内部节点(Internal Node):至少有一个子节点的节点。
- 路径长度:从根节点到某一节点的路径上节点数目。
- 树的高度(Height):树中最长路径的长度。
- 树的深度:节点的深度是从根节点到该节点所经历的边的数目。
5.2 二叉树
二叉树是树形结构中的一种特殊形式,每个节点最多有两个子节点,通常称为左子节点和右子节点。
5.2.1 二叉树的定义和特点
- 定义:二叉树的每个节点最多有两个子节点,分别称为左子节点和右子节点。
- 特点:
- 左子节点的值总是小于或等于父节点的值。
- 右子节点的值总是大于或等于父节点的值。
5.2.2 二叉树的遍历
二叉树的遍历是指按照某种顺序访问树中的所有节点。常见的遍历方式包括:
- 前序遍历(Pre-order):先访问根节点,然后递归地进行前序遍历左子树,最后递归地进行前序遍历右子树。
- 中序遍历(In-order):先递归地进行中序遍历左子树,然后访问根节点,最后递归地进行中序遍历右子树。
- 后序遍历(Post-order):先递归地进行后序遍历左子树,然后递归地进行后序遍历右子树,最后访问根节点。
- 层序遍历(Level-order):按照从上到下、从左到右的顺序逐层访问节点。
示例代码(C++ 实现二叉树的前序遍历):
#include <iostream>
#include <vector>
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
void preOrderTraversal(TreeNode* root) {
if (root == nullptr) return;
std::cout << root->val << " ";
preOrderTraversal(root->left);
preOrderTraversal(root->right);
}
int main() {
TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->left->right = new TreeNode(5);
std::cout << "Pre-order traversal: ";
preOrderTraversal(root);
return 0;
}
5.2.3 二叉搜索树
二叉搜索树(Binary Search Tree, BST)是一种特殊的二叉树,其中每个节点都大于或等于其左子树上的所有节点,并且小于或等于其右子树上的所有节点。
示例代码(C++ 实现二叉搜索树的插入):
#include <iostream>
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
TreeNode* insertBST(TreeNode* root, int val) {
if (root == nullptr) {
return new TreeNode(val);
}
if (val < root->val) {
root->left = insertBST(root->left, val);
} else if (val > root->val) {
root->right = insertBST(root->right, val);
}
return root;
}
int main() {
TreeNode* root = nullptr;
root = insertBST(root, 4);
insertBST(root, 2);
insertBST(root, 6);
insertBST(root, 1);
insertBST(root, 3);
insertBST(root, 5);
// 构建的二叉搜索树为:
// 4
// / \
// 2 6
// / \ / \
// 1 3 5
return 0;
}
5.3 平衡树
平衡树是一种特殊的二叉搜索树,其中任何节点的两个子树的高度差不超过1,以确保树的高度最小化,从而提高搜索、插入和删除操作的效率。
5.3.1 平衡树的定义
平衡树需要在每次插入或删除操作后进行调整,以保持树的平衡。
5.3.2 AVL树
AVL树(Adelson-Velsky and Landis Tree)是一种自平衡二叉搜索树,其中任何节点的两个子树的高度差不超过1。
5.3.3 红黑树
红黑树是一种自平衡二叉搜索树,每个节点都有一个颜色属性(红或黑),并且通过一系列的旋转和重新着色操作来保持树的平衡。
在本章中,我们详细介绍了树形结构,包括树的基本概念、二叉树的定义和特点、二叉树的遍历、二叉搜索树以及平衡树。在下一章中,我们将探讨图形结构,包括图的基本概念、图的存储结构和图的遍历。
6. 图形结构
图形结构是表示对象之间关系的一种数据结构,它由顶点(节点)和边组成。图在许多领域都有应用,如网络、路径规划、社交网络分析等。
6.1 图的基本概念
6.1.1 图的定义
图(Graph)是由顶点(Vertex)集合和边(Edge)集合组成的一种数据结构,其中边表示顶点之间的关系。
6.1.2 图的术语
- 顶点(Vertex):图中的节点,表示对象。
- 边(Edge):连接两个顶点的线,表示对象之间的关系。
- 有向图(Digraph):边有方向的图。
- 无向图(Undirected Graph):边没有方向的图。
- 邻接(Adjacent):两个顶点之间存在边。
- 度(Degree):顶点的邻接边数。在有向图中,分为入度(指向该顶点的边数)和出度(从该顶点出发的边数)。
- 路径(Path):图中的一系列顶点,其中每对相邻顶点间都有一条边。
- 环(Cycle):起点和终点相同的路径。
6.2 图的存储结构
图的存储结构主要有两种:邻接矩阵和邻接表。
6.2.1 邻接矩阵
邻接矩阵是一个二维数组,用于表示图中顶点之间的邻接关系。矩阵的行和列对应图中的顶点,如果两个顶点之间有边,则矩阵中相应的元素为1,否则为0。
示例代码(C++ 使用邻接矩阵表示图):
#include <iostream>
#include <vector>
using namespace std;
const int INF = 0x3f3f3f3f; // 用INF代表两个点之间没有边
void addEdge(vector<vector<int>>& graph, int from, int to) {
graph[from][to] = 1; // 有向图
// graph[to][from] = 1; // 无向图
}
void printGraph(const vector<vector<int>>& graph) {
int n = graph.size();
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cout << graph[i][j] << " ";
}
cout << endl;
}
}
int main() {
int n = 4;
vector<vector<int>> graph(n, vector<int>(n, INF));
addEdge(graph, 0, 1);
addEdge(graph, 0, 2);
addEdge(graph, 1, 2);
addEdge(graph, 2, 0);
addEdge(graph, 2, 3);
printGraph(graph);
return 0;
}
6.2.2 邻接表
邻接表是图的一种链式存储结构,它为每个顶点维护一个链表,链表中的每个节点代表与该顶点相邻的一个顶点。
示例代码(C++ 使用邻接表表示图):
#include <iostream>
#include <vector>
#include <list>
using namespace std;
class Graph {
int V; // 顶点数
vector<list<int>> adj; // 邻接表
public:
Graph(int V) : V(V), adj(V) {}
void addEdge(int v, int w) {
adj[v].push_back(w); // 添加边v->w
// adj[w].push_back(v); // 如果是无向图,还需要添加边w->v
}
void printGraph() {
for (int v = 0; v < V; ++v) {
cout << "顶点 " << v << " -> ";
for (auto i = adj[v].begin(); i != adj[v].end(); ++i)
cout << *i << " ";
cout << endl;
}
}
};
int main() {
Graph g(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.printGraph();
return 0;
}
6.3 图的遍历
图的遍历是指按照某种顺序访问图中的所有顶点。常见的图遍历算法有深度优先搜索(DFS)和广度优先搜索(BFS)。
6.3.1 深度优先搜索(DFS)
深度优先搜索是一种递归算法,它从图中的某个顶点开始,沿着一条路径一直走到不能再走为止,然后回溯并沿着另一条路径继续搜索。
示例代码(C++ 实现DFS):
#include <iostream>
#include <vector>
#include <list>
using namespace std;
class Graph {
int V; // 顶点数
vector<list<int>> adj; // 邻接表
void DFSUtil(int v, vector<bool>& visited); // 用于递归的辅助函数
public:
Graph(int V); // 构造函数
void addEdge(int v, int w); // 添加边
void DFS(int v); // 从顶点v开始进行DFS
};
Graph::Graph(int V) {
this->V = V;
adj.resize(V);
}
void Graph::addEdge(int v, int w) {
adj[v].push_back(w); // 添加边v->w
// adj[w].push_back(v); // 如果是无向图,还需要添加边w->v
}
void Graph::DFSUtil(int v, vector<bool>& visited) {
visited[v] = true;
cout << v << " ";
for (auto i = adj[v].begin(); i != adj[v].end(); ++i)
if (!visited[*i])
DFSUtil(*i, visited);
}
void Graph::DFS(int v) {
vector<bool> visited(V, false);
DFSUtil(v, visited);
}
int main() {
Graph g(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
cout << "深度优先搜索(从顶点0开始): ";
g.DFS(0);
return 0;
}
6.3.2 广度优先搜索(BFS)
广度优先搜索是一种迭代算法,它从图中的某个顶点开始,首先访问所有相邻的顶点,然后依次访问这些相邻顶点的相邻顶点。
示例代码(C++ 实现BFS):
#include <iostream>
#include <vector>
#include <list>
#include <queue>
using namespace std;
class Graph {
int V; // 顶点数
vector<list<int>> adj; // 邻接表
public:
Graph(int V); // 构造函数
void addEdge(int v, int w); // 添加边
void BFS(int s); // 从顶点s开始进行BFS
};
Graph::Graph(int V) {
this->V = V;
adj.resize(V);
}
void Graph::addEdge(int v, int w) {
adj[v].push_back(w); // 添加边v->w
// adj[w].push_back(v); // 如果是无向图,还需要添加边w->v
}
void Graph::BFS(int s) {
vector<bool> visited(V, false);
queue<int> queue;
visited[s] = true;
queue.push(s);
while (!queue.empty()) {
s = queue.front();
cout << s << " ";
queue.pop();
for (auto i = adj[s].begin(); i != adj[s].end(); ++i)
if (!visited[*i]) {
visited[*i] = true;
queue.push(*i);
}
}
}
int main() {
Graph g(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
cout << "广度优先搜索(从顶点0开始): ";
g.BFS(0);
return 0;
}
在本章中,我们详细介绍了图的基本概念、图的存储结构和图的遍历。图是一种强大的数据结构,可以表示复杂的关系和网络。在下一章中,我们将探讨查找和排序算法。
7. 查找和排序
查找和排序是数据结构中两个基本且重要的操作,它们在软件开发中有着广泛的应用。
7.1 查找
查找操作是指在数据结构中找到特定元素的过程。不同的数据结构和查找需求会导致不同的查找算法。
7.1.1 顺序查找
顺序查找(Sequential Search)是最简单直观的查找方法,它逐个检查数据结构中的每个元素,直到找到目标元素或搜索完整个数据结构。
示例代码(C+++ 实现顺序查找):
#include <iostream>
#include <vector>
bool sequentialSearch(const std::vector<int>& data, int target) {
for (int num : data) {
if (num == target) {
return true;
}
}
return false;
}
int main() {
std::vector<int> data = {2, 4, 6, 8, 10};
int target = 6;
if (sequentialSearch(data, target)) {
std::cout << "Found " << target << std::endl;
} else {
std::cout << target << " not found" << std::endl;
}
return 0;
}
7.1.2 二分查找
二分查找(Binary Search)是一种在有序数组中查找特定元素的高效算法。它通过不断地将待搜索的区间分成两半来缩小搜索范围。
示例代码(C+++ 实现二分查找):
#include <iostream>
#include <vector>
#include <algorithm>
int binarySearch(const std::vector<int>& data, int target) {
int left = 0;
int right = data.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (data[mid] == target) {
return mid;
} else if (data[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
int main() {
std::vector<int> data = {2, 4, 6, 8, 10};
int target = 6;
int index = binarySearch(data, target);
if (index != -1) {
std::cout << "Found " << target << " at index " << index << std::endl;
} else {
std::cout << target << " not found" << std::endl;
}
return 0;
}
7.1.3 哈希查找
哈希查找(Hash Search)利用哈希表进行快速查找。哈希表通过哈希函数将元素映射到表中的位置,从而实现平均时间复杂度为O(1)的查找。
示例代码(C+++ 使用 unordered_map 实现哈希查找):
#include <iostream>
#include <unordered_map>
bool hashSearch(const std::unordered_map<int, std::string>& hashTable, int key) {
auto it = hashTable.find(key);
if (it != hashTable.end()) {
std::cout << "Found: " << it->second << std::endl;
return true;
}
std::cout << key << " not found" << std::endl;
return false;
}
int main() {
std::unordered_map<int, std::string> hashTable = {{1, "one"}, {2, "two"}, {3, "three"}};
int key = 2;
hashSearch(hashTable, key);
return 0;
}
7.2 排序
排序是将数据结构中的元素按照一定的顺序重新排列的过程。排序算法在数据处理中非常重要。
7.2.1 冒泡排序
冒泡排序(Bubble Sort)是一种简单的排序算法,它重复地遍历要排序的数列,比较每对相邻元素,如果它们的顺序错误就把它们交换过来。
示例代码(C+++ 实现冒泡排序):
#include <iostream>
#include <vector>
#include <algorithm>
void bubbleSort(std::vector<int>& data) {
bool swapped;
do {
swapped = false;
for (size_t i = 1; i < data.size(); ++i) {
if (data[i - 1] > data[i]) {
std::swap(data[i - 1], data[i]);
swapped = true;
}
}
} while (swapped);
}
int main() {
std::vector<int> data = {64, 34, 25, 12, 22, 11, 90};
bubbleSort(data);
std::cout << "Sorted array: ";
for (int num : data) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
7.2.2 选择排序
选择排序(Selection Sort)是一种简单直观的排序算法,它的工作原理是首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置。
示例代码(C+++ 实现选择排序):
#include <iostream>
#include <vector>
void selectionSort(std::vector<int>& data) {
for (size_t i = 0; i < data.size() - 1; ++i) {
int min_idx = i;
for (size_t j = i + 1; j < data.size(); ++j) {
if (data[j] < data[min_idx]) {
min_idx = j;
}
}
std::swap(data[min_idx], data[i]);
}
}
int main() {
std::vector<int> data = {64, 25, 12, 22, 11};
selectionSort(data);
std::cout << "Sorted array: ";
for (int num : data) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
7.2.3 插入排序
插入排序(Insertion Sort)是一种简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
示例代码(C+++ 实现插入排序):
#include <iostream>
#include <vector>
void insertionSort(std::vector<int>& data) {
for (size_t i = 1; i < data.size(); ++i) {
int key = data[i];
int j = i - 1;
while (j >= 0 && data[j] > key) {
data[j + 1] = data[j];
--j;
}
data[j + 1] = key;
}
}
int main() {
std::vector<int> data = {9, 5, 1, 4, 3};
insertionSort(data);
std::cout << "Sorted array: ";
for (int num : data) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
7.2.4 快速排序
快速排序(Quick Sort)是一种高效的排序算法,采用分治策略来把一个序列分为较小的两个子序列。
示例代码(C++ 实现快速排序):
#include <iostream>
#include <vector>
void quickSort(std::vector<int>& data, int low, int high) {
if (low < high) {
int pi = partition(data, low, high);
quickSort(data, low, pi - 1);
quickSort(data, pi + 1, high);
}
}
int partition(std::vector<int>& data, int low, int high) {
int pivot = data[high];
int i = (low - 1);
for (int j = low; j <= high - 1; j++) {
if (data[j] < pivot) {
i++;
std::swap(data[i], data[j]);
}
}
std::swap(data[i + 1], data[high]);
return (i + 1);
}
int main() {
std::vector<int> data = {10, 7, 8, 9, 1, 5};
int n = data.size();
quickSort(data, 0, n - 1);
std::cout << "Sorted array: ";
for (int num : data) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
7.2.5 归并排序
归并排序(Merge Sort)是建立在归并操作上的一种有效的排序算法,采用分治法的一个应用。
示例代码(C++ 实现归并排序):
#include <iostream>
#include <vector>
void merge(std::vector<int>& data, int left, int mid, int right) {
std::vector<int> temp(right - left + 1);
int i = left;
int j = mid + 1;
int k = 0;
while (i <= mid && j <= right) {
if (data[i] <= data[j]) {
temp[k++] = data[i++];
} else {
temp[k++] = data[j++];
}
}
while (i <= mid) {
temp[k++] = data[i++];
}
while (j <= right) {
temp[k++] = data[j++];
}
for (i = left, k = 0; i <= right; ++i, ++k) {
data[i] = temp[k];
}
}
void mergeSort(std::vector<int>& data, int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(data, left, mid);
mergeSort(data, mid + 1, right);
merge(data, left, mid, right);
}
}
int main() {
std::vector<int> data = {12, 11, 13, 5, 6, 7};
mergeSort(data, 0, data.size() - 1);
std::cout << "Sorted array: ";
for (int num : data) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
在本章中,我们详细介绍了查找和排序算法,包括顺序查找、二分查找、哈希查找以及冒泡排序、选择排序、插入排序、快速排序和归并排序。这些算法在软件开发中有着广泛的应用,掌握它们对于提高程序性能和解决实际问题非常重要。在下一章中,我们将探讨算法分析。
8. 算法分析
算法分析是评估算法性能的重要手段,它帮助我们理解算法在不同情况下的表现,从而选择最合适的算法解决问题。
8.1 算法复杂度
算法复杂度是衡量算法执行时间或空间需求与输入规模之间关系的量度。它通常用大O符号表示,描述算法在最坏情况下的性能。
8.1.1 时间复杂度
时间复杂度描述了算法执行时间随输入规模增长的变化趋势。常见的时间复杂度包括:
- 常数时间:(O(1))
- 线性时间:(O(n))
- 对数时间:(O(\log n))
- 线性对数时间:(O(n \log n))
- 平方时间:(O(n^2))
- 指数时间:(O(2^n))
8.1.2 空间复杂度
空间复杂度描述了算法执行过程中所需存储空间随输入规模增长的变化趋势。它同样使用大O符号表示。
8.2 算法设计技巧
算法设计技巧是构建高效算法的方法论,它们可以帮助我们设计出更优秀的算法。
8.2.1 递归
递归是一种通过函数自我调用解决问题的方法。它适用于可以分解为相似子问题的问题。
示例代码(C++ 实现递归计算阶乘):
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
8.2.2 分治
分治策略是将问题分解为更小的子问题,递归地解决子问题,然后将结果合并以得到原问题的解。
示例代码(C++ 实现分治算法计算数组元素和):
int getSum(int arr[], int l, int r) {
if (l == r)
return arr[l];
int mid = (l + r) / 2;
return getSum(arr, l, mid) + getSum(arr, mid + 1, r);
}
8.2.3 动态规划
动态规划是一种通过将问题分解为重叠子问题并存储子问题的解来避免重复计算的方法。
示例代码(C++ 实现动态规划计算斐波那契数列):
int fibonacci(int n) {
if (n <= 1) return n;
int dp[n + 1];
dp[0] = 0, dp[1] = 1;
for (int i = 2; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2];
return dp[n];
}
8.2.4 贪心算法
贪心算法在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。
示例代码(C++ 实现贪心算法解决活动选择问题):
#include <iostream>
#include <vector>
#include <algorithm>
struct Activity {
int start, end, index;
};
bool compareActivity(Activity a, Activity b) {
return a.end < b.end;
}
void printMaxActivities(std::vector<Activity>& activities) {
std::sort(activities.begin(), activities.end(), compareActivity);
int i = 0;
std::cout << "Selected activities are \n";
for (int j = 1; j < activities.size(); j++) {
if (activities[j].start >= activities[i].end) {
std::cout << "(" << activities[j].start << ", "
<< activities[j].end << ") ";
i = j;
}
}
}
int main() {
std::vector<Activity> activities = {{1, 2, 0}, {3, 4, 1}, {0, 6, 2}, {5, 7, 3}, {8, 9, 4}};
int n = activities.size();
printMaxActivities(activities);
return 0;
}
在本章中,我们详细介绍了算法分析的基本概念和几种常见的算法设计技巧。算法分析帮助我们评估算法的性能,而算法设计技巧则指导我们设计出更高效的算法。在下一章中,我们将探讨数据结构的应用。