快速排序-两种分割方法比较(Hoare’s vs. Lomuto)

快速排序-两种分割方法比较(Hoare’s vs. Lomuto)

  1. 前言

快速排序是一种有效的排序方法,它可以很好平衡时间复杂度,获取不错的排序效果。快速排序从本质上理解,它和冒泡法一样,属于比较排序方法的范畴,冒泡方法中,每次只能相邻两个数据比较,快速排序每次把比较范围扩大到两个维度,从同一侧分为两个队列进行比较(Lomuto)或者从两侧向中间进行合围。快速排序类似双向冒泡,其中一个队列网上冒泡,另外要给队列向下冒泡,这样就显著提升了比较效率,减少循环比较次数。

  1. 快速排序回顾

快速排序采用递归的分治方法,首先找到一个支点数据,对数组中的所有数据进行归类,小于等于支点数据的对象放置到左侧,大于支点的数据对象放置到右侧,一直循环到单个数据对象位置。其主题实现代码,非常简洁。

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),由于各个子数组已经有序,所以不需要进行特别的组合和动作,递归后返回的结果自然满足要求
  1. 分割函数分析

快速排序算法的关键函数是分割函数 (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),直至两个指针指向相同的位置

ComparisonHoare 分割算法Lomuto 分割算法
支点选择通常情况下,选择第一个元素作为支点,当然也有选择中间或最后元素最为支点的情况通常最后一个元素作为支点,可以随机选择支点,随机选择支点后,仅需和最后一个元素交换即可
算法复杂度线性算法线性算法
算法速度相对较快相对较慢
理解难易度相对困难相对容易
支点是否固定过程中支点随时变化支点固定
  1. 代码实现

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

  1. 总结

通过深入学习,更加深入理解两类分割函数对快速排序的影响,同时也回顾了快速排序的分治思想和实现要点。同时对双指针操作数组也有一个全新的认识。

参考资料

  1. Hoare’s vs Lomuto partition scheme in QuickSort - GeeksforGeeks
  2. 《Introduction to algorithm, 4ed》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值