关于排序的稳定性
8种常见排序中(快排、希尔、堆排、归并、冒泡、选择、插入、基数排序)
快些(希尔)选堆不稳定,剩下的稳定
冒泡排序
普通冒泡排序要点
- 两个嵌套的循环
- 第一个循环 i 完成一轮就排序好一个元素,排序好的元素从后放置,如果总元素有 n 个,那么需要排序 n-1 个后就排序完成
- 第二个循环 j 用于遍历未排序完成的元素并进行比较,由于比较的为第 j 个和第 j+1 个元素,所以 j 范围只能从 0 到 n-1,也就是 j<n-1,这样 j+1 就刚好可以取到第 n-1 个,总共遍历了 n个元素
- j 的范围还可以进一步缩小,这是因为在第 i 轮循环时,有 i 个元素已经被排序好且放置在最后了,所以 j 可以不遍历这部分已经排序好的元素,故在 j<n-1的基础上去掉 i 个元素,也就是 j<n-i-1
- 使用标志 flag,当此轮外循环后没有交换,则数组绝对有序,不用再排序,即可退出排序
void bubbleSort(int arr[], int n)
{
bool flag;
for(int i=0; i<n-1; ++i){
flag = false;
for(int j=0; j<n-i-1; ++j){
if(arr[j] > arr[j+1]){
swapArrEle(&arr[j], &arr[j+1]);
flag = true;
}
count++;
}
if(flag == false){
break;
}
}
}
冒泡排序的进一步改进
- 和上文一样,使用 flag,在外循环没有发生交换时即认为数组有序
- 这次改进了内循环的范围,在冒泡排序中,外循环执行到第 k 轮,后面就有 k 个元素是必定有序的,但是有序的元素并不一定刚刚好是 k 个,可能是大于 k 个的,因此内循环可以不遍历所有已经有序的元素。
- 具体做法是,记录最后一个没有发生排序的元素的下标作为内循环范围,此下标等于最后一次发生交换的位置下标。原因:如果第 i 个下标发生了交换,而第 i+1 个下标往后没有发生交换,则 i+1 往后(包括本身)都是有序的,而第 i 个发生了交换,不一定是有序的,视作最后一个没有排序的下标
void bubbleSort2(int arr[], int n)
{
bool swapped = true;
int lastUnsortedEleIndex = n-1;//最后一个没有排序的元素下标
int swappedIndex = -1;//上次发生交换的位置
while(swapped){
swapped = false;
for(int i=0; i<lastUnsortedEleIndex; i++){
if(arr[i]>arr[i+1]){
swapArrEle(&arr[i], &arr[i+1]);
swapped = true;//表示发生了交换
swappedIndex = i;//更新交换的位置
}
count++;
}
//最后一个没有经过排序的元素的下标就是最后一次发生交换的位置
lastUnsortedEleIndex = swappedIndex;
}
}
性能分析
- 上文代码使用了count记录进入内循环的次数,经笔者测试,改进的代码进入内循环的次数在一些情况下会比较少
- 但是时间上,改进的代码不一定少,可能是因为改进代码中,在交换后需要更多的操作来记录交换的下标等信息
- 测试代码:
int main() {
int arr[] = {45, 23, 67, 12, 89, 34, 78, 54, 91, 27,
63, 16, 72, 39, 86, 56, 94, 31, 69, 42,
83, 50, 98, 37, 76, 60, 97, 28, 65, 19,
74, 47, 88, 33, 79, 58, 93, 36, 71, 51,
95, 30, 68, 22, 85, 53, 90, 26, 62, 15,
73, 40, 87, 55, 92, 32, 70, 49, 96, 29,
66, 18, 75, 44, 80, 57, 99, 38, 77, 61,
24, 64, 20, 25, 46, 81, 52, 17, 35, 59,
82, 48, 42, 67, 11, 86, 92, 39, 78, 54,
91, 26, 73, 19, 68, 46, 83, 42, 79, 35
};
int n = sizeof(arr)/sizeof(arr[0]);
cout << "排序前:" << endl;
printArr(arr, n);
bubbleSort(arr, n);
cout << "排序后:" << endl;
printArr(arr, n);
cout << count << endl << endl;
int arr2[] = {45, 23, 67, 12, 89, 34, 78, 54, 91, 27,
63, 16, 72, 39, 86, 56, 94, 31, 69, 42,
83, 50, 98, 37, 76, 60, 97, 28, 65, 19,
74, 47, 88, 33, 79, 58, 93, 36, 71, 51,
95, 30, 68, 22, 85, 53, 90, 26, 62, 15,
73, 40, 87, 55, 92, 32, 70, 49, 96, 29,
66, 18, 75, 44, 80, 57, 99, 38, 77, 61,
24, 64, 20, 25, 46, 81, 52, 17, 35, 59,
82, 48, 42, 67, 11, 86, 92, 39, 78, 54,
91, 26, 73, 19, 68, 46, 83, 42, 79, 35
};
count = 0;
int n2 = sizeof(arr2)/sizeof(arr2[0]);
cout << "改进排序前:" << endl;
printArr(arr2, n2);
bubbleSort2(arr2, n2);
cout << "改进排序后:" << endl;
printArr(arr2, n2);
cout << count << endl << endl;
return 0;
}
选择排序
要点
- 同冒泡排序一致,排序 length - 1 元素则 length 个元素都排序好,所以外循环是 n-1 次,第 i 次外循环则有 i 个元素在数组前排序好
- 内循环则是找出最小元素的下标
void selectSort(int arr[], int n)
{
int minIndex;
for(int i=0; i<n-1; ++i){
minIndex = i;
for(int j=i+1; j<n; ++j){
if(arr[j]<arr[minIndex]){
minIndex = j;
}
count++;
}
if(minIndex != i){
swapArrEle(&arr[i], &arr[minIndex]);
}
}
}
改进:二元选择排序
- 一个内循环同时找出最大值下标和最小值下标
- 排序好的元素分别放在两端
- 当找到的最小值下标与最大值下标相同时,结束外循环。原因:内循环遍历未排序完成的元素时,只有一种情况会使这两种下标相同,也就是未排序部分元素相同,此时这些相同元素也放置在正确的位置上,排序完成
- 先放置最小值下标后,如果最大值的下标是i,而i刚才与最小下标被交换了,所以要更正最大值下标
void selectSort2(int arr[], int n)
{
int minIndex;
int maxIndex;
for(int i=0; i<n/2; ++i){
minIndex = i;
maxIndex = n-1-i;
for(int j=i+1; j<n-i; ++j){
if(arr[j] < arr[minIndex]) minIndex = j;
if(arr[j] > arr[maxIndex]) maxIndex = j;
count++;
}
if(minIndex == maxIndex) break;
swapArrEle(&arr[i], &arr[minIndex]);
//如果最大值的下标是i,而i刚才与最小下标被交换了,所以要更正最大值下标
if(maxIndex == i) maxIndex = minIndex;
swapArrEle(&arr[n-1-i], &arr[maxIndex]);
}
}
插入排序
交换法插入排序要点
- 两层循环
- 外循环从第二个数开始遍历
- 内循环将本次外循环遍历到的数依次与前面的数字比较,符合条件的就交换
- 由于是交换,所以无需其他内存空间,但是缺点是本次交换到的位置并非总是最合适的,会发生多次无效交换
//交换法插入排序
void insertSort(int arr[], int n)
{
for(int i=1; i<n; ++i){//从第二个数开始与前面比较并插入
int j=i;//j作为本轮遍历用的下标
while(arr[j]<arr[j-1] && j>=1){
swapArrEle(&arr[j], &arr[j-1]);
--j;
}
}
}
移动法插入排序要点
- 为了不重复交换,核心思想改为与前面的元素比较,比其大的就往后挪,直到挪出一个合适的位置就插入
- 所以需要额外内存来记录本轮需要比较的数字
//移动法插入排序
void insertSort2(int arr[], int n)
{
for(int i=1; i<n; ++i){
int j=i;
int currEle = arr[i];
while(j>=1 && currEle<arr[j-1]){
arr[j] = arr[j-1];
--j;
}
arr[j] = currEle;
}
}
冒泡、选择、插入排序比较
- 在这三种排序中,选择排序交换次数是最少的
- 在数组几乎有序的情况下,插入排序的时间复杂度最接近线性级别
两数交换不使用临时变量
如果考虑避免移除,应使用按位异或运算:
arr[j] = arr[j] ^ arr[j-1];
arr[j-1] = arr[j] ^ arr[j-1];
arr[j] = arr[j] ^ arr[j-1];