十大排序——快速排序

[TOC]

参考:
(不会真的有人不知道菜鸟教程吧?哈哈)
1.6 快速排序 | 菜鸟教程
1.0 十大经典排序算法 | 菜鸟教程


此快速排序对数据的要求是不能有 重复元素 !!!

简介

Shiel

1. 5种常用的排序

均假定为升序
4种排序

  1. 冒泡排序

    • 第 1 个和第 2 个比,第 2 个跟第 3 个比,第 3 个跟第 4 个比,···,第 n-1 个和第 n 个比,每次把大的数放后面
    • 第1轮比完之后,前n个数的最大数字就到后面了。
    • 之后递归:前 n-1 项、前 n-2 项、···前 2 项
    • 排序完成
  2. 插入排序

    • 把第 2 个数据插入到第 1 个处,保证前 2 个数据有序
    • 把第 3 个数据插入到前 2 个处,保证前 3 个数据有序
    • ·····
    • 把第 n 个数据插入到前 n-1 个处,保证前 n 个数据有序
  3. 选择排序

    • 在所有数据中按照某种算法选择一个数值,如果是升序,选择是最小的——第 1 个就是最小值
    • 再从后面的 n-2 项中,选择一个次最小值,跟第 2 个数字互换——第 2 个数字变成次最小值
    • ······
    • 完成排序
  4. 快速排序

    • 快排名字起的很好,快排快排,说明速度是很快的,快排也比较复杂,很经典的排序算法。
  5. 归并排序

    • 两个两个排,先使两个两个有序
    • 四个四个排,再使四个四个有序
    • 八个八个排,再使八个八个有序
    • ······

2. 排序和查找的关系

排序是查找的前提。

3. 排序时重点关注的

  1. 时间
  2. 空间
  3. 稳定性——每次排序之后不改变原来数据的相对位置就是稳定的
  • 甲 60、乙 39、丙 60、丁 89
  • 即每次排序之后甲仍然在丙前面就是稳定的

菜鸟教程

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》上找到了满意的答案:

快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

$fileName

点击以下图片查看大图

$fileName

算法步骤

Shiel

伪算法

  1. 先找到某一个元素的确切位置(指排序后的正确的位置),即 中间点,从此中间点分成两半
  2. 对中间点左边再找 新的中间点 的确切位置
  3. 对中间点右边再找 新的中间点 的确切位置
  • 即:
    假定是第一个元素(未排序之前),把第一个元素的确切的位置找到,这样左边是一半,右边是一半。

图片演示

字太丑了

一步完成 分步进行

注意事项

1. 后来分成的左边一半和右边一半怎么办?

按照同样的算法,把第一个元素的确切的位置给找到,又把左边一半分成两半,那左边一半怎么排?再找到第一个,继续分······
递归思想分治思想 的应用,需要 递归

2. 此算法最关键的问题是?

如何找到某一个元素的确切的位置。
只有找到了,这个元素的位置才能固定住,才能进行分治和递归,不然根本没办法进行。

3. 如何找某一个元素确切的位置?

快排分很多种,这里讲的是比较快的一种。

  1. 定义一个临时变量 val,将第 1 个元素的值赋给 val
  2. 定义两个指针 low 和 high(参数),low 指针保存第 1 个元素的地址,high 指针保存 最后一个 元素的地址。
  3. 把 l 往右移,h 往左移
  4. 当 l 和 h 重合时的位置即第 1 个元素的位置

4. l 和 h 怎么移?

5 2 6 8 4 3 7

假定升序,最终的效果是 val 左边都是比 val 小的,val 右边都是比 val 大的!

5. l 和 h 移动时涉及到的操作

