前言
在排序算法中,选择类排序(Selection Sort)是一类简单但有效的排序方法。它基于不断选择未排序部分的最小或最大元素,并将其放置在已排序部分的末尾,从而逐步构建有序序列的思想。虽然选择类排序算法的时间复杂度较高,但由于其直观、易于理解和实现的特点,它在某些特定场景下仍然具有一定的应用价值。
简单选择排序
简单选择排序(Simple Selection Sort)也称作直接选择排序,是一种基于比较的排序算法,其基本思想是每次从待排序序列中选择最小(大)的元素,放到已排序序列的末尾,直至整个序列排好序为止。
基本流程:
- 假设待排序序列为arr[],使用双重循环来进行简单选择排序。
- 第i轮选择时,从该序列中的[i,n-1]范围内选择最小值,k记录其下标。
- 将最小值元素arr[k]与arr[i]交换位置
- 重复2、3操作,直至完成全部n-1轮选择操作。
以下是简单选择排序的C语言实现:
void SelectSort(int arr[], int arrsize) {
int i, j, k, temp;
for (i = 0; i < arrsize; i++) {
k = i;
//找到该趟最小的元素,k储存其下标
for (j = i + 1; j < arrsize; j++)
if (arr[j] < arr[k]) k = j;
if (i != k) {
//交换arr[i]与arr[k]
temp = arr[i];
arr[i] = arr[k];
arr[k] = temp;
}
}
}
算法分析:
时间复杂度:
需要进行n-1轮选择和n-1次交换操作,所以时间复杂度为O(n^2),其中n为待排序序列的长度。
空间复杂度:
只在两元素交换时需要一个辅助空间,所以空间复杂度为O(1)。
堆排序
堆(Heap)是一种特殊的树状数据结构,它通常被用来实现优先队列。堆分为大根堆和小根堆两种类型。
- 大根堆(Max Heap)中,对于每个节点i,它的左右子节点j和k上存储的数据必须都小于等于节点i上的数据。
- 小根堆(Min Heap)中,对于每个节点i,它的左右子节点j和k上存储的数据必须都大于等于节点i上的数据。
堆具有以下性质:
- 堆总是一棵完全二叉树。
- 对于大根堆(或小根堆),任一节点的值都大于等于(或小于等于)其子节点的值。
- 堆中每个节点的子树均符合堆的定义。
堆最常见的操作包括:
- 插入一个元素。
- 删除最值(即大根堆中删除最大值,小根堆中删除最小值)。
- 查看最值。
堆排序(Heap Sort)就是利用堆这种数据结构进行排序的方法,其基本思路是将待排序序列构成一个大根堆(或小根堆),然后不断地将堆顶元素取出,直到堆为空时得到一个有序序列。一般升序排序会用到大根堆,降序排序会用到小根堆。
基本流程:
1.构建最大堆或最小堆:
- 首先,将待排序的数组看作是一个完全二叉树。
- 从最后一个非叶子节点开始,依次向上对每个节点进行向下调整操作,使得该节点及其子树满足堆的性质。这个过程称为建堆或堆化(Heapify)操作。
- 经过建堆操作后,数组便成为一个最大堆或最小堆。
2.排序阶段:
- 将堆顶元素(最大堆的情况下为最大值,最小堆的情况下为最小值)与堆中最后一个元素交换位置,并将堆的大小减1。
- 对交换后的堆顶元素进行向下调整操作,将其与子节点进行比较并交换位置,以维持堆的性质。
- 重复执行以上步骤,直到堆为空或只剩下一个元素。
3.完成排序:
- 这样循环执行交换和向下调整的过程,直到所有的元素都被取出并放置在正确的位置上。
- 最终得到的数组就是按照升序(最大堆)或降序(最小堆)排列的结果。
调整堆、建立堆以及使用堆排序算法是堆排序的核心操作,以下是堆排序的C语言实现:
//交换两元素
void swap(int* a, int* b) {
int t = *a;
*a = *b;
*b = t;
}
/*堆的调整分为向上调整和向下调整,向上调整一般在插入元素到堆中时使用,
向下调整一般在删除堆顶元素或修改堆顶元素后使用。建堆可以使用向上调
整来实现,也可以向下调整实现,这里我们只用向下调整来实现堆排序*/
//调整堆(向下调整)
void AdjustHeap(int arr[], int arrsize, int i) {
//i为当前节点下标
int right = 2 * (i + 1);//i的右子节点下标
int left = right - 1;//i的左子节点下标
int maxidx = i;//初始化节点i为最大值
//找到三个节点中的最大值,更新maxidx
if (left<arrsize && arr[left]>arr[maxidx]) {
maxidx = left;
}
if (right<arrsize && arr[right]>arr[maxidx]) {
maxidx = right;
}
//若节点i不是最大值,则与最大值节点交换位置,递归向下调整
if (maxidx != i) {
swap(&arr[i], &arr[maxidx]);
AdjustHeap(arr, arrsize, maxidx);
}
}
//建堆
void BuildHeap(int arr[], int arrsize) {
//从下标为n/2-1的节点(第一个非叶子结点)开始,向前逐个操作
for (int i = arrsize / 2 - 1; i >= 0; i--) {
AdjustHeap(arr, arrsize, i);
}
}
//堆排序
void HeapSort(int arr[], int arrsize) {
BuildHeap(arr, arrsize);//建立初堆
for (int i = arrsize - 1; i > 0; i--) {
swap(&arr[0], &arr[i]);//将堆顶元素与数组最后一个元素互换
AdjustHeap(arr, i, 0);//将arr[0,i-1]重新调整为大根堆
}
}
算法分析:
时间复杂度:
- 建堆操作的时间复杂度为O(n),其中n是待排序数组的长度。
- 执行堆排序的过程需要进行n-1次堆重建(包括交换堆顶元素和堆的最后一个元素以及向下调整操作),每次堆重建的时间复杂度为O(logn)。
- 因此,堆排序的时间复杂度为O(nlogn)。
空间复杂度:
堆排序需要使用一个额外的堆数据结构来存储待排序的元素,并进行堆的构建和调整操作。因此,空间复杂度为O(n)。
总结
简单选择排序和堆排序都是不稳定的排序算法。
- 在简单选择排序中,每次选择最小(或最大)元素进行交换时,可能会改变相同元素之间的相对顺序,因此简单选择排序是一种不稳定的排序算法。
- 在堆排序过程中,通过构建最大堆或最小堆来实现排序。在每一次交换堆顶元素和末尾元素的操作中,可能会打破相同元素的相对顺序,导致堆排序的结果不稳定。
简单选择排序可用于链式储存结构,堆排序只能用于顺序结构。
无论是选择排序、堆排序还是其他排序算法,选择合适的排序方法取决于具体的应用场景和需求。了解不同类型的排序算法可以帮助我们更好地理解和选择合适的排序策略,以提高程序的效率和性能。无论采用何种算法,排序的目标始终是将数据按照指定的顺序进行整理,为后续操作提供有序、便捷的数据结构。