数据结构 | 排序问题之选择排序

选择排序

Posted by Aiden on April 15, 2018

1. 简单选择排序

选择排序(Selection Sort)是一种简单直观的排序算法。

它的工作原理如下: 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。 选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的序列进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

实例分析

以数组 arr=[8, 5, 2, 6, 9, 3, 1, 4, 0, 7] 为例,先直观看一下每一步的变化,后面再介绍细节

第一次从数组 [8, 5, 2, 6, 9, 3, 1, 4, 0, 7] 中找到最小的数 0,放到数组的最前面(与第一个元素进行交换)

                              min                              ↓
8   5   2   6   9   3   1   4   0   7
↑                               ↑
└───────────────────────────────┘

交换后:

0   5   2   6   9   3   1   4   8   7

在剩余的序列中 [5, 2, 6, 9, 3, 1, 4, 8, 7] 中找到最小的数 1,与该序列的第一个个元素进行位置交换:

                       min
                        ↓
0   5   2   6   9   3   1   4   8   7
    ↑                   ↑
    └───────────────────┘

交换后:

0   1   2   6   9   3   5   4   8   7

在剩余的序列中 [2, 6, 9, 3, 5, 4, 8, 7] 中找到最小的数 2,与该序列的第一个个元素进行位置交换(实际上不需要交换):

       min
        ↓
0   1   2   6   9   3   5   4   8   7
        ↑

重复上述过程,直到最后一个元素就完成了排序。

                   min
                    ↓
0   1   2   6   9   3   5   4   8   7
            ↑       ↑
            └───────┘
                           min
                            ↓
0   1   2   3   9   6   5   4   8   7
                ↑           ↑
                └───────────┘
                       min
                        ↓
0   1   2   3   4   6   5   9   8   7
                    ↑   ↑
                    └───┘
                       min
                        ↓
0   1   2   3   4   5   6   9   8   7
                        ↑   
                                   min
                                    ↓
0   1   2   3   4   5   6   9   8   7
                            ↑       ↑
                            └───────┘  
                               min
                                ↓
0   1   2   3   4   5   6   7   8   9
                                ↑      
                                   min
                                    ↓
0   1   2   3   4   5   6   7   8   9
                                    ↑

代码分析:

#include <iostream>
 using namespace std;

void SelectSort(int *pData,int size)
{
    for(int i = 0;i<size-1;++i)
    {
        int index = i;
        for(int j = i+1;j<size;++j)
        {
            if(pData[j]<pData[index])
                index = j;
        }
        if(index != i)
        {
            int temp = pData[i];
            pData[i] = pData[index];
            pData[index] = temp;
        }
    }
}

int main()
{
    int pData[10]={1,5,9,3,4,7,8,2,6,10};
    for(int i = 0;i<10;++i)
        cout<<pData[i]<<' ';
    cout<<endl;
    SelectSort(pData,10);
    for(int i = 0;i<10;++i)
        cout<<pData[i]<<' ';

    return 0;
}

2. 堆排序

作为选择排序的改进版,堆排序可以把每一趟元素的比较结果保存下来,以便我们在选择最小/大元素时对已经比较过的元素做出相应的调整。

堆排序是一种树形选择排序,在排序过程中可以把元素看成是一颗完全二叉树,每个节点都大(小)于它的两个子节点

当每个节点都大于等于它的两个子节点时,就称为大顶堆,也叫堆有序; 当每个节点都小于等于它的两个子节点时,就称为小顶堆。

image.png

算法思想(以大顶堆为例):

  1. 将长度为n的待排序的数组进行堆有序化构造成一个大顶堆
  2. 将根节点与尾节点交换并输出此时的尾节点
  3. 将剩余的n -1个节点重新进行堆有序化
  4. 重复步骤2,步骤3直至构造成一个有序序列

举例:

假设待排序数组为[20,50,10,30,70,20,80]

构造堆:

在构造有序堆时,我们开始只需要扫描一半的元素(n/2-1 ~ 0)即可,为什么? 因为(n/2-1)~0的节点才有子节点,如图1,n=8,(n/2-1) = 3 即3 2 1 0这个四个节点才有子节点

image.png

所以接下来就是将3 2 1 0这四个节点从下到上,从右到左的与它自己的子节点比较并调整最终形成大顶堆,过程如下:

第一次for循环将节点3和它的子节点7 8的元素进行比较,最大者作为父节点(即元素60作为父节点)

[红色表示交换后的状态]

image.png

接下来将节点2和它的子节点5 6的元素进行比较,最大者为父节点(元素80作为父节点)

image.png

然后将节点1和它的子节点3 4的元素进行比较,最大者为父节点(元素70作为父节点)

image.png

最后将节点0和它的子节点1 2的元素进行比较,最大者为父节点(元素80作为父节点)

image.png

至此有序堆已经构造好了!如下图:

image.png

调整堆

1)堆顶元素80和尾40交换后–>调整堆

image.png

2)堆顶元素70和尾30交换后–>调整堆

image.png

4)其他依次类推,最终已排好序的元素如下:

image.png

代码实现:

public class HeapSort {
    private static void heapSort(int[] arr) {
        int len = arr.length -1;
        for(int i = len/2 - 1; i >=0; i --){ //堆构造
            heapAdjust(arr,i,len);
        }
        while (len >=0){
            swap(arr,0,len--);    //将堆顶元素与尾节点交换后,长度减1,尾元素最大
            heapAdjust(arr,0,len);    //再次对堆进行调整
        }
    }

public static  void heapAdjust(int[] arr,int i,int len){
    int left,right,j ;
    while((left = 2*i+1) <= len){    //判断当前父节点有无左节点(即有无孩子节点,left为左节点)
        right = left + 1;  //右节点
        j = left;   //j"指针指向左节点"
        if(j < len && arr[left] < arr[right])    //右节点大于左节点
            j ++;     //当前把"指针"指向右节点
        if(arr[i] < arr[j])    //将父节点与孩子节点交换(如果上面if为真,则arr[j]为右节点,如果为假arr[j]则为左节点)
            swap(arr,i,j);
        else         //说明比孩子节点都大,直接跳出循环语句
            break;
        i = j;
    }
}
    public static  void swap(int[] arr,int i,int len){
             int temp = arr[i];
              arr[i] = arr[len];
             arr[len] = temp;
    }
    public static void main(String[] args) {
        int array[] = {20,50,20,40,70,10,80,30,60};
        System.out.println("排序之前:");
        for(int element : array){
            System.out.print(element+" ");
        }
        heapSort(array);
        System.out.println("\n排序之后:");
        for(int element : array){
            System.out.print(element+" ");
        }
    }
}