首先需要明白这里涉及到两个操作:赋值移动

  • 赋值

    即把当前指向 h(l) 元素赋给 l(h)
    什么时候才需要赋值?

    当此元素的位置错乱时才赋值,即此元素位置放错了:

    1. val 右半部分应该是比 val 大的,那么当 出现比 val 小的元素就赋给左边 l 位置处的 a[low]
    2. val 左半部分应该是比 val 小的,那么当 出现比 val 大的元素就赋给右边 h 位置处的 a[high]
  • 移动

    即把指向 h (l) 的指针往 左移(右移)
    什么时候才需要移指针?

    当此元素的位置正确时就移动呀!即此元素的位置是正确的,那就去看下一个元素的位置是否正确

    1. val 右半部分应该是比 val 大的,那么当 出现比 val 大的元素时说明此元素位置正确,就把 h 往左移,直到找到比 val 小的元素——进行赋值操作
    2. val 左半部分应该是比 val 小的,那么当 出现比 val 小的元素时说明此元素位置正确,就把 l 往右移,直到找到比 val 大的元素——进行赋值操作

6. 移动时会出现的情况

由排列组合知会有 4 种情况:

  1. val 右半部分:a[high] > val
  2. val 右半部分:a[high] < val
  3. val 左半部分:a[low] > val
  4. val 右半部分:a[low] < val

现在你知道这 4 种情况应该怎么做了。

7. 一轮排序后就结束了吗?

经过一轮排序函后只能确定第一个元素的下标,其它元素依然是无效的!
因为一轮排序只是对第一个元素操作,虽然已经比较复杂了,但只有第一个元素确定位置了。

菜鸟教程

伪算法

  1. 从数列中挑出一个元素,称为 “基准”(pivot);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;

图片演示

菜鸟教程:
菜鸟教程

代码实现

测试用例

5 2 6 8 4 3 7

输出结果

2 3 4 5 6 7 8

Shiel

C

/**
* @brief: 快速排序
* @version: 1.0
* @author: @Shiel
* @date: 2023-11-27 08:40:26
**/
# include <stdio.h>

void Sort_Quick( int* a, int low, int high ); // 快排
int FindPos( int* a, int low, int high); // 查找并返回 L=H 的位置

int main(void)
{
int a[7] = {5, 2, 6, 8, 4, 3 ,7};

Sort_Quick(a, 0, 6); // 第二个元素表示第一个元素的下标,第三个参数表示最后一个元素的下标

for( int i=0; i<7; i++ )
{
printf("%d ", a[i]);
}
printf("\n");

return 0;
}

void Sort_Quick( int* a, int low, int high )
{
int pos; // 中间点 L=H 时的位置

if( low < high ) // 只有 low<high 时才继续进行
{
pos = FindPos(a, low, high); // 获取中间点 L=H 的位置
Sort_Quick(a, low, pos - 1); // 中间点左半部递归
Sort_Quick(a, pos + 1, high); // 中间点右半部递归
}
}

int FindPos( int* a, int low, int high )
{
int val = a[low];

while( low < high )
{
// 首先从右边 high 开始向左进行查找
while( low < high && a[high] > val ) // high 指向的元素大于val,high左移
{
high--; // a[high]>val,不需要赋值,则high左移
}
a[low] = a[high]; // 即a[high]<val的情况,此时将a[high]赋给a[low]

// 再从左边 low 开始向右进行查找
while( low < high && a[low] < val ) // low指向的元素小于val,low右移
{
low++; // a[low]<val,不需要赋值,则low右移
}
a[high] = a[low]; // 即a[low]>val的情况,此时将a[low]赋值给a[high]
} //终止循环之后 L和H 一定是相等的

a[low] = val; // L=H,此时将val的值赋给 a[low]和a[high] 是一样的

return high; // 返回下标,等价于 return low;
}

菜鸟教程

C++

