一,冒泡排序
冒泡排序是一种简单的排序算法,它的基本思想是通过比较相邻元素的大小,将较大或较小的元素向数组的一端移动,并重复这个过程,直到整个数组有序为止。
具体实现步骤如下:
- 从数组的第一个元素开始,依次比较相邻的两个元素的大小关系,如果前一个元素比后一个元素大(或小),则交换它们的位置,使较大(或较小)的元素向数组的尾部移动。
- 继续对剩下的元素进行同样的比较和交换操作,直到将最大(或最小)的元素移动到数组的末尾。
- 重复执行第1步和第2步,但不再考虑已经排好序的元素,直到整个数组有序为止。
下面是一个简单的C++实现代码:
void bubble_sort(int arr[], int n) {
for (int i = 0; i < n - 1; ++i) { // 外层循环控制排序轮数
for (int j = 0; j < n - i - 1; ++j) { // 内层循环控制每轮的比较次数
if (arr[j] > arr[j + 1]) { // 比较相邻元素的大小
// 交换相邻元素的位置
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
在上述代码中,我们定义了一个名为bubble_sort()
的函数,它接受一个整型数组和数组长度作为参数。该函数通过嵌套的for
循环实现冒泡排序算法。外层循环控制排序轮数(即需要比较的次数),内层循环控制每轮的比较次数。在比较相邻元素的大小时,如果前一个元素比后一个元素大,则交换它们的位置。
优化方法:
如果序列已经是有序的,可以优化冒泡排序的方法。
具体做法是每趟排序时判断一下是否交换过元素 ,如果没
有交换过元素,证明序列已经有序,排序提前结束。
冒泡排序的时间复杂度为O(n*2),空间复杂度为O(1)。虽然它是一种简单的排序算法,但在实际应用中,它的性能并不理想,因为它需要进行多次交换操作,而交换操作是比较耗时的。对于大规模数据的排序,其他高效的排序算法(如快速排序、归并排序等)更为适用。
二,选择排序
选择排序是一种简单的排序算法,它的基本思想是每次从待排序的元素中选出最小(或最大)的一个元素,将它与序列的第一个元素进行交换,然后再从剩下的元素中选出最小(或最大)的一个元素,将它与序列的第二个元素进行交换,直到整个序列有序为止。
具体实现步骤如下:
- 从数组的第一个元素开始,依次扫描整个数组,找出最小(或最大)的元素,记下它的下标。
- 如果最小(或最大)元素的下标不是当前正在扫描的第一个元素,就将它和第一个元素交换位置。
- 然后从剩余的元素中继续执行步骤1和步骤2,直到整个数组有序为止。
下面是一个简单的C++实现代码:
void selection_sort(int arr[], int n) {
for (int i = 0; i < n - 1; ++i) { // 外层循环控制排序轮数
int min_idx = i; // 记录最小元素的下标
for (int j = i + 1; j < n; ++j) { // 内层循环从剩余元素中寻找最小元素
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}
if (min_idx != i) { // 如果最小元素的下标不是当前正在扫描的第一个元素
// 交换最小元素与当前元素的位置
int tmp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = tmp;
}
}
}
在上述代码中,我们定义了一个名为selection_sort()
的函数,它接受一个整型数组和数组长度作为参数。该函数通过嵌套的for
循环实现选择排序算法。外层循环控制排序轮数(即需要比较的次数),内层循环从剩余元素中寻找最小元素。在找到最小元素后,如果最小元素的下标不是当前正在扫描的第一个元素,就将它和第一个元素交换位置。
优化方法:
选择排序一趟只选取最小值,优化的办法就是一趟把最小
值和最大值都选出来,最小的放在左边,最大的放在右边。
优化后的方法循环趟数将减半。
选择排序的时间复杂度为O(n*2),空间复杂度为O(1)。虽然它比冒泡排序要好一些,但在实际应用中,它的性能也不太理想。对于大规模数据的排序,其他高效的排序算法(如快速排序、归并排序等)更为适用。
三,插入排序
插入排序是一种简单直观的排序算法,它的基本思想是将待排序元素分成两个部分:已排序和未排序。初始时,已排序部分只有一个元素,即序列的第一个元素,而未排序部分包括剩余的元素。排序过程中,每次从未排序部分中取出一个元素,将它插入到已排序部分的合适位置,使得插入后仍然保持已排序部分的有序性,直到所有元素都插入到已排序部分为止。
具体实现步骤如下:
- 将待排序序列分成已排序和未排序两个部分。初始时,已排序部分只包括第一个元素,即
arr[0]
。 - 从未排序的元素中取出一个元素,将它插入到已排序部分的合适位置。具体地,从已排序部分的最右边开始,向左逐个比较已排序部分的元素,找到第一个小于(或大于)当前元素的位置,然后将当前元素插入到该位置之后。
- 重复步骤2,直到未排序部分中的所有元素都插入到已排序部分为止。
下面是一个简单的C++实现代码:
void insertion_sort(int arr[], int n) {
for (int i = 1; i < n; ++i) { // 将第i个元素插入到已排序序列的合适位置
int key = arr[i]; // 当前待排序元素
int j = i - 1; // 已排序序列的最右边元素
while (j >= 0 && arr[j] > key) { // 从右向左比较已排序序列的元素,找到合适位置
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key; // 插入当前待排序元素
}
}
在上述代码中,我们定义了一个名为insertion_sort()
的函数,它接受一个整型数组和数组长度作为参数。该函数通过嵌套的for
循环实现插入排序算法。外层循环控制待排序元素的选择,内层循环比较已排序部分的元素并插入当前待排序元素。
插入排序的时间复杂度为O(n*2),空间复杂度为O(1)。。虽然它不如快速排序、归并排序等高级排序算法效率高,但对于小规模的数据排序,其表现良好。此外,插入排序也是其他高级排序算法(如希尔排序)的基础。
四,希尔排序
希尔排序是一种改进的插入排序算法,也称为缩小增量排序。它通过将原始数组分割成若干个较小的子数组进行排序,最终将整个数组排序。
希尔排序的基本思想是先将待排序的元素按照一定的间隔进行分组,对每个分组内的元素进行插入排序,然后逐步减小间隔,重复进行分组和插入排序操作,直到间隔为1,即最后一次对整个数组进行插入排序。这样,在进行插入排序时,数组中的元素已经基本有序,可以大幅度减少插入排序的移动次数,从而提高排序效率。
具体实现步骤如下:
- 选择一个增量序列,通常是使用希尔增量(例如取间隔为数组长度的一半(为奇数的话再减一),然后每次减半),对数组进行分组。
- 对每个分组内的元素进行插入排序。此时,每个分组内的元素相对有序,但整个数组仍然不是有序的。
- 重复上述步骤,不断缩小增量,直到增量为1。
- 最后一次使用增量为1的插入排序完成对整个数组的排序。
下面是一个简单的C++实现代码:
void shell_sort(int arr[], int n) {
for (int gap = n / 2; gap > 0; gap /= 2) { // 增量序列
for (int i = gap; i < n; ++i) { // 对每个分组进行插入排序
int temp = arr[i];
int j;
for (j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {
arr[j + gap] = arr[j];
}
arr[j + gap] = temp;
}
}
}
在上述代码中,我们定义了一个名为shell_sort()
的函数,它接受一个整型数组和数组长度作为参数。该函数通过嵌套的for
循环实现希尔排序算法。外层循环控制增量序列的选择,内层循环对每个分组进行插入排序。
希尔排序的时间复杂度取决于增量序列的选择,一般情况下为O(n*3/2),空间复杂度为O(1)。相对于简单的插入排序,希尔排序的性能有较大的提升,尤其适用于中等规模的数据排序。然而,希尔排序仍然不如快速排序、归并排序等高级排序算法效率高。
五,快速排序
快速排序是一种高效的排序算法,它的基本思想是通过分治的策略,将待排序序列划分成左右两个子序列,使得左子序列中所有元素均小于右子序列中的元素。然后再对左右子序列分别递归地应用快速排序算法,直到每个子序列只包含一个或零个元素为止。
具体实现步骤如下:
- 选取一个基准元素(pivot),通常选择序列的第一个元素或最后一个元素。
- 将序列中小于等于基准元素的元素放在基准元素的左边,大于基准元素的元素放在基准元素的右边,相等的元素可以放在任意一边。此时,基准元素的位置已经确定。
- 对基准元素左边的子序列和右边的子序列分别递归地应用快速排序算法,重复上述步骤,直到每个子序列只包含一个或零个元素为止。
下面是一个简单的C++实现代码:
int partition(int arr[], int low, int high) {
int pivot = arr[high]; // 基准元素
int i = low - 1; // i指向小于等于pivot的元素
for (int j = low; j <= high - 1; ++j) {
if (arr[j] <= pivot) {
i++;
std::swap(arr[i], arr[j]);
}
}
std::swap(arr[i + 1], arr[high]); // 将基准元素放到正确的位置
return i + 1;
}
void quick_sort(int arr[], int low, int high) {
if (low < high) {
int pivot = partition(arr, low, high); // 划分子序列
quick_sort(arr, low, pivot - 1); // 递归地对左子序列排序
quick_sort(arr, pivot + 1, high); // 递归地对右子序列排序
}
}
在上述代码中,我们定义了一个名为quick_sort()
的函数,它接受一个整型数组、数组最小下标和最大下标作为参数。该函数通过嵌套的递归调用实现快速排序算法。其中,partition()
函数用于划分子序列并返回基准元素的下标。
快速排序的时间复杂度为O(nlogn),空间复杂度为O(logn)。快速排序是一种原地排序算法,不需要额外的存储空间。它的平均性能比其他常见的排序算法(如归并排序)要好,在实际应用中得到广泛应用。
优化方法:
1 )采用更合理的基准数(中心轴) ,减少递归的深度。从数列中选取多个数,取中间数。
2 )结合插入排序,区间在10个元素之内采用插入排序,效率更高。
以下是我认为比较好的博主写的,实现了一下
图解快速排序(C++实现)_快速排序c++实现-CSDN博客
#include<iostream>
#include<string>
#include<vector>
using namespace std;
void quickSort(int left, int right, vector<int>& arr)
{
if (left >= right)
return; // 如果左边索引大于等于右边索引,表示已经排好序,直接返回
int i, j, base, temp;
i = left;
j = right;
base = arr[left]; // 取最左边的数为基准数
while (i < j)
{
// 从右往左找第一个比基准数小的数
while (arr[j] >= base && i < j)
j--;
// 从左往右找第一个比基准数大的数
while (arr[i] <= base && i < j)
i++;
if (i < j)
{
// 将找到的两个数交换位置
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 基准数归位
arr[left] = arr[i];
arr[i] = base;
// 对基准数左边的子数组进行递归排序
quickSort(left, i - 1, arr);
// 对基准数右边的子数组进行递归排序
quickSort(i + 1, right, arr);
}
int main() {
vector<int> a = { 12,54,231,65,12,3,6,6,23,564,54,767,23,235,12,56,1,56 };
quickSort(0, a.size() - 1,a);
for (int i = 0; i < a.size(); i++) {
cout << a[i] << endl;
}
return 0;
}
六,归并排序
归并排序是一种经典的排序算法,它的原理基于分治法。该算法将待排序的序列不断地分割成小的子序列,直到每个子序列只有一个元素,然后再逐步将这些子序列合并起来,最终得到一个有序的序列。
下面是归并排序的详细步骤:
- 将待排序序列不断地平均分割成两个子序列,直到每个子序列只有一个元素。
- 对每对相邻的子序列进行合并操作,形成新的有序子序列,直到所有子序列都合并为一个序列。
合并操作的具体步骤如下:
- 创建一个临时数组,用于存放合并后的有序序列。
- 设定两个指针,分别指向待合并的两个子序列的起始位置。
- 比较两个指针指向的元素,将较小的元素放入临时数组,并将对应指针向后移动一位。
- 重复步骤3,直到其中一个子序列的元素全部放入临时数组。
- 将另一个子序列中剩余的元素依次放入临时数组。
- 将临时数组中的元素复制回原序列的对应位置。
通过不断地分割和合并操作,归并排序可以逐渐将序列排序为有序。由于分割和合并操作都是稳定且时间复杂度为 O(n),因此归并排序的时间复杂度为 O(nlogn)。然而,由于归并排序需要额外的空间来存储临时数组,因此其空间复杂度为 O(n)。
归并排序是一种高效且稳定的排序算法,常被用于对大规模数据进行排序。
当我们使用归并排序对以下序列进行排序时:[8, 3, 5, 1, 9, 2, 7, 6, 4]。
首先,按照归并排序的原理,我们将序列分割成多个子序列,直到每个子序列只有一个元素: [8] [3] [5] [1] [9] [2] [7] [6] [4]
然后,开始逐步合并这些子序列: 合并第一对子序列 [8] 和 [3],得到 [3, 8]。 合并第二对子序列 [5] 和 [1],得到 [1, 5]。 合并第三对子序列 [9] 和 [2],得到 [2, 9]。 合并第四对子序列 [7] 和 [6],得到 [6, 7]。 合并第五对子序列 [4] 和 [6, 7],得到 [4, 6, 7]。
现在,我们得到了有序的子序列:[3, 8] [1, 5] [2, 9] [4, 6, 7]。
接下来,我们再次进行合并操作: 合并第一对子序列 [3, 8] 和 [1, 5],得到 [1, 3, 5, 8]。 合并第二对子序列 [2, 9] 和 [4, 6, 7],得到 [2, 4, 6, 7, 9]。
最后,合并得到的两个有序子序列 [1, 3, 5, 8] 和 [2, 4, 6, 7, 9],得到最终的有序序列:[1, 2, 3, 4, 5, 6, 7, 8, 9]。
这就是使用归并排序对给定序列进行排序的过程。
#include <iostream>
using namespace std;
// 合并两个有序数组,生成新的有序数组
void merge(int arr[], int left, int mid, int right) {
int i = left, j = mid + 1, k = 0;
int temp[right - left + 1];
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
for (int p = 0; p < k; p++) {
arr[left + p] = temp[p];
}
}
// 归并排序递归函数
void merge_sort(int arr[], int left, int right) {
if (left < right) {
int mid = (left + right) / 2;
merge_sort(arr, left, mid);
merge_sort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
int main() {
int arr[] = {8, 3, 5, 1, 9, 2, 7, 6, 4};
int len = sizeof(arr) / sizeof(int);
cout << "Before sorting: ";
for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
cout << endl;
merge_sort(arr, 0, len - 1);
cout << "After sorting: ";
for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
七,堆排序
堆排序(Heap Sort)是一种基于二叉堆数据结构的排序算法。它的原理可以分为两个主要步骤:建堆和排序。
-
建堆(Heapify): 堆排序首先需要将待排序的数组构建成一个二叉堆。二叉堆是一棵完全二叉树,满足父节点的值大于或小于其子节点的值(这取决于是最大堆还是最小堆)。建堆过程可以通过从最后一个非叶子节点开始,依次向上调整每个子树,使得每个子树都满足堆的性质。具体而言,可以使用下面的算法:
- 从最后一个非叶子节点开始,向上遍历到根节点。
- 对于当前节点的每个子树,比较父节点与子节点的值,如果不满足堆的性质,则交换父节点和子节点的值,并继续递归地向下调整子树。
在建堆完成后,根节点即为堆中的最大(或最小)值。
-
排序: 排序阶段从堆顶取出根节点的值,并将其与堆的最后一个叶子节点交换位置,然后缩小堆的范围。接着,对新的堆顶节点进行向下调整,使得剩余部分重新满足堆的性质。重复以上步骤,直到所有元素都被取出并排好序。
堆排序的时间复杂度为 O(n log n),其中 n 表示待排序数组的大小。它是一种不稳定的排序算法,因为在构建堆和交换元素的过程中,可能会改变相同值的元素的相对顺序。
需要注意的是,堆排序通常使用数组来表示堆,而不是使用二叉树的数据结构。这是因为数组具有更好的局部性和随机访问的能力,可以减少额外的指针操作和内存开销。
#include <iostream>
using namespace std;
void heapify(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]);
heapify(arr, n, largest);
}
}
void heapSort(int arr[], int n) {
// 构建最大堆(从最后一个非叶子节点开始向上调整)
for (int i = n / 2 - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 依次取出堆顶元素,放到数组末尾,并重新调整堆
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]);
heapify(arr, i, 0);
}
}
int main() {
int arr[] = {8, 3, 5, 1, 9, 2, 7, 6, 4};
int len = sizeof(arr) / sizeof(int);
cout << "Before sorting: ";
for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
cout << endl;
heapSort(arr, len);
cout << "After sorting: ";
for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
八,计数排序
计数排序(Counting Sort)是一种线性时间复杂度的排序算法,适用于待排序元素的范围较小的情况。它的原理可以分为三个主要步骤:计数、累加和重排。
-
计数: 计数阶段需要统计待排序数组中每个元素出现的次数,并将统计结果存储在一个辅助数组中。具体而言,可以使用下面的算法:
- 创建一个辅助数组
count
,长度等于待排序数组的最大值加一。 - 遍历待排序数组,对每个元素进行计数,将其在
count
数组中对应位置的值加一。
- 创建一个辅助数组
-
累加: 累加阶段需要对计数数组进行累加操作,得到每个元素在排序后数组中的最终位置。具体而言,可以使用下面的算法:
- 创建一个辅助数组
sum
,长度与count
数组相同。 - 初始化
sum[0]
为count[0]
,然后遍历count
数组,依次累加前面位置的值,将结果存储在sum
数组中。
- 创建一个辅助数组
-
重排: 重排阶段根据计数和累加结果,将待排序数组中的元素按照正确的位置重新放置到一个新的数组中。具体而言,可以使用下面的算法:
- 创建一个与待排序数组相同长度的辅助数组
result
。 - 遍历待排序数组,对于每个元素
arr[i]
,根据count[arr[i]]
获取其在排序后数组中的位置,并将其放置到result
数组中该位置。 - 将
result
数组复制回原始数组arr
,以完成排序。
- 创建一个与待排序数组相同长度的辅助数组
计数排序的时间复杂度为 O(n+k),其中 n 表示待排序数组的大小,k 表示待排序元素的范围。它是一种稳定的排序算法,因为在重排阶段,相同值的元素保持了原始的相对顺序。
需要注意的是,计数排序适用于非负整数的排序,对于浮点数或负数的排序,需要进行相应的转换和处理。
#include <iostream>
using namespace std;
void countingSort(int arr[], int n) {
// 找到待排序数组的最大值
int maxVal = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > maxVal) {
maxVal = arr[i];
}
}
// 创建计数数组,并将其所有元素初始化为0
int count[maxVal + 1] = {0};
// 统计待排序数组中每个元素的出现次数
for (int i = 0; i < n; i++) {
count[arr[i]]++;
}
// 对计数数组进行累加操作
for (int i = 1; i <= maxVal; i++) {
count[i] += count[i - 1];
}
// 创建一个临时数组,存储排序结果
int result[n];
// 重排待排序数组中的元素
for (int i = n - 1; i >= 0; i--) {
result[count[arr[i]] - 1] = arr[i];
count[arr[i]]--;
}
// 将排序结果复制回原始数组
for (int i = 0; i < n; i++) {
arr[i] = result[i];
}
}
int main() {
int arr[] = {8, 3, 5, 1, 9, 2, 7, 6, 4};
int len = sizeof(arr) / sizeof(int);
cout << "Before sorting: ";
for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
cout << endl;
countingSort(arr, len);
cout << "After sorting: ";
for (int i = 0; i < len; i++) {
cout << arr[i] << " ";
}
cout << endl;
return 0;
}
九,桶排序
桶排序是一种基于计数的排序算法,它的原理如下:
-
创建固定数量的空桶(bucket),并确定每个桶所能容纳的数据范围。桶的数量应该与待排序元素的数量相当。
-
遍历待排序的数据,并将每个数据放入对应的桶中。具体的放置方式可以通过一定的映射函数来确定。
-
对每个非空的桶进行单独排序。可以使用其他排序算法,也可以继续使用桶排序递归地进行排序。
-
将所有非空桶中的数据按顺序依次取出,得到排好序的结果。
桶排序的时间复杂度取决于桶的数量和桶内排序所采用的算法。在最理想的情况下,当数据均匀分布时,桶排序的时间复杂度可以达到线性级别O(n)。然而,如果数据分布不均匀,可能会导致桶内的数据量差异较大,进而影响排序效率。
需要注意的是,桶排序要求待排序的数据必须满足一定的条件,如数据均匀分布在一定范围内等。对于某些特定的数据集,桶排序可能并不适用或者需要进行一定的数据转换。
误区:
1)如果数据分布不均匀,大量的数据集中在少数桶里,桶
排序就没效果了。因为桶排序的前提就是数据均匀分布。
2 )桶排序要时间就省不了空间,要空间就省不了时间。
结论是桶排序意义不大。
实际上在现实世界中,大部分的数据分布是均匀的,或者在设计的时候可以让它均匀分布,或者说可以转换为均匀的分布。
既然数据均匀分布了,桶排序的效率就能发挥出来。
理解桶思想可以设计出很高效的算法。
(分库分表)分治
十,基数排序
基数排序是一种非比较排序算法,它的原理如下:
-
确定待排序数据的最大位数,用d表示。
-
从最低位开始(即个位),按照该位的大小将所有数据分配到相应的桶中,注意每个桶的大小需要根据该位的值重新确定。例如,如果当前是个位,而数据中最大的数是3位数,则个位上的数值范围为0~9,因此需要10个桶来存放数据。
-
将每个桶中的数据按照顺序依次取出,得到一个新的序列。
-
重复上述过程,对新序列按照十位、百位、千位等高位逐步排序,直到最高位排序完成。
-
最终得到的序列就是有序的结果。
基数排序的时间复杂度与数据集的位数和数据的值域范围有关,设n表示数据集大小,d表示数据集中的数值位数,则基数排序的时间复杂度为O(d*(n+k)),其中k表示每个桶中的数据数量。需要注意的是,基数排序需要额外的存储空间来存放桶,因此空间复杂度也较高。
基数排序虽然可以处理负数,但是需要额外的处理方式,例如可以将负数与正数分别排序后再合并。此外,基数排序还可以对字符串进行排序,只需要按照字符的ASCII码值来处理即可。