目录
选择排序
选择排序是一种非常简单的排序,多次遍历数组,每次遍历时选出最小的数放到数组的头上就行了(每次选出的最小数不参与下一次遍历)。
我们这里做个优化,每次遍历的时候选出最小和最大的数,分别放头和尾。这样每次选可以选出两个数,使效率略有提升。
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void Selectsort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
//选出小的数放begin位置
//选出大的数放end位置
int mini = begin, maxi = begin;
for (int i = begin + 1; i <= end; ++i)
{
if (a[i] > a[maxi])
maxi = i;
if (a[i] < a[mini])
mini = i;
}
swap(&a[begin], &a[mini]);
//修正maxi
if (maxi == begin)
{
maxi = mini;
}
swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
int main()
{
int a[] = { 100,1,4,65,11,15,46,23,60,12,9 };
int sz = sizeof(a) / sizeof(a[0]);
Selectsort(a, sz);
for (int i = 0; i < sz; ++i)
{
printf("%d ", a[i]);
}
return 0;
}
中间选择的过程都很简单,注意修正maxi这一块,如果最大的数一开始 就在头上,不进行修正会出现错误。 通过调试来看:
a[0] 是最大的数100,没有数能和他交换,所以begin和mini交换后(注意此时a[0] 和 a[1]已经交换,100不在头上了),要交换end和maxi,交换到最后去的就不是最大的100而是其他数了,所以end和maxi在交换前,加一句修正,如果一开始头上的数是最大的,那么begin和mini交换完后将mini的位置给maxi就可以了。
选择排序可以说是最差的排序之一了,时间复杂度为O(N^2),并且无论最好情况还是最坏情况都是O(N^2),也就是说不管有序无序,都那么慢,比起上节讲的插入排序要差,插排起码序列部分有序的情况下效率还可以。
堆排序
实现堆排序之前需要先写一个堆出来吗?————没有必要,如果需要先实现一个堆就太过麻烦了,而且这种方式空间复杂度就变成了O(N),原本只需要在数组中选数,现在将数据传入堆里,需要开辟额外空间。所以我们直接将要排序的数组建堆,再选数。这里的原数组基础上建堆就是模拟建堆的过程
这里有两种思路:
一、向上调整模拟建堆
void HeapSortUp(int* a, int n)
{
for (int i = 1; i < n; ++i)
{
AdjustUp(a, i);
}
}
int main()
{
int a[] = { 6,2,5,4,13,7,9,16,56 };
int n = sizeof(a) / sizeof(a[0]);
HeapSortUp(a, n);
int i = 0;
for (i = 0; i < n; ++i)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
这种建堆方式时间复杂度是O(N*logN)
不是很推荐,我们还有更好的方法。
二、向下调整模拟建堆
void AdjustDown(HPDataType* a, int n, int parent)
{
int minchild = parent * 2 + 1;
while (minchild < n)
{
if (minchild + 1 < n && a[minchild] < a[minchild + 1])
{
minchild++;
}
if (a[minchild] > a[parent])
{
swap(&a[minchild], &a[parent]);
parent = minchild;
minchild = parent * 2 + 1;
}
else
break;
}
}
void HeapSortDown(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
}
int main()
{
int a[] = { 6,2,5,4,13,7,9,16,56 };
int n = sizeof(a) / sizeof(a[0]);
int i = 0;
HeapSortDown(a, n);
for (i = 0; i < n; ++i)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
这种方式时间复杂度是O(N)。
所以建堆的时候一般选用向下调整建堆的方法。
证明时间复杂度:
向下调整:
向上调整:
与向下调整一样,这里就不算了,其实很容易想,二叉树越往下节点越多,最后一层就占了一半,向下调整最后一层调整h-1次,比向上调整多了太多,因此时间复杂度大。
堆排序选数
完成模拟建堆后,就是进行排序选数了,这里有个非常关键的点:如果要排成升序,是建大堆还是建小堆?降序又如何呢?
这是个非常容易出错的点,可能大家会觉得要排升序,小的数在前面,那就建小堆,降序大的数在前面,那就建大堆,其实恰恰相反,升序是建大堆,降序是建小堆。
我来证明一下:以升序为例,假设数组是这样的:2,4,5,6,13,7,9,16,56
模拟建成小堆
这时选出次小的数4,堆的形态就发生了变化:
本来4和5是兄弟,现在成了父子,关系全乱了,此时只能重新建堆选数,那么堆排序就没有任何意义了。因此得反过来建大堆。
模拟建成大堆:
这时将堆顶的数56 和最后一个数2 交换,然后不算最后一个数56,向下调整选出次大的(在堆顶),再重复上面操作,就能排出升序了,实际上就是倒着将数组排出来,降序也一样。
代码如下:
typedef int HPDataType;
void AdjustDown(HPDataType* a, int n, int parent)
{
int minchild = parent * 2 + 1;
while (minchild < n)
{
if (minchild + 1 < n && a[minchild] < a[minchild + 1])
{
minchild++;
}
if (a[minchild] > a[parent])
{
swap(&a[minchild], &a[parent]);
parent = minchild;
minchild = parent * 2 + 1;
}
else
break;
}
}
void HeapSortDown(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
int i = 1;
while (i < n)
{
swap(&a[0], &a[n - i]);
AdjustDown(a, n-i, 0);
i++;
}
}
int main()
{
int a[] = { 6,2,5,4,13,7,9,16,56 };
int n = sizeof(a) / sizeof(a[0]);
int i = 0;
HeapSortDown(a, n);
for (i = 0; i < n; ++i)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
降序也一样,改下建小堆就可以了。
冒泡排序
冒泡排序也非常简单,大家接触应该也非常多,直接上代码(略加点优化)
void swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void Bubblesort(int* a, int n)
{
for (int j = 0; j < n; ++j)
{
int exchange = 0;
for (int i = 1; i < n - j; ++i)
{
if (a[i - 1] > a[i])
{
swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
int main()
{
int a[] = { 100,1,4,65,11,15,46,23,60,12,9 };
int sz = sizeof(a) / sizeof(a[0]);
Bubblesort(a, sz);
for (int i = 0; i < sz; ++i)
{
printf("%d ", a[i]);
}
return 0;
}