//严蔚敏《数据结构》标准分割函数
 int Paritition1(int A[], int low, int high) {
   int pivot = A[low];
   while (low < high) {
     while (low < high && A[high] >= pivot) {
       --high;
     }
     A[low] = A[high];
     while (low < high && A[low] <= pivot) {
       ++low;
     }
     A[high] = A[low];
   }
   A[low] = pivot;
   return low;
 }

 void QuickSort(int A[], int low, int high) //快排母函数
 {
   if (low < high) {
     int pivot = Paritition1(A, low, high);
     QuickSort(A, low, pivot - 1);
     QuickSort(A, pivot + 1, high);
   }
 }

C

typedef struct _Range {
    int start, end;
} Range;

Range new_Range(int s, int e) {
    Range r;
    r.start = s;
    r.end = e;
    return r;
}

void swap(int *x, int *y) {
    int t = *x;
    *x = *y;
    *y = t;
}

void quick_sort(int arr[], const int len) {
    if (len <= 0)
        return; // 避免len等于负值时引发段錯誤(Segment Fault)
    // r[]模板列表,p为數量,r[p++]为push,r[--p]为pop且取得元素
    Range r[len];
    int p = 0;
    r[p++] = new_Range(0, len - 1);
    while (p) {
        Range range = r[--p];
        if (range.start >= range.end)
            continue;
        int mid = arr[(range.start + range.end) / 2]; // 选取中间点为基准点
        int left = range.start, right = range.end;
        do {
            while (arr[left] < mid) ++left;   // 检测基准点左侧是否符合要求
            while (arr[right] > mid) --right; //检测基准点右侧是否符合要求
            if (left <= right) {
                swap(&arr[left], &arr[right]);
                left++;
                right--;               // 移动指针以继续
            }
        } while (left <= right);
        if (range.start < right) r[p++] = new_Range(range.start, right);
        if (range.end > left) r[p++] = new_Range(left, range.end);
    }
}

C & 递归法

void swap(int *x, int *y) {
    int t = *x;
    *x = *y;
    *y = t;
}

void quick_sort_recursive(int arr[], int start, int end) {
    if (start >= end)
        return;
    int mid = arr[end];
    int left = start, right = end - 1;
    while (left < right) {
        while (arr[left] < mid && left < right)
            left++;
        while (arr[right] >= mid && left < right)
            right--;
        swap(&arr[left], &arr[right]);
    }
    if (arr[left] >= arr[end])
        swap(&arr[left], &arr[end]);
    else
        left++;
    if (left)
        quick_sort_recursive(arr, start, left - 1);
    quick_sort_recursive(arr, left + 1, end);
}

void quick_sort(int arr[], int len) {
    quick_sort_recursive(arr, 0, len - 1);
}

C++ & 函数法

sort(a,a + n);// 排序a[0]-a[n-1]的所有数.

C++ & 迭代法

// 参考:http://www.dutor.net/index.php/2011/04/recursive-iterative-quick-sort/
struct Range {
    int start, end;
    Range(int s = 0, int e = 0) {
        start = s, end = e;
    }
};
template <typename T> // 整數或浮點數皆可使用,若要使用物件(class)時必須設定"小於"(<)、"大於"(>)、"不小於"(>=)的運算子功能
void quick_sort(T arr[], const int len) {
    if (len <= 0)
        return; // 避免len等於負值時宣告堆疊陣列當機
    // r[]模擬堆疊,p為數量,r[p++]為push,r[--p]為pop且取得元素
    Range r[len];
    int p = 0;
    r[p++] = Range(0, len - 1);
    while (p) {
        Range range = r[--p];
        if (range.start >= range.end)
            continue;
        T mid = arr[range.end];
        int left = range.start, right = range.end - 1;
        while (left < right) {
            while (arr[left] < mid && left < right) left++;
            while (arr[right] >= mid && left < right) right--;
            std::swap(arr[left], arr[right]);
        }
        if (arr[left] >= arr[range.end])
            std::swap(arr[left], arr[range.end]);
        else
            left++;
        r[p++] = Range(range.start, left - 1);
        r[p++] = Range(left + 1, range.end);
    }
}

C++ & 递归法

