排序算法 | 快速排序算法及其优化

目录

快速排序(Quick Sort)

1、算法描述

2、算法分析——快速排序一次划分的三种实现方式

(1) 挖坑法

(2) 左右指针法

(3) 前后指针法

3、算法分析——快速排序基准数的三种选取方式

(1)固定位置选取基准

(2)随机位置选取基准

(3)三分取中法选取基准

4、算法分析——快速排序的两种优化

(1)当数据量很小时,采用直接插入排序

(2)聚集相同基准法

5、算法分析——快速排序的两种调用方式

(1)递归调用

(2)非递归调用

6、算法示例

(1)挖坑法

(2)随机基准法

(3)三分取中法

(4)插排 + 三分取中法

(5)聚集相同基准法 + 三分取中法


快速排序(Quick Sort)

快速排序(Quick Sort)是对冒泡排序的一种改进

快速排序由C. A. R. Hoare在1962年提出。并且在内存使用、程序实现复杂性上表现优秀,尤其是对快速排序算法进行随机化的可能,使得快速排序在一般情况下是最实用的排序方法之一。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以通过递归进行,以此达到整个数据变成有序序列,快速排序也可通过非递归的方式实现

快速排序被认为是当前最优秀的内部排序方法。

1、算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

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

2、算法分析——快速排序一次划分的三种实现方式

(1)挖坑法

基本思想:

  1. 设置两个变量i、j,排序开始的时候:i=0,j=N-1;
  2. 第一个数组元素作为关键数据,赋值给key,即key=A[0];
  3. 从 j 开始向前搜索,即由后开始向前搜索( j-- ),找到第一个小于key的值A[ j ],将A[ j ]和A[ i ]互换;
  4. 从 i 开始向后搜索,即由前开始向后搜索( i++),找到第一个大于key的A[ i ],将A[ i ]和A[ j ]互换;
  5. 重复第3、4步,直到 i=j,将key 填入 A[ i ]中

注意:当以第一个数组元素为基准数时,搜索必须先从后向前进行,当以最后一个数组元素为基准数时,搜索必须先从前向后进行。

挖坑法图示:

(a)一趟排序的过程:

排序算法 | 快速排序算法及其优化_第1张图片

(b)排序的全过程

排序算法 | 快速排序算法及其优化_第2张图片

/* 挖坑法的基本思想就是将基准值保存起来(挖出)
** 从右到左查找比基准值小的值,然后与arr[low]交换(即high位置被挖出)
** 从左到右查找比基准值大的值,然后与arr[high]交换(即low位置被挖出)
** 然后将temp保存的基准值填入low的位置(或者high位置,因为low和high此时相等)
*/
int Partition(int *arr,int low,int high)
{
    int temp = arr[low];
    while(low < high)
    {
        while(low < high && arr[high] >= temp){high--;}
        arr[low] = arr[high];
        while(low < high && arr[low] <= temp){low++;}
        arr[high] = arr[low];
    }
    arr[low] = temp;   
    return low;
}

(2) 左右指针法

  • 选取一个关键字(key)作为枢轴,一般取整组记录的第一个数/最后一个为枢轴。
  • 设置两个变量left = 0;right = N - 1;
  • 从left一直向后走,直到找到一个大于key的值,right从后至前,直至找到一个小于key的值,然后交换这两个数。
  • 重复第三步,一直往后找,直到left和right相遇,这时将key放置left的位置即可。
  • 然后再继续进行排序,划为子问题进行解决
void swap(int *arr,int low,int high)
{
  int tmp = arr[low];
  arr[low] = arr[high];
  arr[high] = tmp;
}

/*  左右指针法进行一次划分
**  以0号元素为基准数,从右开始向左查找小于基准数的数字(high--),找到后停下来
**  再从左向右开始查找大于基准数的数字(low++),找到后停下来
**  如果low小于high,则交换他们的值
**  结束后将temp保存值与low的值(或high的值,因为此时low与high相等)交换
**  完成一次划分
*/

int Pritition(int *arr,int low,int high)
{
    int temp = low;
    while (low= arr[temp]){high--;}
        while (low < high && arr[low] <= arr[temp]){low++;}
        if(low < high){swap(arr,low,high);}
    } 
    swap(arr,low,temp);   // 或 swap(arr,high,temp);	
    return low;   // 或 return high
}

(3) 前后指针法

