快速排序-两种分割方法比较(Hoare’s vs. Lomuto)
- 前言
快速排序是一种有效的排序方法,它可以很好平衡时间复杂度,获取不错的排序效果。快速排序从本质上理解,它和冒泡法一样,属于比较排序方法的范畴,冒泡方法中,每次只能相邻两个数据比较,快速排序每次把比较范围扩大到两个维度,从同一侧分为两个队列进行比较(Lomuto)或者从两侧向中间进行合围。快速排序类似双向冒泡,其中一个队列网上冒泡,另外要给队列向下冒泡,这样就显著提升了比较效率,减少循环比较次数。
- 快速排序回顾
快速排序采用递归的分治方法,首先找到一个支点数据,对数组中的所有数据进行归类,小于等于支点数据的对象放置到左侧,大于支点的数据对象放置到右侧,一直循环到单个数据对象位置。其主题实现代码,非常简洁。
void quick_sort(int *a, int p, int r)
{
int q;
if(p<r) //It should define the exit entry of recursive
{
// Partition the subarray around the pivot, which ends up in A[q].
q = partition(a, p, r);
quick_sort(a, p, q - 1); // recursively sort the low side
quick_sort(a, q + 1, r); // recursively sort the high side
}
}
如果采用《算法导论》的递归分析流程,分为三个阶段,我们分别对其进行相关的分析
- 分割(Divide), 过程中,需要把数组分割为两个子数组。整体的数组表示为a[p,r],以支点为分界点,整体需要分割为两个子数组,分别表示为a[p,q-1], a[q+1,r]两个数组。a[p,q-1]为低端侧的数组,数组里面的所有的值,均小于或等于支点a[q]; a[q+1,r]为高端侧的数组,数组里面所有的元素值均大于a[q]。
- 治理(Conquer),值得一提的是,两个子数组此时可能处于无序状态,需要继续分割,直至至单个元素出现。这个过程中,我们需要对子数组递归调用子数组,直至最终的结果出现。
- 组合(Combine),由于各个子数组已经有序,所以不需要进行特别的组合和动作,递归后返回的结果自然满足要求
- 分割函数分析
快速排序算法的关键函数是分割函数 (partition function),它肩负着分割数组以及返回支点下标的功能,这个函数的性能对整体快速排序算法至关重要。一般情况下,分割函数的实现分为两大类,
- 一类是Lomuto算法,它以最后一个元素为支点,从左边开始对数组进行分割,整个分割过程中,支点位置保持不变,分割完成后,对支点和i+1的值进行交换
- 一类是Hoare算法,它从双侧(i侧和j侧_开始进行比较,过程中分割点的位置会不停的变化,直至i=j,结束迭代运算
首先从Lomuto算法开始,Lomuto算法过程演示图,
(a) 表示原始数组,为了便于理解,把i,j当作指针理解,迭代之前,指针i指向p-1;指针j指向p;这个设计非常巧妙,确保i指针和j指针形成有重叠的两个区域,扩展成“你追我赶”的最终局面
(b) 由于a[j]<=a[r](a[0]<a[7]), 我们对a[0]和a[0]进行首次交换,同时指针i和指针j各自往前移动一步
© 由于a[j]>=a[r](a[2]>a[7]),我们不做任何交换,保留i指针在原为,同时j指针往前移动一步,中间过程遵循相同逻辑
…
(h)此时可以看到整个数组已经由四个区(低侧,高侧,未排序,支点)减少到三个区(底侧,高侧,支点),这时候未排序区域消失
(i) 交换a[i+1]和a[r],分割完成,并返回i+1作为支点的下标
过程中的两类操作,(a)类操作,当a[j]>a[r],i指针保持不动,j指针增加1,没有交换发生;(b)类操作a[j]<=a[r],需要交换数据,完成后i和j指针都往前移动1位(增加1)
接着我们再探索一下Hoare分割方法,与Lomuto方法不同,Hoare 算法的数据比较从两边开始比较,如果起始指针用i表示,末端指针用j表示,过程中两个指针,逐渐向中间靠拢,最后i和j指针重合,从而分割结束,返回i值作为支点的下标。在这个过程中,一般选择起始元素作为支点,支点的位置会随着比较而不停变化。
逐行分析Hoare分割算法的过程,
(a) 原始数组,i指针指向数组头部,j指针指向数组的尾部,规定第一个元素(橙色)为支点(轴点)
(b) 从尾部开始操作指针,直至a[i]>a[j](2>1)
©交换i和j指向的数值,同时i指针向前移动1位
(d)从头部开始操作指针,比较a[i]和a[j]的值,直至a[i]>a[j](8>2)
(e)交换两个数值,循环(a)~(e),直至两个指针指向相同的位置
Comparison | Hoare 分割算法 | Lomuto 分割算法 |
---|---|---|
支点选择 | 通常情况下,选择第一个元素作为支点,当然也有选择中间或最后元素最为支点的情况 | 通常最后一个元素作为支点,可以随机选择支点,随机选择支点后,仅需和最后一个元素交换即可 |
算法复杂度 | 线性算法 | 线性算法 |
算法速度 | 相对较快 | 相对较慢 |
理解难易度 | 相对困难 | 相对容易 |
支点是否固定 | 过程中支点随时变化 | 支点固定 |
- 代码实现
Hoare 分割算法,分为头文件,函数实现和测试
a) 头文件quick_sort.h
/**
* @file quick_sort.h
* @author your name (you@domain.com)
* @brief Use hoare method to partition the array
* @version 0.1
* @date 2023-03-12
*
* @copyright Copyright (c) 2023
*
*/
#ifndef QUICK_SORT_H
#define QUICK_SORT_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**
* @brief Use recursive method to finish the quick sort
*
* @param a Array list
* @param p Start position of array
* @param r End position of array
*/
void quick_sort(int *a, int p, int r);
/**
* @brief Partion a into two sections by using hoare method(from two SIDES)
* Move from two sides to the center, pivot will be the first element
* @param a Array list
* @param p Start position of array
* @param r End position of array
* @return int Return partition location
*/
int partition_hoare(int *a,int p,int r);
/**
* @brief Swap two elements in the array
*
* @param p First element in the array
* @param q Second element in the array
* @return void
*/
void swap(int *p, int *q);
/**
* @brief Show the array list
*
* @param a Array list
* @param n Number of array element
*/
void show_array(int *a, int n);
#endif
b)函数实现
/**
* @file quick_sort.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-03-11
*
* @copyright Copyright (c) 2023
*
*/
#ifndef QUICK_SORT_C
#define QUICK_SORT_C
#include "quick_sort.h"
void quick_sort(int *a, int p, int r)
{
int q;
if(p<r) //It should define the exit entry of recursive
{
// Partition the subarray around the pivot, which ends up in A[q].
q = partition_hoare(a, p, r);
quick_sort(a, p, q - 1); // recursively sort the low side
quick_sort(a, q + 1, r); // recursively sort the high side
}
}
int partition_hoare(int *a, int p, int r)
{
int i;
int j;
i=p;
j=r;
while(i<j)
{
while(i<j && a[i]<=a[j])
{
j--;
}
if(i<j)
{
swap(a+i,a+j);
i++;
}
while(i<j &&a[i]<=a[j])
{
i++;
}
if(i<j)
{
swap(a+i,a+j);
j--;
}
}
return i;
}
void swap(int *p, int *q)
{
int temp;
temp=*p;
*p=*q;
*q=temp;
}
void show_array(int *a, int n)
{
int i;
for(i=0;i<n;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
#endif
c)测试函数
/**
* @file quick_sort_main.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-03-12
*
* @copyright Copyright (c) 2023
*
*/
#ifndef QUICK_SORT_MAIN_C
#define QUICK_SORT_MAIN_C
#include "quick_sort.c"
int main(void)
{
int a[]={2,8,7,1,3,5,6,4};
int n=sizeof(a)/sizeof(int);
int r;
int p;
p=0;
r=n-1;
quick_sort(a,p,r);
show_array(a,n);
printf("The program is ending\n");
getchar();
return EXIT_SUCCESS;
}
#endif
Lomuto 分割算法,分为头文件,函数实现和测试
a) 头文件
/**
* @file quick_sort.h
* @author your name (you@domain.com)
* @brief Use Lomuto method to partition the array
* @version 0.1
* @date 2023-03-11
*
* @copyright Copyright (c) 2023
*
*/
#ifndef QUICK_SORT_H
#define QUICK_SORT_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**
* @brief Use recursive method to finish the quick sort
*
* @param a Array list
* @param p Start position of array
* @param r End position of array
*/
void quick_sort(int *a, int p, int r);
/**
* @brief Partion a into two sections by using lomuto method(from ONE SIDE)
* Pivot element had been fixed before the exchange occurence
* @param a Array list
* @param p Start position of array
* @param r End position of array
* @return int Return partition location
*/
int partition_lomuto(int *a,int p,int r);
/**
* @brief Swap two elements in the array
*
* @param p First element in the array
* @param q Second element in the array
* @return void
*/
void swap(int *p, int *q);
/**
* @brief Show the array list
*
* @param a Array list
* @param n Number of array element
*/
void show_array(int *a, int n);
#endif
b)函数实现
/**
* @file quick_sort.c
* @author your name (you@domain.com)
* @brief
* @version 0.1
* @date 2023-03-11
*
* @copyright Copyright (c) 2023
*
*/
#ifndef QUICK_SORT_C
#define QUICK_SORT_C
#include "quick_sort.h"
void quick_sort(int *a, int p, int r)
{
int q;
if(p<r) //It should define the exit entry of recursive
{
// Partition the subarray around the pivot, which ends up in A[q].
q = partition_lomuto(a, p, r);
quick_sort(a, p, q - 1); // recursively sort the low side
quick_sort(a, q + 1, r); // recursively sort the high side
}
}
int partition_lomuto(int *a, int p, int r)
{
int i;
int j;
for(i=p-1,j=p;j<r;j++)
{
if(a[j]<=a[r])
{
i=i+1;
swap(a+i,a+j);
}
}
swap(a+(i+1),a+r);
return (i+1);
}
void swap(int *p, int *q)
{
int temp;
temp=*p;
*p=*q;
*q=temp;
}
void show_array(int *a, int n)
{
int i;
for(i=0;i<n;i++)
{
printf("%d ",a[i]);
}
printf("\n");
}
#endif
c) 测试函数,同Hoare
- 总结
通过深入学习,更加深入理解两类分割函数对快速排序的影响,同时也回顾了快速排序的分治思想和实现要点。同时对双指针操作数组也有一个全新的认识。
参考资料
- Hoare’s vs Lomuto partition scheme in QuickSort - GeeksforGeeks
- 《Introduction to algorithm, 4ed》