template <typename T>
void quick_sort_recursive(T arr[], int start, int end) {
    if (start >= end)
        return;
    T mid = arr[end];
    int left = start, right = end - 1;
    while (left < right) { //在整个范围内搜寻比枢纽元值小或大的元素,然后将左侧元素与右侧元素交换
        while (arr[left] < mid && left < right) //试图在左侧找到一个比枢纽元更大的元素
            left++;
        while (arr[right] >= mid && left < right) //试图在右侧找到一个比枢纽元更小的元素
            right--;
        std::swap(arr[left], arr[right]); //交换元素
    }
    if (arr[left] >= arr[end])
        std::swap(arr[left], arr[end]);
    else
        left++;
    quick_sort_recursive(arr, start, left - 1);
    quick_sort_recursive(arr, left + 1, end);
}
template <typename T> //整數或浮點數皆可使用,若要使用物件(class)時必須設定"小於"(<)、"大於"(>)、"不小於"(>=)的運算子功能
void quick_sort(T arr[], int len) {
    quick_sort_recursive(arr, 0, len - 1);
}

参考地址:
https://github.com/hustcc/JS-Sorting-Algorithm/blob/master/6.quickSort.md
https://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F

C#

//快速排序(目标数组,数组的起始位置,数组的终止位置)
static void QuickSort(int[] array, int left = 0, int right = -1)
{
if (right.Equals(-1)) right = array.Length - 1;
try
{
int keyValuePosition; //记录关键值的下标
//当传递的目标数组含有两个以上的元素时,进行递归调用。(即:当传递的目标数组只含有一个元素时,此趟排序结束)
if (left < right)
{
keyValuePosition = Partion(array, left, right); //获取关键值的下标(快排的核心)
QuickSort(array, left, keyValuePosition - 1); //递归调用,快排划分出来的左区间
QuickSort(array, keyValuePosition + 1, right); //递归调用,快排划分出来的右区间
}
}
catch (Exception ex)
{
Console.WriteLine("Exception: {0}", ex);
}
}

///快速排序的核心部分:确定关键值在数组中的位置,以此将数组划分成左右两区间,关键值游离在外。(返回关键值应在数组中的下标)
static int Partion(int[] array, int left, int right)
{
int leftIndex = left; //记录目标数组的起始位置(后续动态的左侧下标)
int rightIndex = right; //记录目标数组的结束位置(后续动态的右侧下标)
int keyValue = array[left]; //数组的第一个元素作为关键值
int temp;
//当 (左侧动态下标 == 右侧动态下标) 时跳出循环
while (leftIndex < rightIndex)
{
while (leftIndex < rightIndex && array[leftIndex] <= keyValue) //左侧动态下标逐渐增加,直至找到大于keyValue的下标
{
leftIndex++;
}
while (leftIndex < rightIndex && array[rightIndex] > keyValue) //右侧动态下标逐渐减小,直至找到小于或等于keyValue的下标
{
rightIndex--;
}
if (leftIndex < rightIndex) //如果leftIndex < rightIndex,则交换左右动态下标所指定的值;当leftIndex==rightIndex时,跳出整个循环
{
temp = array[leftIndex];
array[leftIndex] = array[rightIndex];
array[rightIndex] = temp;
}
}

//当左右两个动态下标相等时(即:左右下标指向同一个位置),此时便可以确定keyValue的准确位置
temp = keyValue;
if (temp < array[rightIndex]) //当keyValue < 左右下标同时指向的值,将keyValue与rightIndex - 1指向的值交换,并返回rightIndex - 1
{
array[left] = array[rightIndex - 1];
array[rightIndex - 1] = temp;
return rightIndex - 1;
}
else //当keyValue >= 左右下标同时指向的值,将keyValue与rightIndex指向的值交换,并返回rightIndex
{
array[left] = array[rightIndex];
array[rightIndex] = temp;
return rightIndex;
}
}