定义两个指针一前一后前面指针找比基数小的数,后面指针找比基数大的数,前面的指针找到后,将前后指针所指向的数据交换,当前面的指针遍历完整个数组时,将基数值与后指针的后一个位置的数据进行交换,然后以后指针的后一个位置作为分界,然后将数组分开,进行递归排序。

int Partition(int arr[], int left, int right)
{ 
    int fast = left;  // 若arr[fast] >= key ,则fast++
    int slow = fast-1 ;  // arr[fast] >= key时,slow不动,直到fast找到大于小于key的数字,然后交换
    int key = arr[right];
    while (fast <= right)
    {
        if (arr[fast] <= key && ++slow != fast)
        {
            swap(arr,slow,fast);    
        }
        fast++;
    }
    return slow;
}

3、算法分析——快速排序基准数的三种选取方式

(1)固定位置选取基准

基本思想:选取第一个或最后一个元素作为基准值。

注意:基本的快速排序选取第一个或最后一个元素作为基准。但是,这是一直很不好的处理方法。 

int Partition(int *arr, int left, int right)
{
    int i = left;
    int j = right;
    int k = arr[left];   // 使用首元素作为基准值
    while (i < j)
    {
        while(i < j && arr[j] >= k) {j--;}
        arr[i] = arr[j];
        while(i < j && arr[i] <= k) {i++;}
        arr[j] = arr[i];
    }
    arr[i] = k;
    return i;
}

(2)随机位置选取基准

基本思想:选取待排序列中任意一个数作为基准值,选出的任意值与low下标元素交换,可继续使用Partition函数

void swap(int *arr,int low,int high)
{
  int tmp = arr[low];
  arr[low] = arr[high];
  arr[high] = tmp;
}

int Partition(int *arr, int low, int high)  // 快排一次划分算法
{
    srand((unsigned)time(NULL));
    swap(arr,low,low + rand() % (high-low+1));

    int i = low;
    int j = high;
    int k = arr[low];
    while (i < j)
    {
        while(i < j && arr[j] >= k) {j--;}
        arr[i] = arr[j];
        while(i < j && arr[i] <= k) {i++;}
        arr[j] = arr[i];
    }
    arr[i] = k;
    return i;
}

(3)三分取中法选取基准

基本思想:arr[mid] <= arr[low] <= arr[high],使得arr[low]处于三数中的中间值,可继续使用Partition函数

void swap(int *arr,int low,int high)
{
  int tmp = arr[low];
  arr[low] = arr[high];
  arr[high] = tmp;
}

// arr[mid] <= arr[low] <= arr[high]
void SelectPivotMedianOfThree(int *arr,int low,int high)  
{
    int mid = (low+high) >> 1;
    if(arr[mid] > arr[low])
    {    
        swap(arr,mid,low);
    }
    if(arr[low] > arr[high])
    {
        swap(arr,low,high);
    }
    if(arr[mid] > arr[high])
    {
        swap(arr,mid,high);
    }
}

int Partition(int *arr, int low, int high)// 快排一次划分算法
{
    SelectPivotMedianOfThree(arr,low,high);

    int i = low;
    int j = high;
    int k = arr[low];
    while (i < j)
    {
        while(i < j && arr[j] >= k) {j--;}
        arr[i] = arr[j];
        while(i < j && arr[i] <= k) {i++;}
        arr[j] = arr[i];
    }
    arr[i] = k;
    return i;
}

4、算法分析——快速排序的两种优化

(1)当数据量很小时,采用直接插入排序

原因:对于很小和部分有序的数组,快速排序不如插入排序好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插入排序

截止范围:待排序序列长度N = 10,虽然在5~20之间任一截止范围都有可能产生类似的结果,这种做法也避免了一些有害的退化情形。

void InsertSort(int *arr,int low,int high)
{
    int tmp ;
    for(int i = low+1;i <= high;i++)
    {
        tmp = arr[i];
        int j = i-1;
        while (j >= low && arr[j] > tmp)
        {
            arr[j+1] = arr[j];
            j--;
        }
    arr[j+1] = tmp;
    }
}

void Quick(int *arr,int low,int high)
{
    int k = Partion(arr,low,high);
    if(high-low+1<10)  // 当整个序列的大小high-low+1<10时,采用直接插入排序
    {
        InsertSort(arr,low,high);
        return;
    }
    else
    {
        if(low < k-1)
        {
            Quick(arr,low,k-1);
        }
        if(high > k+1)
        {
            Quick(arr,k+1,high);
        }
    }
}

(2)聚集相同基准法

