1、线性枚举
线性枚举指的就是遍历某个一维数组(顺序表)的所有元素,找到满足条件的那个元素并且返回,返回值可以是下标,也可以是元素本身。时间复杂度为O(nm),其中n是线性表的长度。
求一个线性表中的最大值,可以先设定一个最大值,把它初始化为一个非常小的数,然后遍历给定的线性表,将其中的每个元素和目前的最大值比较,如果比它大,则更新这个最大值;如果比它小,就不做任何处理。遍历完毕返回最大值就是我们要求的解了。写成伪代码如下:
function getMax(n,a)
{
max = a
for i->(0,n-1)
if a[i] > max
max = a[i]
return max
}
2、二分枚举/二分查找
二分枚举,也叫二分查找,指的就是给定一个区间,每次选择区间的中点,并且判断区间中点是否满足某个条件,从而选择左区间继续求解还是右区间继续求解,直到区间长度不能再切分为止。时间复杂度为 。
// 条件判定
int isGreen(int val) {
return val == 1;
}
// 二分枚举模板
int binarySearch(int *arr, int arrSize, int x) {
int l = -1, r = arrSize; // (1)
int mid;
while(r - l > 1) { // (2)
mid = l + (r - l) / 2; // (3)
if( isGreen(arr[mid], x) ) // (4)
r = mid; // (5)
else
l = mid; // (6)
}
return r; // (7)
}
3、选择排序
选择排序(SelectionSort)是一种简单直观的排序算法。它首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。时间复杂度为。
void selectionSort(vector<int>& a) {
int n = (int)a.size();
for (int i = 0; i < n; ++i) {
int min = i;
for (int j = i + 1; j < n; ++j) {
if (a[j] < a[min]) {
min = j;
}
}
int tmp = a[min];
a[min] = a[i];
a[i] = tmp;
}
}
4、冒泡排序
冒牌排序(Bubble Sort)是一种简单的排序算法,它通过重复遍历待排序的数列,比较相邻元素并交换它们的顺序,直到没有需要交换的元素为止。其基本原理可以概括为以下几个步骤:
1. 比较相邻元素:从数列的开始位置,比较第一个和第二个元素。如果第一个元素比第二个元素大,就交换它们的位置。
2. 继续比较:然后,继续比较第二个和第三个元素,依此类推,直到最后一个元素。
3. 一轮完成:经过一轮比较后,最大的元素会被“冒泡”到数列的末尾。
4. 重复过程:对剩下的元素重复上述过程,直到整个数列有序。
5. 优化:如果在某一轮中没有发生任何交换,说明数列已经有序,可以提前结束排序过程。
冒牌排序的时间复杂度为 (O(n^2)),其中(n) 是待排序元素的数量。这使得它在处理大规模数据时效率较低,但由于其实现简单,常用于教学和小规模数据的排序。
void bubbleSort(vector<int>& a)
{
int n = (int)a.size();
for (int i = n - 1; i >= 0; --i) // 遍历N个元素
{
for (int j = 0; j < i; ++j) // 从头开始遍历
{
if (a[j] > a[j + 1]) // 如果前值大于后值,交换 即:将最小值放在最前面
{
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
}
}
}
}
5、插入排序
插入排序(Insertion Sort)是一种简单的排序算法,它的基本思想是将待排序的元素逐个插入到已经排好序的部分中,直到所有元素都有序为止。插入排序的工作方式类似于我们在玩扑克牌时整理手牌的过程。
插入排序的基本原理:
-
初始状态:将数组的第一个元素视为已排序的部分,剩余元素为未排序的部分。
-
逐个插入:从未排序部分取出一个元素,将其插入到已排序部分的适当位置。这个过程需要比较当前元素与已排序部分的元素,并将已排序部分的元素后移以腾出位置。
-
重复过程:重复上述步骤,直到所有元素都被插入到已排序部分中。
时间复杂度:
-
最坏情况下时间复杂度为 O(n^2)(当数组是逆序时)。
-
最好情况下时间复杂度为 O(n)(当数组已经有序时)。
void insertionSort(vector<int>& a) {
for (int i = 1; i < a.size(); ++i) {
int x = a[i]; // 取出要比较的值
int j;
for (j = i - 1; j >= 0; --j) { // 依次与前值比较
if (x < a[j]) { // 比比较值大的值后移
a[j + 1] = a[j];
}
else {
break;
}
}
a[j + 1] = x; // 将比较值放入该插入的位置
}
}
6、计数排序
计数排序(Counting Sort)是一种非比较排序算法,适用于范围较小的整数排序。它的基本思想是通过统计每个元素出现的次数,然后根据这些计数来确定每个元素在排序后数组中的位置。计数排序的时间复杂度为 (O(n + k)),其中 (n) 是待排序元素的数量,(k) 是元素值的范围。
计数排序的基本原理:
1. 确定范围:首先找出待排序数组中的最小值和最大值,以确定元素值的范围。
2. 创建计数数组:根据范围创建一个计数数组,用于存储每个元素的出现次数。计数数组的大小为 (k)(最大值 - 最小值 + 1)。
3. 统计次数:遍历待排序数组,将每个元素的出现次数记录到计数数组中。
4. 累加计数:将计数数组中的值进行累加,得到每个元素在排序后数组中的位置。
5. 构建输出数组:根据计数数组的信息,将原数组中的元素放到正确的位置,构建排序后的输出数组。
6. 复制回原数组(可选):如果需要,可以将输出数组中的元素复制回原数组。
计数排序的适用条件:
- 计数排序适合用于范围较小的整数排序(例如,0 到 k 的范围),当 (k) 远小于 (n) 时,计数排序的效率非常高。
- 不适用于浮点数或字符串排序,除非对其进行特定处理。
void countingSort(vector<int>& a, int m) {
int n = (int)a.size();
int* count = new int[m + 1];
memset(count, 0, sizeof(int) * (m + 1));
for (int i = 0; i < n; ++i) { // 数组以数值大小为下标计数
count[a[i]]++;
}
int s = 0;
for (int v = 0; v <= m; ++v) { // 根据顺序列出
while (count[v]) {
a[s++] = v;
--count[v];
}
}
delete[] count;
}
7、归并排序
归并排序(Merge Sort)是一种有效的排序算法,采用分治法(Divide and Conquer)策略。它的基本思想是将待排序的数组分成两个子数组,分别对这两个子数组进行排序,然后将已排序的子数组合并成一个最终的排序数组。归并排序的时间复杂度为 (O(n log n)),在最坏、最好和平均情况下时间复杂度均为 (O(n log n))。
归并排序的基本原理:
1. 分解:将待排序的数组分成两半,递归地对每一半进行归并排序,直到每个子数组的大小为 1(单个元素自然有序)。
2. 合并:将两个已排序的子数组合并成一个排序好的数组。合并的过程需要比较两个子数组的元素,并按顺序将它们放入新的数组中。
归并排序的步骤:
1. 分解:将数组不断地分成两半,直到每个子数组只包含一个元素。
2. 合并:将分解得到的子数组两两合并,形成更大的已排序数组,直到所有元素都合并为一个完整的排序数组。
void merge(vector<int>& a, int l, int m, int r) {
int n1 = m - l + 1; // 左侧个数
int n2 = r - m; // 右侧个数
int* temp = new int[n1 + n2];
for (int i = 0; i < n1; ++i) {
temp[i] = a[l + i];
}
for (int j = 0; j < n2; ++j) {
temp[n1 + j] = a[m + 1 + j];
}
int i = 0, j = n1, k = l;
while (i < n1 && j < n1 + n2) {
if (temp[i] <= temp[j]) {
a[k++] = temp[i++];
}
else {
a[k++] = temp[j++];
}
}
while (i < n1) {
a[k++] = temp[i++];
}
while (j < n1 + n2) {
a[k++] = temp[j++];
}
delete[] temp;
}
void mergeSort(vector<int>& a, int l, int r) {
if (l >= r) {
return;
}
int m = (l + r) / 2;
mergeSort(a, l, m); // 将数据递归调用,对半分
mergeSort(a, m + 1, r);
merge(a, l, m, r); // 对数据进行排序,从最小组到最大组
}
8、快速排序
快速排序(Quicksort)是一种高效的排序算法,采用分治法(Divide and Conquer)策略。它的基本思想是通过一个“基准”(pivot)元素将待排序的数组分成两个子数组,使得左侧子数组的所有元素都小于基准元素,而右侧子数组的所有元素都大于基准元素。然后,递归地对这两个子数组进行快速排序。以下是快速排序的详细原理和步骤:
快速排序的基本步骤
1. 选择基准元素:
- 从待排序数组中选择一个元素作为基准元素。选择基准的方法可以有多种,如选择第一个元素、最后一个元素、随机选择一个元素或选择中间元素等。
2. 分区操作:
- 通过一趟扫描,将数组分成两个部分:
- 所有小于基准元素的元素放在基准元素的左侧。
- 所有大于基准元素的元素放在基准元素的右侧。
- 这个过程称为“分区”(Partition)。
3. 递归排序:
- 递归地对基准元素左侧和右侧的子数组进行快速排序。
4. 终止条件:
- 当子数组的大小为 0 或 1 时,认为该子数组已经有序,递归结束。
快速排序的特点
时间复杂度:
- 最优情况:(O(n log n))(当每次分区都能将数组均匀分割)
- 平均情况:(O(n log n))
- 最坏情况:(O(n^2))(当数组已经有序或者逆序时,选择的基准总是最小或最大元素)
空间复杂度:(O(log n))(递归调用栈的空间)
不稳定性:快速排序是一种不稳定的排序算法,相同元素的相对位置可能会改变。
int Partition(vector<int>& a, int l, int r) {
int idx = l + rand() % (r - l + 1);
swap(a[l], a[idx]);
int i = l, j = r;
int x = a[i];
while (i < j) {
while (i < j && a[j] > x)
j--;
if (i < j)
swap(a[i], a[j]), ++i;
while (i < j && a[i] < x)
i++;
if (i < j)
swap(a[i], a[j]), --j;
}
return i;
}
void QuickSort(vector<int>& a, int l, int r) {
if (l >= r) {
return;
}
int pivox = Partition(a, l, r);
QuickSort(a, l, pivox - 1);
QuickSort(a, pivox + 1, r);
}
9、桶排序
桶排序(Bucket Sort)是一种分布式排序算法,它将元素分到不同的桶中,然后对每个桶中的元素进行排序,最后将所有桶中的元素合并起来。桶排序特别适合于输入数据均匀分布的情况。
桶排序的基本步骤
1. **创建桶**:根据输入数据的范围和数量,创建一定数量的桶。每个桶可以是一个列表或其他数据结构。
2. **分配元素**:遍历输入数组,将每个元素放入相应的桶中。桶的选择通常基于元素的值。
3. **排序桶中的元素**:对每个非空桶中的元素进行排序,可以使用其他排序算法(如快速排序、插入排序等)。
4. **合并桶**:将所有桶中的元素按顺序合并,形成一个排好序的数组。
时间复杂度
- **平均时间复杂度**:O(n + k),其中 n 是输入元素的数量,k 是桶的数量。
- **最坏时间复杂度**:O(n²),当所有元素都落入同一个桶时。
- **空间复杂度**:O(n + k),需要额外的空间来存储桶。
10、基数排序
基数排序(Radix Sort)是一种非比较型整数排序算法,它通过将整数分解为多个数字(或位),并根据这些数字的位进行排序。基数排序的基本思想是将待排序的数字按位分组,从最低位到最高位(或从最高位到最低位)逐步进行排序,通常使用稳定的排序算法(如计数排序)作为子排序。
基数排序的基本步骤
1. **确定最大数字的位数**:找出待排序数组中最大数字的位数,以决定需要进行多少次排序。
2. **按位排序**:从最低位开始,对数组进行排序。对每一位使用稳定的排序算法(如计数排序)进行排序。
3. **重复步骤 2**:对每一位重复排序过程,直到最高位。
时间复杂度
- **时间复杂度**:O(d * (n + k)),其中:
- **d** 是数字的位数(对于十进制数,d 是 log10(max))。
- **n** 是待排序元素的数量。
- **k** 是桶的数量(在计数排序中,通常是数字的范围)。
- **空间复杂度**:O(n + k),需要额外的空间来存储计数数组和输出数组。
const int MAXN = 50005; // 传入的最大个数
const int MAXT = 7; // 数值的位数
const int BASE = 10; // 进制
int PowOfBase[MAXT];
int RadixBucket[BASE][MAXN];
int RadixBucketTop[BASE];
void RadixSort(vector<int>& a) {
int n = (int)a.size();
PowOfBase[0] = 1;
for (int i = 1; i < MAXT; ++i) {
PowOfBase[i] = PowOfBase[i - 1] * BASE;
}
for (int i = 0; i < n; ++i) {
a[i] += PowOfBase[MAXT - 1];
}
int pos = 0;
while (pos < MAXT) {
memset(RadixBucketTop, 0, sizeof(RadixBucketTop));
for (int i = 0; i < n; ++i) {
int rdx = a[i] / PowOfBase[pos] % BASE;
RadixBucket[rdx][RadixBucketTop[rdx]++] = a[i];
}
int top = 0;
for (int i = 0; i < BASE; ++i) {
for (int j = 0; j < RadixBucketTop[i]; ++j) {
a[top++] = RadixBucket[i][j];
}
}
pos++;
}
for (int i = 0; i < n; ++i) {
a[i] -= PowOfBase[MAXT - 1];
}
}
11、堆排序
堆排序(Heap Sort)是一种基于比较的排序算法,它利用堆这种数据结构来进行排序。堆是一种完全二叉树,具有堆性质:对于最大堆,父节点的值总是大于或等于其子节点的值;对于最小堆,父节点的值总是小于或等于其子节点的值。堆排序通常使用最大堆来实现升序排序。
堆排序的基本步骤
1. **构建最大堆**:将待排序的数组构建成一个最大堆。最大堆的根节点是最大元素。
2. **交换和调整**:将堆顶元素(最大元素)与堆的最后一个元素交换,然后减少堆的大小(排除最后一个元素),并对新的堆顶元素进行“堆化”(调整)以恢复堆的性质。
3. **重复步骤 2**:重复交换和调整的过程,直到堆的大小为 1。
时间复杂度
- **时间复杂度**:O(n log n),其中 n 是待排序元素的数量。
- 建立堆的时间复杂度为 O(n)。
- 每次堆化的时间复杂度为 O(log n),总共需要进行 n 次堆化。
- **空间复杂度**:O(1),堆排序是原地排序算法,不需要额外的存储空间。
优缺点
## 优点
- **时间复杂度稳定**:无论输入数据的状态如何,时间复杂度始终为 O(n log n)。
- **原地排序**:不需要额外的存储空间。
## 缺点
- **不稳定**:堆排序不是稳定的排序算法,相同元素的相对顺序可能会改变。
- **常数因素较大**:在实际应用中,堆排序的常数因素较大,性能通常不如快速排序和归并排序。
适用场景
堆排序适用于以下情况:
- 需要保证时间复杂度为 O(n log n) 的场合。
- 需要原地排序的场合。
- 对于大规模数据的排序,尤其是在内存有限的情况下。
堆排序是一种可靠的排序算法,尤其在需要稳定时间复杂度和原地排序的情况下非常有用。
idType lson(idType idx) { // 当前数的左子结点
return idx * 2 + 1;
}
idType rson(idType idx) { // 当前数的右子结点
return idx * 2 + 2;
}
idType parent(idType idx) { // 当前数的父结点
return (idx - 1) / 2;
}
bool better(eleType a, eleType b) { // < 小顶堆 > 大顶堆
return a > b;
}
void Heapify(vector<eleType>& heap, int size, eleType curr) {
idType lsonId = lson(curr);
idType rsonId = rson(curr);
idType optId = curr;
if (lsonId < size && better(heap[lsonId], heap[optId])) {
optId = lsonId;
}
if (rsonId < size && better(heap[rsonId], heap[optId])) {
optId = rsonId;
}
if (optId != curr) {
swap(heap[curr], heap[optId]);
Heapify(heap, size, optId);
}
}
void HeapSort(vector<int>& nums) {
for (int i = nums.size() / 2; i >= 0; --i) { // 遍历堆,排序
Heapify(nums, nums.size(), i);
}
for (int i = nums.size() - 1; i >= 0; --i) { // 取出顶部数据(最大值/最小值)
swap(nums[0], nums[i]);
Heapify(nums, i, 0);
}
}
12、哈希算法
哈希算法(Hash Algorithm)是一种将输入数据(通常是任意长度)转换为固定长度的输出(哈希值或散列值)的算法。哈希算法广泛应用于数据存储、数据检索、数据完整性校验、密码学等领域。哈希函数的特点使其在计算机科学中非常重要。
哈希算法的基本特性
1. **确定性**:相同的输入总是产生相同的输出。
2. **快速计算**:哈希函数应能快速计算出哈希值。
3. **抗碰撞性**:难以找到两个不同的输入产生相同的哈希值(碰撞)。
4. **抗篡改性**:对输入数据的微小改动会导致哈希值的显著变化。
5. **固定输出长度**:无论输入数据的大小如何,输出的哈希值长度是固定的。
常见的哈希算法
1. **MD5(Message-Digest Algorithm 5)**:
- 输出长度:128位(16字节)。
- 常用于数据完整性校验,但由于其安全性问题(存在碰撞攻击),不再推荐用于安全敏感的应用。
2. **SHA-1(Secure Hash Algorithm 1)**:
- 输出长度:160位(20字节)。
- 也曾广泛使用,但同样因为安全性问题(已被证明可被攻击)而不再推荐用于安全应用。
3. **SHA-256(Secure Hash Algorithm 256)**:
- 输出长度:256位(32字节)。
- 属于SHA-2系列,安全性较高,广泛用于区块链技术和数字签名。
4. **SHA-3(Secure Hash Algorithm 3)**:
- 最新的哈希标准,支持多种输出长度(224、256、384、512位)。
- 设计上与SHA-2不同,基于Keccak算法,具有更高的安全性和灵活性。
5. **bcrypt**:
- 主要用于密码哈希,具有自适应性,能够根据计算能力调整哈希复杂度。
6. **Argon2**:
- 现代密码哈希函数,具有抗GPU和ASIC攻击的特性。
哈希算法的应用
1. **数据存储**:
- 哈希表:通过哈希函数将数据映射到固定大小的数组中,以实现快速的查找和插入。
2. **数据完整性校验**:
- 在文件传输或存储时,使用哈希值来验证数据是否被篡改。
3. **密码存储**:
- 将用户密码哈希后存储,增加安全性。即使数据库被攻击,攻击者也无法直接获得用户密码。
4. **数字签名**:
- 在数字签名中,先对消息进行哈希,再用私钥对哈希值进行签名,以确保消息的完整性和身份验证。
5. **区块链**:
- 区块链技术中广泛使用哈希函数,确保区块数据的完整性和不可篡改性。
总结
哈希算法是一种重要的技术,其在计算机科学和信息安全领域有着广泛的应用。选择适当的哈希算法对于确保数据的安全性和完整性至关重要。在现代应用中,推荐使用SHA-256及以上版本或专门的密码哈希函数(如bcrypt、Argon2)来处理敏感数据。
17、贪心算法
心算法是一种用于解决最优化问题的算法策略,它通过逐步构建解决方案的方式来达到全局最优。贪心算法的基本思想是:在每一步选择中都采取当前状态下最好或最优的选择,从而希望通过一系列的局部最优选择达到全局最优。
贪心算法的基本步骤:
- 选择性:在每个阶段做出一个局部最优的选择。
- 可行性:确保所做的选择是可行的,即不违反问题的约束条件。
- 最优性:通过选择的局部最优解,构建出全局最优解。
2093

被折叠的 条评论
为什么被折叠?



