【数据结构学习记录27】——选择排序和堆排序

一.选择排序

1.原理

选择排序比较简单,就是每一次遍历后,将无序区的一个最大(最小)值与无序区的第一个值或最后一个值交换,这样,无序区渐渐的变为有序,最终完成排序。它的时间复杂度也是 O ( n 2 ) O(n^2) O(n2)

2.过程

在这里插入图片描述

3.代码

int selectsort(int arry[], int len)
{
    
    
    int min, i, j, temp;

    for(i = 0; i < len - 1; ++i)
    {
    
    
        min = i;
        for (j = i; j < len; ++j)
        {
    
    
            if (arry[j] < arry[min])
            {
    
    
                min = j;
            }
        }
        temp = arry[i];
        arry[i] = arry[min];
        arry[min] = temp;
    }
}

二.堆排序

1.堆的概念

假设序列 k 1 , k 2 , ⋅ ⋅ ⋅ , k n {k_1,k_2,\cdot\cdot\cdot,k_n} k1,k2,,kn有且仅当满足以下关系的时候,我们叫它堆:
{ k i ≤ k 2 i , k i ≤ k 2 i + 1 或 { k i ≥ k 2 i , k i ≥ k 2 i + 1 其 中 ( i = 1 , 2 , ⋅ ⋅ ⋅ , n 2 ) \begin{cases} k_i ≤ k_{2i}, \\ k_i ≤ k_{2i+1} \end{cases} 或 \begin{cases} k_i ≥ k_{2i}, \\ k_i ≥ k_{2i+1} \end{cases} 其中(i = 1,2,\cdot\cdot\cdot,\frac{n}{2}) { kik2i,kik2i+1{ kik2i,kik2i+1(i=1,2,,2n)
注意,顺序序列编号是从1开始的。
前者是小于等于关系,所以我们又称它为:小根堆
后者是大于等于关系,所以我们又称它为:大根堆

2.堆与完全二叉树的关系

那么如果我们通过这个序列(数组)来模拟完全二叉树的话,在树状结构下,堆有以下性质:
每个父节点均大于(小于)它的儿子结点,看图:
假设有这个小根堆:
在这里插入图片描述

把它"转换"成完全二叉树的话为:
在这里插入图片描述
不难看出,结点n的孩子结点为2n和2n+1

3.堆排序的过程

我们现在知道了,这个堆二叉树(顺序序列模拟的)的根结点一定是最大(小)的,那么该元素一定是有序的状态。那么我们排序的思路应该为:

  1. 构造堆
  2. 除外根结点元素(将堆顶元素除外或者与最后一个元素交换后并除外)
  3. 重新调整结构,使剩余元素又构成一个堆
  4. 重复2、3直到所有元素有序

当然,在第2点中,为了使得基本树形结构不被受到破坏,所以我们选择将堆顶元素与末尾元素交换后,并将末尾元素划为有序区,也就是除外,不参与第3点的重新调整结构。
所以,在此基础上,我们升序一般就采用大根堆,降序采用小根堆
经过复杂的推导,它的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

4.构造堆

堆的初始化,我们需要以下几个步骤:
对所有双亲结点进行以下调整堆操作:

  1. 若孩子结点均小于父结点,则结束。
  2. 在非1的情况下:父节点与孩子结点的最大值交换,并对交换后的孩子结点进行1、2的操作。

我们知道,假设一棵满叶子的完全二叉树,设它有n个结点,那么叶子结点数量 ( n + 1 ) 2 \frac{(n+1)}{2} 2(n+1),则父结点有 ( n − 1 ) 2 \frac{(n-1)}{2} 2(n1)
然后,假设我们初始化堆,我们就从最后一个父结点开始,向前(从右到左,从下到上)开始循环构造堆。
从代码上来看就是:

    // 从最后一个父结点开始,将所有结点给调整一次。
    for (i = len/2; i > 0; --i)
    {
    
    
        HeapAdjust(heapArry, i, len);
    }

5.调整堆

我们调整堆的话,思路以及说了:

  1. 若孩子结点均小于父结点,则结束。
  2. 在非1的情况下:父节点与孩子结点的最大值交换,并对交换后的孩子结点进行1、2的操作。

代码如下:

int HeapAdjust(int arry[], int index, int len)
{
    
    
    int i;
    arry[0] = arry[index]; // arry[0] 没被使用,刚好可以拿来当交换时的临时变量用
    for (i = 2*index; i <= len; i*=2)   // 从该结点的左孩子开始,且每次循环都直接到孩子的孩子,且i肯定不能超过树的大小
    {
    
    
        if (i < len && arry[i] < arry[i+1]) // 这里是判断i当前孩子是左孩子大还是右孩子大,将i指向最大的孩子
        {
    
    
            ++i;
        }
        // 把上者<改成>,下者>=改为<=,则该堆排序变成降序
        if (arry[0] >= arry[i]) // 如果我们的处理的结点大于了孩子结点,那么就必须交换。
        {
    
    
            break;
        }
        else
        {
    
    
            arry[index] = arry[i];  // 我们处理的结点被孩子顶替
            index = i;              // 我们新处理的结点变成了被顶替的孩子位置
        }
    }
    arry[index] = arry[0];  // 一直到了最后,孩子一直顶替,直到没法顶替了,就是我们最开始待处理结点的位置
}

图例:
假设我们只差根结点未调整了,现在要对index=1的结点进行调整:
在这里插入图片描述
首先进来,确定我们的元素状态:5是待调整,7是当前指向儿子:
在这里插入图片描述
此时,[0]<[i],也就是5<7,因该发生调整,所以[1]因该等于[2],然后待调整的从[1]变到了[2],指向的儿子又变到了[4]:
在这里插入图片描述
此时,[0]<[i],也就是5<6,因该发生调整,所以[2]因该等于[4],然后待调整的从[2]变到了[4],指向的儿子又变到了[8](不存在):
在这里插入图片描述

那么退出循环后,我们就确定好了待调整位置index=4,所以讲temp也就是arry[0]的值给arry[4]:
在这里插入图片描述

这样就完成了一次堆调整。

6.代码

具体的过程可以自己中途输出HeapSort来看堆排序与构造的过程。

#include <stdio.h>
#include <stdlib.h>

int HeapAdjust(int arry[], int index, int len)
{
    
    
    int i;
    arry[0] = arry[index]; // arry[0] 没被使用,刚好可以拿来当交换时的临时变量用
    for (i = 2*index; i <= len; i*=2)   // 从该结点的左孩子开始,且每次循环都直接到孩子的孩子,且i肯定不能超过树的大小
    {
    
    
        if (i < len && arry[i] < arry[i+1]) // 这里是判断i当前孩子是左孩子大还是右孩子大,将i指向最大的孩子
        {
    
    
            ++i;
        }
        // 把上者<改成>,下者>=改为<=,则该堆排序变成降序
        if (arry[0] >= arry[i]) // 如果我们的处理的结点大于了孩子结点,那么就必须交换。
        {
    
    
            break;
        }
        else
        {
    
    
            arry[index] = arry[i];  // 我们处理的结点被孩子顶替
            index = i;              // 我们新处理的结点变成了被顶替的孩子位置
        }
    }
    arry[index] = arry[0];  // 一直到了最后,孩子一直顶替,直到没法顶替了,就是我们最开始待处理结点的位置
}

int HeapSort(int arry[], int len)
{
    
    
    int i = 1;

    int *heapArry = (int*)malloc(sizeof(int) * (len+1));    // 构造一个从下标1开始的序列。
    for (i = 1; i <= len; ++i)
    {
    
       
        heapArry[i] = arry[i - 1];
    }

    // 从最后一个父结点开始,将所有结点给调整一次。
    for (i = len/2; i > 0; --i)
    {
    
    
        HeapAdjust(heapArry, i, len);
    }

    // 堆排序。
    for (i = len; i > 0; --i)
    {
    
    
        arry[i-1] = heapArry[1];    // 堆顶是我们的最大元素,赋值给原数组
        heapArry[1] = heapArry[i];  // 因为是交换,所以要把最后一个元素给堆顶,堆顶给最后一个元素(有序),但我们可以舍弃这个保存,因为存到了老数组里
        HeapAdjust(heapArry, 1, i - 1); // i之后的结点是有序的(尽管没有赋值),所以不参与堆的构造
    }
}

int main()
{
    
    
    int a[7] = {
    
    5,6,3,7,2,1,4};
    int i;
    HeapSort(a, 7);
    for (i = 0; i < 7; ++i)
    {
    
    
        printf("%d ", a[i]);
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/u011017694/article/details/111504356