排序算法 | 快速排序算法及其优化_第3张图片

void FocusNumPar(int *arr,int low,int par,int high,int *left,int *right)
{
    int parLeft = par-1;
    if(low < high)
    {
        for(int i=par-1;i>=low;i--)
        {
            if(arr[i]==arr[par])
            {
                if(i!=par)
                {
                    swap(arr,i,parLeft);
                    parLeft--;
                }
                else
                {
                    i--;
                }
            }
        }
        *left = parLeft;

        int parRight = par+1;
        for(int i=par+1;i<=high;i++)
        {
            if(arr[i]==arr[par])
            {
                if(i!=par)
                {
                    swap(arr,i,parRight);
                    parRight++;
                }
                else
                {
                    i++;
                }
            }
        }
        *right = parRight;
    }
}

void Quick(int *arr,int low,int high)
{
    ThreeMiddle(arr,low,high);
    int left = 0;
    int right = 0;

    int k = Partion(arr,low,high);	
    FocusNumPar(arr,low,k,high,&left,&right);
    if(low <= left-1)
    {
        Quick(arr,low,left);
    }
    if(high >= right+1)
    {
        Quick(arr,right,high);
    }
}

5、算法分析——快速排序的两种调用方式

(1)递归调用

快递排序大部分的版本都是递归调用的方式来实现的:通过Partition来实现划分,并递归实现前后的划分。

void Quick(int *arr,int low,int high)
{
	int k = Partition(arr,low,high);
	if(low < k-1)
	{
		Quick(arr,low,k-1);
	}
	if(high > k+1)
	{
		Quick(arr,k+1,high);
	}
}

(2)非递归调用

因为递归的本质是栈,因此我们非递归实现的过程中,借助栈来保存中间变量就可以实现非递归了。在这里中间变量也就是通过Pritation函数划分之后分成左右两部分的首尾指针只需要保存这两部分的首尾指针即可

void Quick(int *arr,int len)   // 快排非递归调用
{
	int Size = (int)log((double)len/(int)(double)2);
	int *Stack = (int *)malloc(sizeof(int) * Size * len);
	assert(Stack!=NULL);
	int top = 0;
	int low = 0;
	int high = len-1;
	int k = Partition(arr,low,high);
	if(low < k-1)
	{
		Stack[top++] = low;
		Stack[top++] = k-1;
	}
	if(high > k+1)
	{
		Stack[top++] = k+1;
		Stack[top++] = high;
	}
	while (top > 0)   //栈为空则排序完毕
	{
		high = Stack[--top];
		low = Stack[--top];
		k = Partition(arr,low,high) ;
		if(low < k-1)
		{
			Stack[top++] = low;
			Stack[top++] = k-1;
		}
		if(high > k+1)
		{
			Stack[top++] = k+1;
			Stack[top++] = high;
		}
	}
	free(Stack);
}

6、算法示例

(1)挖坑法

