【经典排序算法】选择排序、堆排序

时间:2022-05-13 11:06:51

1、选择排序

实现思想:首先在未排序序列中找到最小(大)元素,存放在排序序列的起始位置,然后再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾,以此类推,直到所有元素均排序完毕。通俗的讲就是从无序区(不同于插入排序,这里的无序区包含所有元素)选一个最小(大)的元素直接放在有序区的最后,即选择一个最小的元素依次放在前面,即选择最小(大)元素就有个逐一比较和交换的过程。

实现步骤:

设数组为a[0…n-1]

1. 初始时,数组全为无序区,为a[0…n-1]。令i = 0;

2. 在无序区a[i…n-1] 中选取一个最小的元素,将其与 a[i] 交换,交换之后a[0…i+1] 就形成了有序区,无序区为a[i+1…n-1];

3. i++,并重复第 2 步直到 i==n-1,排序完成。

上面的第 2 步是将数据进行交换,但每次只交换一次数据,选取最小元素,是保存最小的元素的索引值,然后交换无序区第一个元素与该索引指定的元素(无序区的最小值)。

void swap(int &a, int &b)
{
if (a != b)
{
a ^= b;
b ^= a;
a ^= b;
}
}

void SelectSort(int Array[], int length)
{
int i, j;
int Minindex;

for (i = 0; i < length - 1; ++i) //最后留下的元素自然是最大的
{
Minindex = i;
for (j = i + 1; j < length; ++j)
{
if (Array[j] < Array[Minindex])
Minindex = j; //更新最小值的索引
}
swap(Array[i], Array[Minindex]); //将最小值与无序区的第一个元素交换
}
}

从上面程序可知,选择排序的时间复杂度为O(N^2),空间复杂度为O(1)。选择排序是交换无序区的第一个元素和最小元素,如果当前元素是最小值,而当前这个小的元素的前面出现一个与无序区第一个元素相等的元素,那么交换之后,这相等的原本在前面的第一个元素就到了与之相等的那个元素的后面,举个例子:5 8 5 2 9,第一遍选择之后第一个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序是不稳定排序算法。

变相的选择排序(含选择排序思想):

void SelectSort(int Array[], int length)
{
for (int i = 0; i < length - 1; ++i)
for (int j = i + 1; j < length; ++j) //单次之后最小数据在最前面
{
if (Array[i] > Array[j])
swap(Array[i], Array[j]);
}
}

2、堆排序

堆排序是利用堆这种数据结构设计的一种排序算法,这里的堆是一个近似完全二叉树的结构,并同时满足堆序性质:在一个堆中,对于每一个节点 X,X 的父节点的关键字要大于等于(或小于等于)该节点的关键字,根节点除外。小于等于称之为最小堆,大于等于称之为最大堆。对于最大堆,其根节点关键字最大。

另外,完全二叉树很有规律,可以用一个数组表示而不需要指针,在起始位置为 0 的数组中任一位置 i 上的元素,其左儿子在位置 2*i+1 上,右儿子在位置 2*i+2 上,它的父节点则在位置 (i-1)/2 上。

堆排序的设计思想(升序):1)先将数组转换为最大堆(特殊的完全二叉树);2)提取根节点,与数组的最后一个元素交换,这样数组的最后一个元素就是最大值;3)然后再将除去最后一个元素的数组调整成最大堆;4)重复2,3步,直至数组无法组成最大堆,排序完毕。

所以堆排序的关键点落到了构建堆和调整堆上面了。这里的构建堆并不是额外构建堆这个数据结构,而是将数组中的元素进行交换,使之对应索引满足上面提及的规律,即在起始位置为 0 的数组中任一位置 i 上的元素,其左儿子在位置 2*i+1 上,右儿子在位置 2*i+2 上,它的父节点则在位置 (i-1)/2 上,满足其父节点大于等于其孩子节点。左右孩子节点并无要求只需小于其父节点即可。

1. 构建最大堆

如下图,节点前面表示编号,后面表示数值,假定输入数组为 Array[] = {6, 5, 3, 1, 7, 2}。数组拟合成最大堆形式,从底部向上,每次是比较交换具有 “血缘关系” 的父子节点,将这三(两)个节点中的最大值与父节点交换。在构建堆得过程中,我们是从完全二叉树的最下层最右边的非终端节点开始构建。首先找到数组尾部元素的父节点对应位置(这样保证每次定位的节点都有孩子节点),然后用该父节点与其孩子节点作比较交换,然后数组元素前移,数组所对应的完全二叉树中的元素位置也发生了变化,则再次比较交换该节点与其孩子节点,如此循环,直至前移到数组的首位置。

【经典排序算法】选择排序、堆排序

2. 调整最大堆

调整堆就是在交换数组首元素和无序区的末尾元素后,将无序区重新调整为最大堆,直至无序区消失,排序完成。

【经典排序算法】选择排序、堆排序

#define PARENT(x) ((x-1) >> 1)
#define LEFT(x) ((x << 1) + 1)
#define RIGHT(x) ((x << 1) + 2)

void swap(int &a, int &b)
{
if (a != b)
{
a ^= b;
b ^= a;
a ^= b;
}
}

//调整最大堆
void MaxHeapify(int Array[], int Index, int HeapSize)
{
int L = LEFT(Index); //Index为父节点索引
int R = RIGHT(Index);
int Largest;

//选择三(两个)个元素(“血缘关系”)的最大值
if (L <= HeapSize && Array[L] > Array[Index])
Largest = L;
else
Largest = Index;

if (R <= HeapSize && Array[R] > Array[Largest])
Largest = R;

if (Largest != Index)
{
swap(Array[Largest], Array[Index]); //最大值与父节点位置交换
MaxHeapify(Array, Largest, HeapSize);
/*这里的MaxHeapify()递归是用于最大堆的调整,从上至下调整,且只需调整一棵子树
构建最大堆是从数组尾端往前,并不需要递归,实际上构建时也不会出现递归调用*/
}
}

//构建最大堆
void BuildHeapify(int Array[], int HeapSize)
{
for (int i = PARENT(HeapSize); i >= 0; --i)
MaxHeapify(Array, i, HeapSize); //必须单步向前
}

void HeapSort(int Array[], int length)
{
int HeapSize = length - 1;

BuildHeapify(Array, HeapSize);

for (int i = HeapSize; i >= 0; --i)
{
swap(Array[0], Array[i]);
--HeapSize;
MaxHeapify(Array, 0, HeapSize);
//删除根节点后,,有一棵子树满足最大堆,有一棵不满足,只需调整一棵子树
}
}

构建堆时,对于每个父节点即非终端节点来说,最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(N);正式排序时,第 i 次取堆顶元素然后重建堆需要用O(logi) 时间(二叉树的深度),需要取 N-1 次堆顶元素,因此,取元素再重建堆的时间复杂度为O(NlogN)。所以总体来说堆排序的时间复杂度为O(NlogN)。值得注意的是,堆排序对原始输入序列的排序状态并不敏感,因此其各个时间复杂度均为O(NlogN)

在空间复杂度上,只有一个用来交换的暂存单元,因此空间复杂度为O(1)。另外可以得知堆排序的元素比较交换是跳跃式进行的,因此堆排序是一种不稳定的排序算法。

上面实现的是最大堆,另外也可是修改一下使之成为最小堆,只需要在调整的时候,改为将父节点与孩子节点比较后的最小值与父节点互换,然后再调整下一层(必须保证父节点均小于等于孩子节点),之后递减数组索引再次重复上述过程。与最大堆一样,调整的时候需要从上往下逐层调整。