冒泡排序
排序过程:
反复遍历处理一个未排序的数组。将元素与下一个元素比较,其中较大的元素交换到后面。这样最终就可以把最大的元素排在最后。在下一次遍历时忽略最后一位,再次将剩余的最大元素放在剩余元素的最后一位。
对于下面这个含有十个随机数的数组:
18 37 82 97 4 35 83 42 11 52
首次处理:(比较n-1次)
比较18和37,37较大不用交换。比较37和82,82较大不用交换。比较82和97,97较大不用交换。比较97和4,97较大与4交换。比较97和35,97较大与35交换。比较97和83,97较大与83交换。比较97和42,97较大与42交换。比较97和11,97较大与11交换。比较97和52,97较大与52交换。
18 37 82 4 35 83 42 11 52 ||| 97
第二次处理:(比较n-2次)
......
移动了82和83
18 37 4 35 82 42 11 52 ||| 83 97
......
经过9轮比较后数组变为了有序数组:
4 11 18 35 37 42 52 82 83 97
性能特点:
空间复杂度:
时间复杂度:
最好时间复杂度: 进入第二层循环后循环一次没有交换就退出(下面优化后的代码)
平均&最坏时间复杂度:
稳定性:稳定
稳定性------在原始数据序列中,相同的元素在经过排序后,他们的前后顺序并没有发生改变,则排序时稳定的排序。比如对于键值对<int, string>,如果序列中的string是有顺序的,则稳定的排序在排序后可以保持string的顺序。
冒泡排序原理上非常简单直接,空间占用小,但是时间复杂度较大。
冒泡排序是所有排序算法中效率最低的,原因是数据交换次数太多。
代码用到两层for循环:第一层for (int i ......表示处理不同次数,第二层for (int j......表示依序对于数组中元素处理。
cpp
#include <iostream>
#include <random>
#include <ctime>
void BubbleSort(int arr[], int size)
{
for (int i = 0; i < size - 1; i++)
{
for (int j = 0; j < size - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
}
}
//用系统时间种随机数种子,使用随机数生成100以内随机数10个,之后排序数组
int main()
{
int arr[10];
srand(time(NULL));
for (int i = 0; i < 10; i++)
{
arr[i] = rand() % 100;
std::cout << arr[i] << " ";
}
BubbleSort(arr, 10);
//换行输出排序后数组
std::cout << std::endl << "Array Sorted: " << std::endl;
for (int i = 0; i < 10; i++)
{
std::cout << arr[i] << " ";
}
}
输出:
5 12 24 90 80 53 28 70 43 30
Array Sorted:
5 12 24 28 30 43 53 70 80 90
优化:
cpp
void BubbleSort(int arr[], int size)
{
for (int i = 0; i < size - 1; i++)
{
//优化代码:如果一次处理没有做过任何数据交换,则数组已经有序
bool flag = true; //flag表示没有做过交换
for (int j = 0; j < size - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
flag = false; //如果进入if语句,把flag置为false
}
}
if (flag) //如果没有做过交换,跳出循环
return;
}
}
选择排序
排序过程:
反复遍历序列,每次遍历选出范围内的最小值,将这个最小值与范围内第一个元素交换。每次遍历范围去掉上一次放置了最小值的位置。
同样举例这个数组:
18 37 82 97 4 35 83 42 11 52
第一次遍历:
4 37 82 97 18 35 83 42 11 52
我们在遍历时设计一个min记录当前最小值的位置,在遍历完后确认最小值位于min = 4的位置,将这个位置上的元素与第一个元素交换。
第二次遍历:
4 ||| 11 82 97 18 35 83 42 37 52
排除掉第一个元素,在剩余九个元素中找到最小值位于min = 8的位置,将这个位置与遍历范围内的第一个元素交换。
第三次遍历:
4 11 ||| 18 97 82 35 83 42 37 52
min = 4
......
在完成了9次遍历后,我们得到了最终结果:
4 11 18 35 37 42 52 82 ||| 83 97
性能特点:
对比冒泡排序的过程可以发现,选择排序大量减少了交换次数,所以效率比冒泡排序高一些。但是没有办法像冒泡排序那样跳过已经有序的数组。
平均&最好&最坏时间复杂度:
空间复杂度:
稳定性:不稳定
稳定性举例:5① 5② 3 => 3 5② 5①,在排序后相同的元素原本的顺序被改变了。
有上面的思路,容易得到代码:
cpp
void ChoiceSort(int arr[], int size)
{
//遍历 size - 1 次
for (int i = 0; i < size - 1; i++)
{
int min = i;
//每次从第 i + 1 个开始遍历,寻找最小值
for (int j = i + 1; j < size; j++)
{
if (arr[j] < arr[min])
min = j;
}
//找到最小值位置后,如果最小值不在遍历范围首位,与遍历的第一个元素交换
if (min != i)
{
int temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
}
//用系统时间种随机数种子,使用随机数生成100以内随机数10个,之后排序数组
int main()
{
int arr[10];
srand(time(NULL));
for (int i = 0; i < 10; i++)
{
arr[i] = rand() % 100;
std::cout << arr[i] << " ";
}
ChoiceSort(arr, 10);
//换行输出排序后数组
std::cout << std::endl << "Array Sorted: " << std::endl;
for (int i = 0; i < 10; i++)
{
std::cout << arr[i] << " ";
}
}
插入排序
如果数据趋于有序,那么插入排序是效率最高的排序算法。
在基础排序算法中,插入排序效率>冒泡排序&选择排序。插入排序不仅仅没有交换,比较的次数也很少。
排序过程:
用同样的数组演示:
18 37 82 97 4 35 83 42 11 52
插入排序每次将前i个数组成的序列默认为有序,从第i个数开始向前遍历到第一个数,与第i+1个数比较,将第i+1个数插入到首个小于等于它的数的后方。
第一次遍历:
18 ||| 37 82 97 4 35 83 42 11 52
将18这个序列默认为有序,将37与之对比,18 <= 37,将37插入到18后方。
第二次遍历:
18 37 ||| 82 97 4 35 83 42 11 52
将18 37这个序列默认为有序,从第二个元素向前遍历与82对比,37 <= 82,将82插入到37后方。
第三次遍历:
18 37 82 ||| 97 4 35 83 42 11 52
将18 37 82这个序列默认为有序,从第三个元素向前遍历与97对比,82 <= 97,将97插入到82后方。
第四次遍历:
18 37 82 97 ||| 4 35 83 42 11 52
从第四个元素向前遍历与4对比,没有数字小于等于4,将4插入到第一位前面。
4 18 37 82 97 ||| 35 83 42 11 52
第五次遍历:
4 18 37 82 97 ||| 35 83 42 11 52
从第五个元素向前遍历与35对比,18<=35,将35插入到18前面。
4 18 35 37 82 97 ||| 83 42 11 52
......
最后我们得到有序序列:
4 11 18 35 37 42 52 82 83 97 |||
性能特点:
空间复杂度:
时间复杂度:
最好时间复杂度: 已经有序的循环,内层循环会直接跳出
平均时间复杂度&最坏时间复杂度:
稳定性:稳定
参照上面的过程,容易得出代码:
cpp
void InsertSort(int arr[], int size)
{
for (int i = 1; i < size; i++)
{
int temp = arr[i];
int j = i - 1;
//寻找前序元素中小于等于temp的
for (; j >= 0; j--)
{
//如果元素小于等于temp,则跳出循环
if (arr[j] <= temp)
{
break;
}
//如果元素大于temp,则覆盖后面一位,相当于后移
arr[j + 1] = arr[j];
}
//跳出循环后,将temp放置在小于等于它的元素的下一位
//如果循环完成,此时的j = -1,temp会放在第一位
arr[j + 1] = temp;
}
}
//用系统时间种随机数种子,使用随机数生成100以内随机数10个,之后排序数组
int main()
{
int arr[10];
srand(time(NULL));
for (int i = 0; i < 10; i++)
{
arr[i] = rand() % 100;
std::cout << arr[i] << " ";
}
InsertSort(arr, 10);
//换行输出排序后数组
std::cout << std::endl << "Array Sorted: " << std::endl;
for (int i = 0; i < 10; i++)
{
std::cout << arr[i] << " ";
}
}
希尔排序
对数据进行分组插入排序,使得整个数组逐渐区域有序。
排序过程:
用同样的数组演示:
18 37 82 97 4 35 83 42 11 52
希尔排序需要对数组进行分组,我们可以间隔几个元素分组为一组,分组间隔越来越小,直到间隔为1,分组只剩下一个。
对于这个10个元素的数组,分组间隔每次为上一次的1/2,设 gap = size / 2 = 10 / 2 = 5
将数据分组为:
18 35
37 83
82 42
97 11
4 52
对每一组进行插入排序:
18 35
37 83
42 82
11 97
4 52
得到:
18 37 42 11 4 35 83 82 97 52
此时在局部分组中元素有序,在整个数组中元素趋于有序。
再将分组间隔变为原来的1/2,gap = gap / 2 = 2,数据分组为:
18 42 4 83 97
37 11 35 82 52
对每一组进行插入排序:
4 18 42 83 97
11 35 37 52 82
得到:
4 11 18 35 42 37 83 52 97 82
再令gap = gap / 2 = 1,数据分组为一组,对整个数组进行插入排序:
4 11 18 35 42 37 83 52 97 82
得到:
4 11 18 35 37 42 52 82 83 97
性能特点:
空间复杂度:
时间复杂度:
虽然在外层增加了分组循环,但是分组以原来的 减小,循环层数并不是原来的线性关系,而是对数关系。
对于规模的数据,循环只进行
次。
对于n = 10000000 规模的数据,循环只进行了 次。
增加的时间复杂度相比其他for循环增加的时间复杂度可以忽略。
平均时间复杂度: 由于希尔排序可以使序列整体更快趋于有序,平均时间复杂度相对更小。详细推导可以另行搜索。
最好时间复杂度:
最坏时间复杂度:
另外,上面仅仅演示一种分组方式,还有其他的分组方式可以更好优化时间复杂度。
稳定性:不稳定 可能将值相同的元素分入不同的组,造成顺序变化
希尔排序是对插入排序的改进,从插入排序的代码优化得到希尔排序的代码:
可以从插入排序的代码对照修改,更有利于理解希尔排序的代码。
cpp
void ShellSort(int arr[], int size)
{
//外层嵌套分组循环,以越来越小的间隔将元素分组
for (int gap = size / 2; gap > 0; gap = gap / 2)
{
//i从第一个分组的第二个元素开始遍历后面的元素,就可以对所有的分组进行排序
for (int i = gap; i < size; i++)
{
int temp = arr[i];
int j = i - gap; //同个分组中的上一个元素的位置
//寻找前序元素中小于等于temp的
//只需要将原来的对j的+1-1操作改为+gap-gap
for (; j >= 0; j = j - gap) //对同个分组中的前序元素依次遍历
{
//如果元素小于等于temp,则跳出循环
if (arr[j] <= temp)
{
break;
}
arr[j + gap] = arr[j]; //将元素在同个分组中前移
}
//循环结束后,将temp放在同组中标记元素的后一位
arr[j + gap] = temp;
}
}
}
//用系统时间种随机数种子,使用随机数生成100以内随机数10个,之后排序数组
int main()
{
int arr[10];
srand(time(NULL));
for (int i = 0; i < 10; i++)
{
arr[i] = rand() % 100;
std::cout << arr[i] << " ";
}
ShellSort(arr, 10);
//换行输出排序后数组
std::cout << std::endl << "Array Sorted: " << std::endl;
for (int i = 0; i < 10; i++)
{
std::cout << arr[i] << " ";
}
}
四种基础排序算法性能对比
性能对比:
|------|------------------------------------|------------------------------------------|--------------------------------------|--------------------------------------|-----|
| | 空间复杂度 | 平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 稳定性 |
| 冒泡排序 | |
|
|
| 稳定 |
| 选择排序 | |
|
|
| 不稳定 |
| 插入排序 | |
|
|
| 稳定 |
| 希尔排序 | |
|
|
| 不稳定 |
其中插入排序效率最好,尤其在数据趋于有序的情况下,插入排序效率最高。而希尔排序是对插入排序的优化,在随机的序列中表现往往更好。
冒泡排序由于发生过多的交换,效率最差。选择排序因为减少了数据交换的次数,效率次之。
我们完成了四种排序算法的函数的编写,接下来在main函数中设计代码比较四种排序方法消耗的时间。可以自行修改COUNT=100000或其他值比较。
代码:
cpp
#include <iostream>
#include <random>
#include <ctime>
void BubbleSort(int arr[], int size)
{
for (int i = 0; i < size - 1; i++)
{
//优化代码:如果一次处理没有做过任何数据交换,则数组已经有序
bool flag = true; //flag表示没有做过交换
for (int j = 0; j < size - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
flag = false; //如果进入if语句,把flag置为false
}
}
if (flag) //如果没有做过交换,跳出循环
return;
}
}
void ChoiceSort(int arr[], int size)
{
//遍历 size - 1 次
for (int i = 0; i < size - 1; i++)
{
int min = i;
//每次从第 i + 1 个开始遍历,寻找最小值
for (int j = i + 1; j < size; j++)
{
if (arr[j] < arr[min])
min = j;
}
//找到最小值位置后,如果最小值不在遍历范围首位,与遍历的第一个元素交换
if (min != i)
{
int temp = arr[min];
arr[min] = arr[i];
arr[i] = temp;
}
}
}
void InsertSort(int arr[], int size)
{
for (int i = 1; i < size; i++)
{
int temp = arr[i];
int j = i - 1;
//寻找前序元素中小于等于temp的
for (; j >= 0; j--)
{
//如果元素小于等于temp,则跳出循环
if (arr[j] <= temp)
{
break;
}
//如果元素大于temp,则覆盖后面一位,相当于后移
arr[j + 1] = arr[j];
}
//跳出循环后,将temp放置在小于等于它的元素的下一位
//如果循环完成,此时的j = -1,temp会放在第一位
arr[j + 1] = temp;
}
}
void ShellSort(int arr[], int size)
{
//外层嵌套分组循环,以越来越小的间隔将元素分组
for (int gap = size / 2; gap > 0; gap = gap / 2)
{
//i从第一个分组的第二个元素开始遍历后面的元素,就可以对所有的分组进行排序
for (int i = gap; i < size; i++)
{
int temp = arr[i];
int j = i - gap; //同个分组中的上一个元素的位置
//寻找前序元素中小于等于temp的
for (; j >= 0; j = j - gap) //对同个分组中的前序元素依次遍历
{
//如果元素小于等于temp,则跳出循环
if (arr[j] <= temp)
{
break;
}
arr[j + gap] = arr[j]; //将元素在同个分组中前移
}
//循环结束后,将temp放在同组中标记元素的后一位
arr[j + gap] = temp;
}
}
}
int main()
{
const int COUNT = 10000;
//将数组定义在堆上
int* arr_b = new int[COUNT];
int* arr_c = new int[COUNT];
int* arr_i = new int[COUNT];
int* arr_s = new int[COUNT];
srand(time(NULL));
//初始化,对每个数组赋相同的随机数
for (int i = 0; i < COUNT; i++)
{
int val = rand() % COUNT;
arr_b[i] = val;
arr_c[i] = val;
arr_i[i] = val;
arr_s[i] = val;
}
//clock_t变量记录系统时间刻度
clock_t begin, end;
//冒泡排序消耗时间
begin = clock();
BubbleSort(arr_b, COUNT);
end = clock();
//计算clocks间隔,并将单位转换成秒
std::cout << "BubbleSort Spend Time: " << (end - begin) * 1.0 / CLOCKS_PER_SEC << "s" << std::endl;
//选择排序消耗时间
begin = clock();
ChoiceSort(arr_c, COUNT);
end = clock();
std::cout << "ChoiceSort Spend Time: " << (end - begin) * 1.0 / CLOCKS_PER_SEC << "s" << std::endl;
//插入排序消耗时间
begin = clock();
InsertSort(arr_i, COUNT);
end = clock();
std::cout << "InsertSort Spend Time: " << (end - begin) * 1.0 / CLOCKS_PER_SEC << "s" << std::endl;
//希尔排序消耗时间
begin = clock();
ShellSort(arr_s, COUNT);
end = clock();
std::cout << "ShellSort Spend Time: " << (end - begin) * 1.0 / CLOCKS_PER_SEC << "s" << std::endl;
}
输出:
BubbleSort Spend Time: 0.226s
ChoiceSort Spend Time: 0.101s
InsertSort Spend Time: 0.053s
ShellSort Spend Time: 0.002s
COUNT = 100000时的输出:
BubbleSort Spend Time: 23.41s
ChoiceSort Spend Time: 8.171s
InsertSort Spend Time: 4.862s
ShellSort Spend Time: 0.025s