#include
void Show(int *arr,int len);
int Partition(int *arr,int low,int high)
{
    static int n=1; // 记录排序次数
    int temp = arr[low];
    while(low < high)
    {
        while(low < high && arr[high] >= temp){high--;}
        arr[low] = arr[high];
        while(low < high && arr[low] <= temp){low++;}
        arr[high] = arr[low];
    }
    arr[low] = temp;   

    printf("第%d步排序结果:",n++);  // 输出每步排序结果
    for(int i = 0;i < 10;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");

    return low;
}

void QuickSort(int *arr,int low,int high)
{
    int k = Partition(arr,low,high);
    if(low < k-1)
    {
        QuickSort(arr,low,k-1);
    }
    if(high > k+1)
    {
        QuickSort(arr,k+1,high);
    }
}
void Show(int *arr,int len)
{
    for(int i = 0;i < len;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[] = {12,34,45,21,4,6,33,53,15,5};
    int len = sizeof(arr)/sizeof(arr[0]);
    printf("       排序前:");
    Show(arr,len);
    QuickSort(arr,0,len-1);
    printf("       排序后:");
    Show(arr,len);
}

运行结果:

排序算法 | 快速排序算法及其优化_第4张图片

(2)随机基准法

#include
#include
#include

void Show(int *arr,int len);
void swap(int *arr,int low,int high)
{
  int tmp = arr[low];
  arr[low] = arr[high];
  arr[high] = tmp;
}

int Partition(int *arr, int low, int high)  // 快排一次划分算法
{
    srand((unsigned)time(NULL));
    swap(arr,low,low + rand() % (high-low+1));
    printf("随机基准:%2d,",arr[low]);

    static int n=1; // 记录排序次数
    int temp = arr[low];
    while(low < high)
    {
        while(low < high && arr[high] >= temp){high--;}
        arr[low] = arr[high];
        while(low < high && arr[low] <= temp){low++;}
        arr[high] = arr[low];
    }
    arr[low] = temp;   

    printf("第%d步排序结果:",n++);  // 输出每步排序结果
    for(int i = 0;i < 10;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");

    return low;
}

void QuickSort(int *arr,int low,int high)
{
    int k = Partition(arr,low,high);
    if(low < k-1)
    {
        QuickSort(arr,low,k-1);
    }
    if(high > k+1)
    {
        QuickSort(arr,k+1,high);
    }
}

void Show(int *arr,int len)
{
    for(int i = 0;i < len;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");
}


int main()
{
    int arr[] = {12,34,45,21,4,6,33,53,15,5};
    int len = sizeof(arr)/sizeof(arr[0]);
    printf("                   排序前:");
    Show(arr,len);
    QuickSort(arr,0,len-1);
    printf("                   排序后:");
    Show(arr,len);
}

运行结果:

排序算法 | 快速排序算法及其优化_第5张图片

(3)三分取中法

#include

void Show(int *arr,int len);
void swap(int *arr,int low,int high)
{
  int tmp = arr[low];
  arr[low] = arr[high];
  arr[high] = tmp;
}

// arr[mid] <= arr[low] <= arr[high]
void SelectPivotMedianOfThree(int *arr,int low,int high)  
{
    int mid = (low+high) >> 1;
    if(arr[mid] > arr[low])
    {    
        swap(arr,mid,low);
    }
    if(arr[low] > arr[high])
    {
        swap(arr,low,high);
    }
    if(arr[mid] > arr[high])
    {
        swap(arr,mid,high);
    }
}

int Partition(int *arr, int low, int high)// 快排一次划分算法
{
    SelectPivotMedianOfThree(arr,low,high);
    printf("基准数为%2d,",arr[low]);
	
    static int n=1; // 记录排序次数
    int i = low;
    int j = high;
    int k = arr[low];
    while (i < j)
    {
        while(i < j && arr[j] >= k) {j--;}
        arr[i] = arr[j];
        while(i < j && arr[i] <= k) {i++;}
        arr[j] = arr[i];
    }
    arr[i] = k;

    printf("第%d步排序结果:",n++);  // 输出每步排序结果
    for(int i = 0;i < 10;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");

    return i;
}

void QuickSort(int *arr,int low,int high)
{
    int k = Partition(arr,low,high);
    if(low < k-1)
    {
        QuickSort(arr,low,k-1);
    }
    if(high > k+1)
    {
        QuickSort(arr,k+1,high);
    }
}
void Show(int *arr,int len)
{
    for(int i = 0;i < len;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[] = {12,34,45,21,4,6,33,53,15,5};
    int len = sizeof(arr)/sizeof(arr[0]);
    printf("                  排序前:");
    Show(arr,len);
    QuickSort(arr,0,len-1);
    printf("                  排序后:");
    Show(arr,len);
}

 运行结果:

排序算法 | 快速排序算法及其优化_第6张图片

(4)插排 + 三分取中法

#include

void Show(int *arr,int len);
void swap(int *arr,int low,int high)
{
  int tmp = arr[low];
  arr[low] = arr[high];
  arr[high] = tmp;
}

// arr[mid] <= arr[low] <= arr[high]
void SelectPivotMedianOfThree(int *arr,int low,int high)  
{
    int mid = (low+high) >> 1;
    if(arr[mid] > arr[low])
    {    
        swap(arr,mid,low);
    }
    if(arr[low] > arr[high])
    {
        swap(arr,low,high);
    }
    if(arr[mid] > arr[high])
    {
        swap(arr,mid,high);
    }
}

int Partition(int *arr, int low, int high)// 快排一次划分算法
{
    SelectPivotMedianOfThree(arr,low,high);
    printf("基准数为%2d,",arr[low]);
    
    static int n=1; // 记录排序次数
    int i = low;
    int j = high;
    int k = arr[low];
    while (i < j)
    {
        while(i < j && arr[j] >= k) {j--;}
        arr[i] = arr[j];
        while(i < j && arr[i] <= k) {i++;}
        arr[j] = arr[i];
    }
    arr[i] = k;

    printf("第%d步排序结果:",n++);  // 输出每步排序结果
    for(int i = 0;i < 10;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");

    return i;
}

void InsertSort(int *arr,int low,int high)
{
    int tmp ;
    for(int i = low+1;i <= high;i++)
    {
        tmp = arr[i];
        int j = i-1;
        while (j >= low && arr[j] > tmp)
        {
            arr[j+1] = arr[j];
            j--;
        }
    arr[j+1] = tmp;
    }
}

void QuickSort(int *arr,int low,int high)
{
    if(high-low+1<10)  // 当整个序列的大小high-low+1<10时,采用插入排序
    {
        printf("\n序列大小小于10,采用直接插入排序!!\n\n");
        InsertSort(arr,low,high);
        return;
    }
    else
    {
        int k = Partition(arr,low,high);
        if(low < k-1)
        {
            QuickSort(arr,low,k-1);
        }
        if(high > k+1)
        {
            QuickSort(arr,k+1,high);
        }
    }
}
void Show(int *arr,int len)
{
    for(int i = 0;i < len;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");
}
int main()
{
    int arr[] = {12,34,45,21,4,6,33,53,15,5};
    int len = sizeof(arr)/sizeof(arr[0]);
    printf("                  排序前:");
    Show(arr,len);
    QuickSort(arr,0,len-1);
    printf("                  排序后:");
    Show(arr,len);
}

运行结果:

排序算法 | 快速排序算法及其优化_第7张图片

(5)聚集相同基准法 + 三分取中法

#include

void Show(int *arr,int len);
void swap(int *arr,int low,int high)
{
  int tmp = arr[low];
  arr[low] = arr[high];
  arr[high] = tmp;
}

// arr[mid] <= arr[low] <= arr[high]
void SelectPivotMedianOfThree(int *arr,int low,int high)  
{
    int mid = (low+high) >> 1;
    if(arr[mid] > arr[low])
    {    
        swap(arr,mid,low);
    }
    if(arr[low] > arr[high])
    {
        swap(arr,low,high);
    }
    if(arr[mid] > arr[high])
    {
        swap(arr,mid,high);
    }
}

int Partition(int *arr, int low, int high)// 快排一次划分算法
{
    SelectPivotMedianOfThree(arr,low,high);
    printf("基准数为%2d,",arr[low]);
    
    static int n=1; // 记录排序次数
    int i = low;
    int j = high;
    int k = arr[low];
    while (i < j)
    {
        while(i < j && arr[j] >= k) {j--;}
        arr[i] = arr[j];
        while(i < j && arr[i] <= k) {i++;}
        arr[j] = arr[i];
    }
    arr[i] = k;

    printf("第%d步排序结果:",n++);  // 输出每步排序结果
    for(int i = 0;i < 10;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");

    return i;
}

void FocusNumPar(int *arr,int low,int par,int high,int *left,int *right)
{
    int parLeft = par-1;
    if(low < high)
    {
        for(int i=par-1;i>=low;i--)
        {
            if(arr[i]==arr[par])
            {
                if(i!=par)
                {
                    swap(arr,i,parLeft);
                    parLeft--;
                }
                else
                {
                    i--;
                }
            }
        }
        *left = parLeft;

        int parRight = par+1;
        for(int i=par+1;i<=high;i++)
        {
            if(arr[i]==arr[par])
            {
                if(i!=par)
                {
                    swap(arr,i,parRight);
                    parRight++;
                }
                else
                {
                    i++;
                }
            }
        }
        *right = parRight;
    }
}

void QuickSort(int *arr,int low,int high)
{
    int left = 0;
    int right = 0;
    int k = Partition(arr,low,high);	
    FocusNumPar(arr,low,k,high,&left,&right);
    if(low <= left-1)
    {
        QuickSort(arr,low,left);
    }
    if(high >= right+1)
    {
        QuickSort(arr,right,high);
    }
}
void Show(int *arr,int len)
{
    for(int i = 0;i < len;i++)
    {
        printf("%5d",arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[] = {1,4,6,7,6,6,7,6,8,6};
    int len = sizeof(arr)/sizeof(arr[0]);
    printf("                  排序前:");
    Show(arr,len);
    QuickSort(arr,0,len-1);
    printf("                  排序后:");
    Show(arr,len);
}

运行结果:

 排序算法 | 快速排序算法及其优化_第8张图片

你可能感兴趣的:(C,GS—C,C,程序设计,算法与数据结构)