前言
本篇介绍十大经典排序算法中的五个最常用的算法,选择合适的排序算法取决于具体的应用场景和数据特点。若数据规模较小且简单,可以选择冒泡排序或插入排序;若数据规模较大且需要较高的性能,可以考虑快速排序、堆排序或希尔排序。
根据时间复杂度区分:
1、O(nlogn) a、希尔排序 ;b、堆排序; c、快速排序; d、归并排序
2、O(n) a、计数排序; b、基数排序; c、桶排序
3、O(n2) a、冒泡排序; b、选择排序; c、插入排序
排序算法的优点主要包括以下几个方面:
1、性能好:
某些排序算法在平均情况下具有较好的性能表现,如快速排序和堆排序,它们的时间复杂度为O(nlogn),适用于大型数据集的排序。
2、适应性强:
有些排序算法对于部分有序或近乎有序的数据集具有较好的适应性,如插入排序,在这种情况下,它的效率较高。
3、稳定性:
稳定的排序算法能够保持相同元素的相对顺序不变,对于某些需要保持原有顺序的场景,这是一个重要的优点,如冒泡排序和插入排序。
4、空间复杂度低:
部分排序算法的空间复杂度较低,只需要常数级别的额外空间,如冒泡排序和插入排序,这在内存有限或需要节省内存空间的情况下是一个优点。
5、易于实现:
某些排序算法的实现相对简单,代码量较少,易于理解和实现,如冒泡排序和插入排序,这使得它们在一些特定情况下成为首选。
正文
01-冒泡排序
特点:冒泡排序是一种简单直观的排序算法,它会多次遍历要排序的数列,每次遍历都会比较相邻的元素,如果顺序错误就交换它们。
优点:实现简单,易于理解和实现,适用于小型数据集。
缺点:效率较低,对于大型数据集效率低下,时间复杂度为O(n2)。
动图解释:
代码实现:
#include <iostream>
using namespace std;
int main()
{
// 利用冒泡排序实现升序排列
int arr[9] = { 4,2,8,0,5,7,1,9,3 };
int len = sizeof(arr) / sizeof(arr[0]);
cout << "排序前数据:" << endl;
for (int i = 0; i <= len;i++)
{
cout << arr[i] << " ";
}
cout << endl;
// 开始冒泡排序
//总共排序轮数为 元素个数-1
for (int i = 0; i < len - 1;i++)
{
// 内层循环 次数 = 元素个数-当前轮数-1
for (int j = 0; j < len - i -1 ;j++)
{
// 如果第一个数字比第二个数字大,则交换两个数字
while (arr[j]>arr[j+1]) // 使用if或者while均可
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
// 排序后结果
cout << "排序后数据:" << endl;
for (int i = 0; i <= len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
02-插入排序
特点:插入排序通过构建有序序列,对未排序数据逐个进行插入操作,从而将序列扩大为有序序列。
优点:对于小型数据集或部分有序的数据集,插入排序效率较高,且在实现上比较简单。
缺点:对于大型数据集,效率较低,时间复杂度为O(n2),尤其是在数据集基本有序时效率降低。
动图解释:
代码实现与解释:
下方函数中的参数列表中第一个参数这里用的是Array[],数组名加中括号,证明这里传入的是一个数组,也就是函数调用时,直接使用InsertSort(Array,len); 其实 Array[]等同于*Array,因为咱们定义指针是需要这个指针指向一段地址的,而数组名恰好就是该数组的首地址,因此,虽然定义函数时定义的是一个指针,但是调用函数传入时,仅需要将数组名传进去即可,那也就是说 int Array[] 还有一种写法 int * Array
#include <iostream>
using namespace std;
void InsertSort(int Array[], int len) {
int i, j, temp;//第一层循环
for (i = 1; i < len; i++) { // 将各元素插入到已经排好的序列
if (Array[i] < Array[i - 1]) { // 若是当前的数值小于前面的
temp = Array[i]; // 使用一个变量暂存该数据
for (j = i - 1; j >= 0 && Array[j] > temp; --j) {
Array[j + 1] = Array[j]; //所有大于temp的元素向后移位
}
Array[j + 1] = temp;
}
}
}
int main()
{
int arr[9] = { 4,2,8,0,5,7,1,9,3 };
int len = sizeof(arr) / sizeof(arr[0]);
cout << "排序前数据:" << endl;
for (int i = 0; i <= len;i++)
{
cout << arr[i] << " ";
}
cout << endl;
InsertSort(arr, len);
// 排序后结果
cout << "排序后数据:" << endl;
for (int i = 0; i <= len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
03-希尔排序
特点:希尔排序是插入排序的改进版本,它将待排序的数组分割成若干个子序列,然后分别对子序列进行插入排序,最后再对整个数组进行一次插入排序。
优点:相较于简单的插入排序,希尔排序在大型数据集上有较好的性能表现,时间复杂度介于O(nlogn)到O(n2)之间,取决于增量序列的选择。
缺点:希尔排序的增量序列的选择对性能有较大影响,不同的增量序列可能导致不同的性能表现。
具体代码实现与解释如下:
#include <iostream>
using namespace std;
void ShellSort(int Array[], int len)
{
int d, i, j, temp;
for (d = len / 2; d > 0; d = d / 2) { // 将各元素插入到已经排好的序列
for (i = d; i < len; i++) { //若是当前的数值小于前面的
//这里还是应该加上条件语句的,因为不加的话,即使当前元素大于该子表上一个元素
// 也会进行数据赋值给temp,会浪费时间
if (Array[i] < Array[i - d]) {
temp = Array[i];//这句代码用于检查该子表中前面还有没有其他数据需要处理
for (j = i - d; j >= 0 && temp < Array[j]; j -= d) {
// 这里让子表中的下一个元素等于当前元素,而不是直接安顺序后移
// 这的j+d=6,也就是上面Array[i]的元素下标
Array[j + d] = Array[j];
}
//上一步骤发现当前小于前面的元素就比如9在前,2在后,
//此时在佛如循环里已经进行替换9在后面了,而2赋值在temp中
//这里j+d=2也就是上面Array[i-d]的下标
Array[j + d] = temp;
}
}
}
}
int main()
{
int arr[9] = { 4,2,8,0,5,7,1,9,3 };
int len = sizeof(arr) / sizeof(arr[0]);
cout << "排序前数据:" << endl;
for (int i = 0; i <= len;i++)
{
cout << arr[i] << " ";
}
cout << endl;
ShellSort(arr, len);
// 排序后结果
cout << "排序后数据:" << endl;
for (int i = 0; i <= len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
04-快速排序
特点:快速排序采用分治法,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的小,然后再按此方法对这两部分数据分别进行快速排序,最终实现整个序列有序。
优点:在平均情况下,快速排序的性能非常好,时间复杂度为O(nlogn),且实现简单。
缺点:最坏情况下的性能较差,时间复杂度为O(n2),且对于大型数据集,可能会出现栈溢出问题。
动图解释:
具体代码实现与解释如下:
#include <iostream>
using namespace std;
// 用第一个元素将待排序序列划分成左右两个部分
int Partition(int Array[], int low, int high) {
int pivot = Array[low];// 第一个元素作为基轴
while (low < high) { //这里判断1ow是否小于high的循环条件
//这里又进入一层循环,也就是如果high对应的值大于基轴值,则元素不用往前移,并且high自动--,位置前移
while (low < high && Array[high] >= pivot) { --high; }
//当high下标对应的元素比基轴值小的时候,跳出循环,将该元素移动到1ow下标对应的位置
Array[low] = Array[high];
// 当high下标对应的循环跳出之后,接着进行1ow下标对应的循环,
//如果1ow对应的值小于基轴值,则元素不用往后移,并且1ow自动++,位置后移
while (low < high && Array[low] <= pivot) { ++low; }
//当1ow下标对应的元素比基轴值大的时候,跳出循环,将该元素移动到high下标对应的位置
Array[high] = Array[low];
}
//当1ow<high的时候,证明所有元素全部比较完毕,将刚才做基轴的元素赋值给1ow对应元素即可
Array[low] = pivot;
//然后返回划分子表的下标位置
return low;
}
// 这里就是调用上个函数进行递归运算
void Quicksort(int Array[], int low, int high) {
if (low < high) {//这里就明显运用了递归的思想
int pivotpos = Partition(Array, low, high);//划分
//第一步执行完毕,返回一个pivotpos值,就是最终1ow和high共同停留的位置,
//那么此时得到的一个子表就是0-pivotpos - 1范围的子表,对其再次进行Partition函数调用
Quicksort(Array,low,pivotpos - 1);//划分左子表
//此时得到的另一个子表就是pivotpos+1-0范围的子表,对其再次进行Partition函数调用
Quicksort(Array,pivotpos + 1,high);//划分右子表
}
}
int main()
{
int arr[9] = { 4,2,8,0,5,7,1,9,3 };
int low = 0;
int len = sizeof(arr) / sizeof(arr[0]) - 1 ;
cout << "排序前数据:" << endl;
for (int i = 0; i <= len;i++)
{
cout << arr[i] << " ";
}
cout << endl;
Quicksort(arr, low, len);
// 排序后结果
cout << "排序后数据:" << endl;
for (int i = 0; i <= len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
05-堆排序
特点:堆排序利用堆这种数据结构,通过建立最大堆或最小堆来进行排序。
优点:堆排序在最坏情况下的性能也比较好,时间复杂度为O(nlogn),且不需要额外的空间开销。
缺点:相对于快速排序,堆排序的实现稍显复杂,而且不够稳定。
动图解释:
具体代码实现与解释如下:
堆排序方法进行排序,首先建立一个大根堆,特点就是 根>=左右孩子首先应该将所有的非终端节点(分支节点)全部检查一遍,看是否满足大根堆的要求不满足则进行调整。在顺序存储的完全二叉树中,非终端节点就是编号为i<=[n/2]比如总共有8个数据,证明有8个节点,则i<=4,也就是前四个就是非终端节点,处理时先处理最远的,也就是4号节点,其实就是第四个数,这是线性表中的数,从1开始数对于非终端节点i,它的左孩子为2i,也就是第8个数,右孩子为2i+1,父节点为i/2.方法就是检查该节点是否大于左右孩子节点,若不大于,则与更大的孩子互换即可
#include <iostream>
using namespace std;
void HeadAdjust(int Array[], int k, int len) {
int temp = Array[k];// 最好别用哨兵的方法,直接找一个变量存储
//这里一开始让i=根节点的左孩子位置,进入循环
for (int i = 2 * k; i <= len; i *= 2) {
//下面的这个循环就是在做一个元素下坠的操作,首先判断左孩子(i)是否小于右孩子11
//若是小于,则i++,此时i指向右孩子,若是大于,不满足,继续指向左孩子
if (i < len && Array[i] < Array[i + 1]) { i++; }
//这里如果节点大于左孩子或者右孩子,其实这里就比较了一个,因为左孩子和右孩子的比较
//在上一步条件语句中已经执行,这里比较之后,如果满足,证明节点值大,直接退出循环即可
if (temp >= Array[i]) { break; }
//若是不满足,则将孩子与节点位置互换,并且同时k向前移动,占据i的位置
//这里如果还没有结束,可以继续循环,此时k又变成了下一个节点,继续上述步骤
else { Array[k] = Array[i]; k = i; }
}
//当i > len时,全部执行完毕,退出循环,此时再将在哨兵位置保存的值赋给Array[k]
Array[k] = temp;
}
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
void Heapsort(int Array[], int len) {
for (int i = len / 2 - 1; i >= 0; i--) {
HeadAdjust(Array, i, len);
}
//用1en-1是因为对数组来说是从0下标开始计数
for (int i = len - 1; i > 0; i--) {
swap(Array[i],Array[0]);
//如果不封装,直接交换也可以
// int temp = Array[i];
// Array[i] = Array[0];
// Array[0] = temp;
HeadAdjust(Array, 0, i - 1);
}
}
int main()
{
int arr[9] = { 4,2,8,0,5,7,1,9,3 };
int len = sizeof(arr) / sizeof(arr[0]);
cout << "排序前数据:" << endl;
for (int i = 0; i < len;i++)
{
cout << arr[i] << " ";
}
cout << endl;
Heapsort(arr, len);
// 排序后结果
cout << "排序后数据:" << endl;
for (int i = 0; i < len; i++)
{
cout << arr[i] << " ";
}
cout << endl;
system("pause");
return 0;
}
总结
上述总结了最常用的几种排序算法,排序算法的优点使得它们能够在不同的场景下发挥作用,根据具体需求和数据特点选择合适的排序算法至关重要。