选择排序的基本思想是,在待排序序列A[p...n-1]
中选择一个最值加入到A[0...p-1]
中,形成更长的有序序列A[0...p]
,初始时有序表仅有一个元素,对于表长为n
的表,经过n-1
次选择即可排序完成,针对选择最值方式的不同,有两种基本的算法,一个是简单选择排序(暴力选择),一个是堆排序(基于数据结构堆)
对于简单选择排序,实现方式非常简单,有如下实现
#include <limits.h>
void simple_Select(int A[], int length)
{
for (int i = 0; i < length - 1; ++i)
{
int min_index = -1;
int min_val = INT_MAX;
for (int j = length - 1; j >= i; --j)
{
if (A[j] < min_val)
{
min_val = A[j];
min_index = j;
}
}
A[min_index] = A[i];
A[i] = min_val;
}
}
定义堆具有如下性质:
-
是完全二叉树
-
所有分支节点的值大于(小于)其左、右孩子(若存在)的值称之为大(小)顶堆
调整堆的方法是从上向下筛选,也就是说,若分支节点不满足堆的定义,就将这个结点与孩子节点交换,其中若为大顶堆要求这个孩子节点具有最大值,小顶堆则是取具有最小值的孩子结点交换,如此往复直到这个向下筛选的结点满足堆定义
初始化一个堆则从最后一个分支结点开始到完全二叉树根节点,依次调整即可
而堆排序就是先将待排序序列初始化为堆,然后交换第一个和最后一个结点(删除堆顶元素),此时堆规模减一,然后重新调整,对于表长为n
的无序表只需执行n-1次交换即可完成排序
于是我们有如下实现
/*完全二叉树下标与结点父子关系的一个简单例子
0
/ \
1 2
/ \ / \
3 4 5 6
(1 - 1) / 2 = 0 (2 - 1) / 2 = 0
(3 - 1) / 2 = 1 (4 - 1) / 2 = 1
(5 - 1) / 2 = 2 (6 - 1) / 2 = 2
parent = (pos - 1) / 2
lchild = 2 * i + 1, rchild = lchild + 1
*/
// cannot be called by user
void _adjust_MaxHeap(int A[], int pos, int length)
{
int adj_val = A[pos];
int i = pos, k;
while (i < length) //事实上只要输入合法,是不会出现越界访问的,i的有效性由while循环内if语句保证,因此这里其实可以不用这么写,直接写个true也行
{
int adjusted = 0;
k = i * 2 + 1;
if (k + 1 < length && A[k] < A[k + 1])
++k;
if (k < length && adj_val < A[k])
{
A[i] = A[k];
i = k;
adjusted = 1;
} //这里可以不用adjusted变量,只需要else语句块执行break即可,因为没有执行if语句块表明调整完毕,直接退出循环
if (!adjusted)
{
A[i] = adj_val;
break;
}
}
}
// cannot be called by user
void _create_MaxHeap(int A[], int length) //建堆从最后一个分支节点开始调整
{
for (int i = 0; i <= (length - 2) / 2; ++i)
_adjust_MaxHeap(A, (length - 2) / 2 - i, length);
}
void heap_Sort(int A[], int length)
{
_create_MaxHeap(A, length);
for (int i = length - 1; i > 0; --i)
{
int swap_val = A[i];
A[i] = A[0];
A[0] = swap_val;
_adjust_MaxHeap(A, 0, i); //注意参数传递,第二个参数是堆数组的首元素下标,第二个参数是堆的规模
}
}
最后,堆排序的时间复杂度是O(nlogn)
,这是因为建堆是线性时间,排序时每次筛选最小值是对数时间,会筛选n-1
次(假设表长为n
)
另外,堆的插入是插到堆尾,然后自下往上调整(不是向下筛选!)