目录
一.堆
堆(Heap)是一种特殊的完全二叉树,广泛应用于计算机科学中,特别是在数据处理和算法设计中。它主要用于实现优先队列,以及在排序算法(如堆排序)中起到关键作用。了解堆的概念之前,先简答介绍一下二叉树。
1.二叉树
1.二叉树
想象一下你在公园里看到一棵树,它从地面生长出来,分叉出很多枝条。二叉树的概念与此类似,但有一个关键的区别:每个分叉点(我们称之为"节点")最多只能有两个分支。
二叉树的几个关键点:
节点(Node):树中的每个元素称为节点。每个节点可以包含一些数据(比如数字、字符或任何类型的数据)。
根节点(Root Node):树的最顶端的节点,是所有分支的起源点。
叶节点(Leaf Nodes):没有子节点的节点称为叶节点,它们是树的末端。
父节点和子节点(Parent and Child Nodes):每个节点(除了根节点)都有一个父节点,且可以有最多两个子节点。
二叉树可以是有序的,也可以是无序的,这取决于树的具体应用。
2.完全二叉树
现在,让我们把一个二叉树想象成一个家族树。如果这个家族的每一代人都结婚并且恰好有两个孩子(不多也不少),那么这个家族树就是一个完全二叉树。
完全二叉树的特点:
1.所有的层(除了可能最后一层)都是完全填满的,也就是说,它们拥有最大数量的节点。
2.如果最后一层不满,那么所有的节点都会尽可能地向左边靠拢。
完全二叉树是一种特殊的二叉树,它保证了树的填充是最优化的,从而使得树的高度尽可能地低,这有助于提高在树上进行搜索、插入和删除操作的效率。
2.大根堆和小根堆
大根堆(也称为最大堆)和小根堆(也称为最小堆)是堆结构的两种形式,它们在数据的组织和管理中扮演着重要的角色,特别是在优先队列和排序算法中。
1.大根堆
大根堆是一种特殊的完全二叉树,满足以下性质:
1.树中任意节点的值都大于或等于其子节点的值。
2.这意味着树的根节点包含了整个树中的最大值。
在大根堆中进行操作时,我们总是确保树的这一性质不被破坏。比如:
插入操作:当我们向大根堆中插入一个新的元素时,我们首先把它放在树的最底层,保持树的完全二叉树形态。然后,如果这个新元素大于它的父节点,我们就将它与父节点交换(这个过程叫做上浮),直到恢复大根堆的性质。
删除最大元素:由于最大元素总是位于根节点,删除它非常直接。我们将根节点与最后一个元素交换,然后删除最后一个元素(原根节点)。此时,新的根节点可能违反大根堆的性质,因此我们进行下沉操作,即将它与它的最大子节点交换,直到恢复大根堆的性质。
2.小根堆
小根堆也是一种特殊的完全二叉树,但它满足的性质与大根堆相反:
树中任意节点的值都小于或等于其子节点的值。
这意味着树的根节点包含了整个树中的最小值。
小根堆的操作逻辑与大根堆相似,但方向相反:
插入操作:向小根堆中插入新元素后,如果这个新元素小于它的父节点,我们将它与父节点交换,直到恢复小根堆的性质。
删除最小元素:删除最小元素(位于根节点)后,将根节点与最后一个元素交换,然后进行下沉操作,即如果新的根节点大于它的子节点,就与最小的子节点交换,直到恢复小根堆的性质。
3.数组排成大根堆
问题:给定一个数组,实现排序成大根堆(heapinsert)
解决思路:
heapfy函数:这个函数的目的是确保以
i
为根的子树满足大根堆的性质。它首先找到i及其两个子节点中的最大值。如果最大值不是i
,则将其与i
的值交换,并递归地调用heapfy
以确保交换后的子树也满足大根堆的性质。buildheap函数:这个函数用于将一个未排序的数组转换成一个大根堆。它从最后一个非叶子节点开始,逆序遍历所有的非叶子节点,对每个节点调用
heapfy
函数。main函数:在这里创建了一个测试用的
vector<int>
数组,并调用buildheap
函数将其转换为大根堆,然后打印出来。
代码示例:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
void heapfy(vector<int>& arr, int n,int i ) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left<n && arr[left]>arr[largest]) largest = left;
if (right<n && arr[right]>arr[largest]) largest = right;
if (largest != i) {
swap(arr[i], arr[largest]);
heapfy(arr, n, largest);
}
}
void buildheap(vector<int>& arr) {
int n = arr.size();
//int i = n / 2 - 1;
for (int i = n / 2 - 1; i >= 0; i--) {
heapfy(arr, n, i);
}
}
int main() {
vector<int> arr = { 2,8,6,3,7,5,4,1,9 };
int len = arr.size();
buildheap(arr);
cout << "sorted array:" << endl;
for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
system("pause");
return 0;
}
3.堆排序
排序思路:
1.给定数组,排成大根堆。heapsize
2.将最大值(根节点值)与最后的值交换,heapsize--。
3.将剩下的数组重新排成大根堆
重复步骤2,3
代码示例:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
void heapfy(vector<int>& arr, int n,int i ) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;
if (left<n && arr[left]>arr[largest]) largest = left;
if (right<n && arr[right]>arr[largest]) largest = right;
if (largest != i) {
swap(arr[i], arr[largest]);
heapfy(arr, n, largest);
}
}
void buildheap1(vector<int>& arr) {
int n = arr.size();
//int i = n / 2 - 1;
for (int i = n / 2 - 1; i >= 0; i--) {
heapfy(arr, n, i);
}
for (int i = n - 1; i >= 0; i--) {
swap(arr[i], arr[0]);
heapfy(arr, i, 0);
}
}
int main() {
vector<int> arr = { 2,8,6,3,7,5,4,1,9 };
int len = arr.size();
buildheap1(arr);
cout << "sorted array:" << endl;
for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
system("pause");
return 0;
}
4.堆结构
优先级队列结构==堆结构
堆结构的优先队列是数据结构和算法中的一个核心概念,它允许以任意顺序插入元素,但总是按照一定的优先级规则(如最大值或最小值优先)来移除元素。这种数据结构在很多场景中都非常有用,比如任务调度、事件管理、以及算法中的各种优化问题。
1.优先队列的基本概念
优先队列是一种特殊的队列,其中的每个元素都有一个优先级关联。当访问或移除元素时,具有最高优先级的元素首先被访问或移除,而不是像在标准队列中那样遵循先进先出(FIFO)的原则。优先级的具体定义可以根据实际需求来设定,例如可以是整数、字符或其他自定义类型,用于表示元素的重要性或紧急程度。
2.堆结构在优先队列中的应用
堆(特别是二叉堆)是实现优先队列的一种非常高效的方式,因为它可以在对数时间内完成插入和移除最优先元素的操作。在大根堆中,最高优先级的元素是最大元素;在小根堆中,最高优先级的元素是最小元素。
插入操作:当一个新元素被添加到优先队列中时,它被放置在堆的末尾(即完全二叉树的最后一个位置),然后执行上浮操作,将它移动到正确的位置以维护堆的性质。
移除操作:移除最优先的元素通常涉及到移除堆的根节点(最大元素或最小元素),然后将堆的最后一个元素移动到根节点的位置,并执行下沉操作,以维护堆的性质。
3.优先队列的优点
高效性:利用堆结构,优先队列可以在
O(log n)
时间内完成插入和移除最优先元素的操作,其中n
是队列中的元素数量。这比其他一些基于数组或链表的实现要高效得多,特别是在元素数量很大时。动态管理:优先队列不需要预先排序元素,它可以动态地添加元素并保持优先级的顺序,这对于那些实时数据输入和需要即时处理的场景非常重要。
灵活性:优先队列可以根据具体应用场景灵活定义优先级规则,这使得它可以被广泛应用于各种不同的问题解决方案中。
4.应用场景
优先队列的应用非常广泛,包括:
任务调度系统:在操作系统或应用程序中调度具有不同优先级的任务。
图算法:如Dijkstra和Prim算法,使用优先队列来选择下一个要处理的最小边或节点。
事件模拟:在事件驱动的模拟中管理和处理不同优先级的事件。
数据流管理:处理来自不同来源的具有优先级的数据流。
二.比较器
1.比较器的实质就是重载运算符
2.比较器可以很好的应用在特殊标准的排序上
3.比较器可以很好的应用在根据特殊标准排序的结构上
在编程中,比较器(Comparator)是一种用于比较两个对象以确定它们的排序顺序的函数或对象。比较器广泛应用于排序算法和数据结构中,允许程序员定义自定义的排序顺序。在C++中,比较器通常以函数或函数对象(也称为仿函数)的形式实现,这些函数或对象可以传递给排序函数(如std::sort
)或数据结构(如std::set
或std::priority_queue
)。
1.使用函数作为比较器
可以定义一个比较函数,并将其作为参数传递给排序函数。这个比较函数需要接受两个参数(要比较的对象)并返回一个布尔值,指示第一个参数是否应该排在第二个参数之前。
#include <algorithm>
#include <vector>
#include <iostream>
// 比较函数
bool compare(int a, int b) {
return a < b; // 升序排序
}
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9};
std::sort(numbers.begin(), numbers.end(), compare);
for (int number : numbers) {
std::cout << number << " ";
}
return 0;
}
2.使用函数对象作为比较器
函数对象(也称为仿函数)是一个行为类似函数的对象。通过定义一个含有operator()
的类,可以创建一个可以像函数那样被调用的对象。这允许在比较器中保持状态或使用类的其他功能。
#include <algorithm>
#include <vector>
#include <iostream>
// 比较函数对象
struct Compare {
bool operator()(int a, int b) {
return a < b; // 升序排序
}
};
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9};
std::sort(numbers.begin(), numbers.end(), Compare());
for (int number : numbers) {
std::cout << number << " ";
}
return 0;
}
3.在数据结构中使用比较器
还可以使用比较器来定义数据结构中元素的排序规则,比如在std::set
或std::priority_queue
中。这允许创建按特定顺序排序的集合或队列。
#include <set>
#include <iostream>
// 使用函数对象作为比较器
struct Compare {
bool operator()(int a, int b) const {
return a > b; // 逆序
}
};
int main() {
std::set<int, Compare> numbers = {3, 1, 4, 1, 5, 9};
for (int number : numbers) {
std::cout << number << " ";
}
return 0;
}
4.比较器的应用
使用C++来定义一个较为复杂的比较器,用于排序一个包含自定义对象的vector
。假设我们有一个Student
类,包含姓名、分数和年龄三个属性。我们的目标是根据多个条件对学生进行排序:首先按照分数降序排序,如果分数相同,则按照年龄升序排序,如果年龄也相同,则按照姓名的字典序升序排序。
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Student {
public:
string name;
int score;
int age;
Student(string name, int score, int age) : name(name), score(score), age(age) {}
};
// 复杂的比较器
struct ComplexComparator {
bool operator()(const Student& a, const Student& b) const {
// 首先按分数降序排序
if (a.score != b.score) return a.score > b.score;
// 如果分数相同,则按年龄升序排序
if (a.age != b.age) return a.age < b.age;
// 如果年龄也相同,则按姓名的字典序升序排序
return a.name < b.name;
}
};
int main() {
std::vector<Student> students = {
{"Alice", 90, 20},
{"Bob", 75, 22},
{"Charlie", 90, 21},
{"Dave", 75, 20},
{"Eve", 90, 20}
};
sort(students.begin(), students.end(), ComplexComparator());
cout << "Sorted students:\n";
for (const auto& student : students) {
std::cout << "Name: " << student.name
<< ", Score: " << student.score
<< ", Age: " << student.age << "\n";
}
return 0;
}
这个例子展示了使用比较器的优点之一:能够基于多个条件进行排序,而且通过定义比较器,排序逻辑集中于一个地方,使得代码易于理解和维护。这种方法在处理复杂对象或需要根据多个属性进行排序的情况下尤其有用。
三.桶排序
桶排序(Bucket Sort)的原理基于将数组分割成多个区间(即“桶”),然后分别对每个桶进行排序,最后将所有桶中的元素按顺序合并。桶排序特别适用于当输入数据呈均匀分布时,因为它可以在这种情况下提供接近线性的时间复杂度,即O(n)
,其中n
是数组中元素的数量。
1.桶排序的核心步骤
1.创建桶:根据输入数据的特征(如范围和分布),创建一定数量的桶。每个桶代表一个值范围。
2.分配数据到桶中:遍历原始数组,根据元素的值将每个元素分配到对应的桶中。分配的规则取决于元素的值与桶值范围的关系。
3.对每个桶内部进行排序:单独对每个桶内的元素进行排序。可以使用任何排序算法,如快速排序、插入排序等。
4.合并桶:按顺序收集每个桶中的元素,合并成一个有序的数组。
2.桶排序的原理解析
分而治之:桶排序利用了分而治之的策略,通过将大问题(排序大数组)分解成小问题(排序小数组即桶内排序),然后合并结果。
数据的均匀分布:桶排序假设数据是均匀分布在整个范围内的。当数据均匀分布时,每个桶中的元素数量大致相同,这样每个小问题的规模都相对较小,可以快速解决。
时间复杂度:在理想情况下(即数据均匀分布,桶的数量适中),桶排序的时间复杂度可以达到
O(n)
。这是因为数据被均匀分配到每个桶中,每个桶内部的排序时间复杂度是O(m log m)
,其中m
是每个桶中元素的平均数量。由于所有桶中元素的总和是n
,并且当桶的数量适当时,m
将接近常数,使得总的排序时间主要由分配数据到桶中的O(n)
步骤决定。空间复杂度:桶排序需要额外的空间来创建桶,其空间复杂度为
O(n+k)
,其中k
是桶的数量。
3.桶排序代码及详解
1.示例代码
#include <iostream>
#include <vector>
#include <algorithm> // 包含 std::sort
using namespace std;
class BucketSort {
public:
// 主要的桶排序函数
void bucketSort(vector<int>& nums) {
if (nums.empty()) return; // 如果数组为空,直接返回
// 查找数组中的最大值和最小值
int minVal = *min_element(nums.begin(), nums.end());
int maxVal = *max_element(nums.begin(), nums.end());
// 计算桶的大小和数量,确保每个桶的大小至少为1
int bucketSize = max(1, (maxVal - minVal) / static_cast<int>(nums.size())) + 1;
int bucketCount = (maxVal - minVal) / bucketSize + 1;
vector<vector<int>> buckets(bucketCount);
// 将每个元素放入相应的桶中
for (int num : nums) {
int bucketIdx = (num - minVal) / bucketSize;
buckets[bucketIdx].push_back(num);
}
// 对每个桶进行排序,并收集排序后的元素
int index = 0;
for (auto& bucket : buckets) {
sort(bucket.begin(), bucket.end()); // 对桶内元素进行排序
for (int num : bucket) {
nums[index++] = num; // 将排序后的元素收集到原数组中
}
}
}
};
int main() {
vector<int> nums = { 19, 27, 35, 43, 31, 22, 54, 66, 78 };
BucketSort sorter;
sorter.bucketSort(nums); // 使用桶排序算法
// 打印排序后的数组
for (int num : nums) {
cout << num << " ";
}
cout << endl;
return 0;
}
2.代码详解
1. 确定桶的数量和大小
(1)最大值(maxVal
)和最小值(minVal
):首先,通过遍历数组找到最大值和最小值。这两个值用于确定所有元素的范围,从而帮助计算桶的数量和大小。
(2)桶的大小(bucketSize
):
计算公式为
这个公式确保了每个桶至少能够容纳一个元素的范围(即当所有元素相同时,桶的大小至少为1),同时+1是为了确保最大值也能被包含在某个桶内。
(3)桶的数量(bucketCount
):
计算公式为
这个公式基于元素范围和桶的大小计算需要多少个桶来覆盖整个范围,+1同样是为了确保覆盖最大值。
2. 将元素分配到桶中
(1)桶索引(bucketIdx
):对于数组中的每个元素,计算其应该放入哪个桶的索引,公式为
这个公式根据元素的值相对于整个范围的位置来确定其应该分配到的桶。通过这种方式,可以保证所有元素都根据其值被均匀地分配到各个桶中。
3. 对每个桶内部进行排序
每个桶内部使用std::sort
进行排序。由于每个桶中的元素数量相对较少,这一步骤通常很快。
注意:这里的排序算法可以自行选择,用于提高代码效率
4. 合并桶
最后,遍历每个桶,并按顺序将桶中的元素重新放回原始数组中。由于之前已经对每个桶内部进行了排序,所以合并后的数组是有序的。
5. 总结
这种方法特别适用于数据分布相对均匀的情况,因为这可以保证每个桶中的元素数量大致相同,从而使得整体排序效率较高。此外,由于桶排序在各个桶之间是独立的,因此它很适合并行处理,进一步提高排序效率。