常见排序算法总结

排序的相关概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假设在待排序的记录序列中,存在多个具有相同关键字的记录,若经过排序,这些记录的相对次数能够保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,经过排序以后,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,即在排序过程中要在内外存之间移动数据的排序。

常见的排序算法

常见排序算法总结

常见排序算法的实现

1 插入排序

1.1直接插入排序

直接插入排序是一种简单的插入排序法,基本思想是:把待排序的记录按其关键码的大小依次插入到一个已经排好序的有序序列中,知道所有的记录插入完毕,得到一个新的有序序列。
过程:
1.拿到一个新序列,把第一个记录当成有序区间;
2.从第二个记录开始,与前面有序区间中的值从后往前依次比较,若小于一个数,则插入到这个数的前面,从插入位置开始往后的区间中的数依次后移。
3.接着从第三个记录开始,重复这个过程,知道最后一个记录完成。
代码实现:

void InsertSort(int *array, int size)
{
    for (int i = 1; i < size; ++i)//把0号下标即第一个元素当成有序序列
    {
         int key = array[i];//保存好要插入的元素
         int end = i - 1;//标记有序序列的最后一个元素
         while (end >= 0 && key < array[end])
         {
             array[end + 1] = array[end];
             end--;
         }
         //从循环出来代表end位置的元素小于或等于key,而end+1位置的元素比key大
         array[end + 1] = key;
    }
}

直接插入排序的特性总结:

  • 元素集合越接近有序,直接插入排序算法的效率越高
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:稳定
  • 适用场景:接近有序,数据量比较少

1.2希尔排序(缩小增量排序)

希尔排序法又称缩小增量法,它是插入排序的一种优化,基本思想是:先选定一个整数,把待排序的序列中所有记录分成若干个小组,所有距离为gap的记录分在同一个组内,并对每一组内的记录进行排序。然后,取gap=gap/2,重复上述分组和排序的工作,当达到gap=1时,所有就在同一组内排好序。
过程:
1.过程类似于直接插入排序,是直接插入排序的一种优化
2.首先,确定一个步长gap,根据步长,将序列分成N/gap个组,每一部分单独运用直接插入排序
3.把步长缩短,重复这个过程,直到步长为1
代码实现:

void ShellSort(int *array, int size)
{
    int gap = size;
    while (gap>1)
    {
         gap = gap / 3 + 1;
         for (int i = gap; i < size; ++i)
         {
             int key = array[i];
             int end = i - gap;
             while (end >= 0 && key < array[end])
             {
                 array[end + gap] = array[end];
                 end -= gap;
             }
             array[end + gap] = key;
         }
    }
}

希尔排序的特性总结:

  • 希尔排序是对直接插入排序的优化
  • 时间复杂度需要推导,平均时间复杂度:O(N1.3-N2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 希尔提出的gap取法是每次减半,但是有人发现取gap=gap/3+1时,时间复杂度最优,O(N1.25-1.6N1.25)

2 选择排序

2.1直接选择排序

基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,放在序列的起始位置(或末尾位置),知道全部待排序的数据元素排完。
过程:
1.遍历整个序列,找到整个序列中最大的数,与末尾位置的数交换位置。
2.遍历到倒数第二个数,找到最大的数,与倒数第二个数交换位置。
3.重复整个过程,直到整个序列都排好序。
代码实现:

void SelectSort(int *array, int size)
{
    //选择的趟数
    for (int i = 0; i < size - 1; i++)
    {
         //选择的方式
         int maxPos = 0;
         for (int j = 1; j < size - i; j++)
         {
             if (array[j] > array[maxPos])
                 maxPos = j;
         }
         if (maxPos != size-i-1)
             Swap(&array[maxPos], &array[size - 1 - 
i]);
    }
}

直接选择排序的特性总结:

  • 很好理解,但是效率不是很好,实际中很少用。
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

直接选择排序优化
一次遍历,一起找出最大和最小元素
代码实现:

//选择排序优化
void SelectSortOP(int *array, int size)
{
    int begin = 0;
    int end = size - 1;
    while (begin < end)
    {
         int minPos = begin;
         int maxPos = begin;
         int index = begin + 1;
         //找最大最小元素的位置
         while (index<=end)
         {
             if (array[index] < array[minPos])
                 minPos = index;

             if (array[index]>array[maxPos])
                 maxPos = index;

             index++;
         }
         if (maxPos != end)
             Swap(&array[maxPos], &array[end]);
           
         if (minPos == end)
             minPos = maxPos;

         if (minPos != begin)
             Swap(&array[minPos], &array[begin]);

         begin++;
         end--;
    }
}

2.2堆排序

堆排序是指利用堆这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来选择数据。需要注意的是排升序要建大堆,排降序建小堆
代码实现:

//堆调整算法
void HeapAdjust(int* array, int size, int parent)
{
    int child = ((parent << 1) + 1);
    while (child < size)
    {
         //找左右孩子中较大的孩子
         if (child + 1 < size && array[child + 1] > 
array[child])
             child += 1;

         //检测双亲是否比较大的孩子大
         if (array[child]>array[parent])
         {
             Swap(&array[child], &array[parent]);
             parent = child;
             child = ((parent << 1) + 1);
         }
         else
             return;
    }
}

//堆排序
void HeapSort(int *array, int size)
{
    //1.建堆
    int root = ((size - 2) >> 1);
    for (; root >= 0; root--)
    {
         HeapAdjust(array, size, root);
    }
    //2.堆排序
    int end = size - 1;
    while (end)
    {
         Swap(&array[0], &array[end]);
         HeapAdjust(array, end, 0);
         end--;
    }
}

堆排序的特性总结:

  • 堆排序使用堆来选数,效率就高了很多
  • 时间复杂度:O(NlogN)
  • 空间复杂度:O(1)
  • 稳定性:不稳定

3 交换排序

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来交换这两个记录在序列中的位置。

3.1 冒泡排序

过程:
1.从第一个元素位置开始,比较相邻的两个数的大小,如果后面的数小于前面的数,则交换位置。
2.遍历一遍下来,最后一个数为整个序列中的最大值。
3.把最后一个数排除,重复上述过程。
代码实现:

void BubbleSort(int* array, int size)
{
    for (int i = 0; i < size - 1; ++i)
    {
         int flag = 0;
         for (int j = 1; j < size-i; ++j)
         {
             if (array[j - 1]>array[j])
             {
                 flag = 1;
                 Swap(&array[j - 1], &array[j]);
             }
         }
         if (!flag)//如果一趟下来没有发生交换,则说明已经有序
             return;
    }
}

冒泡排序的特性总结:

  • 时间复杂度:O(N)
  • 空间复杂度:O(1)
  • 稳定性:稳定

3.2 快速排序

基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两个子序列进行排序,使得左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有元素都已经有序。
代码实现:

//快速排序
void QuickSort(int* array, int left,int right)
{
    if (right - left > 1)
    {
         int div = Pation(array, left, right);
         QuickSort(array, left, div);
         QuickSort(array, div + 1, right);
    }
}

按照基准值划分为左右两半部分的常见方式有:

  1. hoare法
//hoare法
int Pation1(int* array, int left, int right)
{
    int begin = left;
    int end = right - 1;
    int key = array[right - 1];
    while (begin<end)
    {
         //从前往后找比key大的元素
         while (begin<end && array[begin] <= key)
             begin++;

         //从后往前找比key小的元素
         while (begin<end && array[end] >= key)
             end--;
         if (begin<end)
             Swap(&array[begin], &array[end]);
    }
    if (begin!=right-1)
         Swap(&array[begin], &array[right - 1]);

    return begin;
}
  1. 挖坑法
//挖坑法
int Pation2(int* array, int left, int right)
{
    int begin = left;
    int end = right - 1;
    int key = array[right - 1];
    while (begin<end)
    {
         while (begin < end && array[begin] <= key)
             begin++;

         if (begin < end)
         {
             array[end] = array[begin];
             end--;
         }
         while (begin<end && array[end] >= key)
             end--;

         if (begin < end)
         {
             array[begin] = array[end];
             begin++;
         }
    }
    array[begin] = key;
    return begin;
}
  1. 前后指针法
//前后指针法
int Pation3(int* array, int left, int right)
{
    int prev = left - 1;
    int cur = left;
    int key = array[right - 1];
    while (cur<right)
    {
         if (array[cur] < key && ++prev != cur)
             Swap(&array[cur], &array[prev]);
         
         ++cur;
    }
    if (++prev != right - 1)
         Swap(&array[prev], &array[right - 1]);

    return prev;
}

快排的优化

  1. 三数取中法
    从上面可以看到,每次取基准值是取的最右边的元素,而若这个值刚好是这个序列中的极大值或者极小值,这样将影响快排的性能。所以采用三数取中法,尽可能的避免这种情况发生。
    代码实现:
//三数取中法
int GetMiddleIndex(int* array, int left, int right)
{
    int mid = left + ((right - left) >> 1);
    if (array[left] < array[right - 1])
    {
         if (array[mid] < array[left])
             return left;

         else if (array[mid]>array[right - 1])
             return right - 1;

         else
             return mid;
    }
    else
    {
         if (array[mid] > array[left])
             return left;

         else if (array[mid] < array[right - 1])
             return right - 1;

         else
             return mid;
    }
}

通过这种方法在取基准值之前,计算出一个mid位置,若mid不是最右边的位置,则交换mid位置和最右边位置中的值。

  1. 递归到小的区间时,可以考虑使用插入排序。

快速排序的特性总结:

  • 时间复杂度:平均为O(NlogN)
  • 空间复杂度:递归的深度(二叉树的高度),平均为O(logN)
  • 稳定性:不稳定

快排的非递归实现

//非递归快排
void QuickSortNor(int* array, int size)
{
    Stack s;
    StackInit(&s);
    StackPush(&s, size);
    StackPush(&s, 0);
    while (!StackEmpty(&s))
    {
         int left = StackTop(&s);
         StackPop(&s);
         int right = StackTop(&s);
         StackPop(&s);
         if (right - left > 1)
         {
             int div = Pation2(array, left, right);
             StackPush(&s, right);
             StackPush(&s, div + 1);
             StackPush(&s, div);
             StackPush(&s, left);
         }
    }
    StackDestroy(&s);
}

4 归并排序

基本思想:归并排序是一个外排序,它可以对磁盘的文件进行排序,是采用分治法的一个非常典型的应用。它将待排序的元素序列分储层两个长度相等的子序列,对每一个子序列进行排序,然后将他们合并为一个序列。合并两个子序列的过程称为二路归并。
代码实现:

//归并算法
void MergeData(int* array, int left, int mid,int 
right ,int* temp)
{
    int begin1 = left;
    int end1 = mid;
    int begin2 = mid ;
    int end2 = right;
    int index = left;
    while (begin1 < end1 && begin2<end2)
    {
         if (array[begin1] <= array[begin2])
         {
             temp[index++] = array[begin1++];
         }
         else
         {
             temp[index++] = array[begin2++];
         }
    }
    while (begin1 < end1)
    {
         temp[index++] = array[begin1++];
    }
    while (begin2<end2)
    {
         temp[index++] = array[begin2++];
    }
}

void _MergeSort(int* array,int left,int right,int* 
temp)
{
    if (right - left > 1)
    {
         int mid = left + ((right - left) >> 1);
         _MergeSort(array, left, mid,temp);
         _MergeSort(array, mid, right,temp);
         MergeData(array, left, mid, right, temp);
         memcpy(array + left, temp + left, (right - 
left)*sizeof(array[0]));
    }
}

//归并排序
void MergeSort(int* array, int size)
{
    int* temp = (int*)malloc(sizeof(array[0])*size);
    assert(temp);
    _MergeSort(array, 0, size,temp);
    free(temp);
}

归并排序的特性总结:

  • 时间复杂度:O(NlogN)
  • 空间复杂度:O(N)
  • 稳定性:稳定
  • 外部排序,可以对磁盘中的文件进行排序

非递归归并排序
代码实现:

//非递归归并排序
void MergeSortNor( int* array,int size)
{
    int* temp = (int*)malloc(sizeof(array[0])*size);
    assert(temp);
    int gap = 1;
    while (gap < size)
    {
         for (int i = 0; i < size; i += 2 * gap)
         {
             int left = i;
             int mid = left + gap;
             int right = mid + gap;
             if (mid>size)
                 mid = size;

             if (right>size)
                 right = size;

             MergeData(array, left, mid, right, 
temp);
         }
         memcpy(array, temp, size*sizeof(array[0]));
         gap *= 2;
    }
    free(temp);
}

5 非比较排序

计数排序

基本思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
过程:
1.根据已知列范围开辟空间
2.统计每个元素出现的次数
3.根据统计的结果将序列回收到原来的序列中
代码实现:

//非选择排序:计数排序
void CountSort(int* array, int size)
{
    //1.确定元素的范围
    int minValue = array[0];
    int maxValue = array[0];
    for (int i = 0; i < size; ++i)
    {
         if (array[i] < minValue)
             minValue = array[i];

         if (array[i]>maxValue)
             maxValue = array[i];
    }
    int range = maxValue - minValue + 1;
    int* count = 
(int*)malloc(sizeof(array[0])*range);
    assert(count);
    memset(count, 0, range*sizeof(array[0]));
    //2.统计每个元素出现的次数
    for (int i = 0; i < size; ++i)
    {
         count[array[i] - minValue]++;
    }
    //3.回收
    size_t index = 0;
    for (int i = 0; i < range; ++i)
    {
         int j = 0;
         while (j < count[i])
         {
             array[index++] = i + minValue;
             j++;
         }
    }
    free(count);
}

计数排序的特性总结:

  • 时间复杂度:O(N)
  • 空间复杂度:O(range)
  • 稳定性:稳定

各算法的性能比较

排序算法 平均时间复杂度 最好情况 最坏情况 平均空间复杂度 稳定性 排序方式
直接插入排序 O(N^2) O(N) O(N^2) O(1) 稳定 内部排序
希尔排序 O(N^(1.3-2)) 依赖于gap取法 依赖于gap取法 O(1) 不稳定 内部排序
直接选择排序 O(N^2) O(N^2) O(N^2) O(1) 不稳定 内部排序
堆排序 O(NlogN) O(NlogN) O(NlogN) O(1) 不稳定 内部排序
冒泡排序 O(N^2) O(N) O(N^2) O(1) 稳定 内部排序
快速排序 O(NlogN) O(NlogN) O(N^2) O(logN) 不稳定 内部排序
归并排序 O(NlogN) O(NlogN) O(NlogN) O(N) 稳定 外部排序
计数排序 O(max(N, k)) O(max(N, k)) O(max(N, k)) O(k) 稳定 外部排序