quick sort的几种实现

本文介绍了快速排序的基本过程,并探讨了传统实现、含有重复元素的数组排序及非递归实现等不同场景下的优化方法。

简介

    之所以要写点和quick sort相关的,主要是因为我们很多时候只是关注一下某些问题的一个标准答案。实际上在我们碰到不同的情形,在原有问题的基础上做一点小小的变动,会带来更理想的结果。这里针对传统的实现,有相同元素的实现和非递归的实现做了一个探讨。

 

第一种实现

    我们知道quick sort的过程其实描述还是比较简单的。它主要就是挑选一个中间值,通过partition方法将整个数组划分成小于这个值和大于这个值的两个部分。然后再针对这两个部分递归的去排序。因此从一个宏观的角度来说,一个典型的实现如下:

void quickSort(int[] a, int l, int r){
    if(l < r) {
        int q = newPartition(a, l, r);
        quickSort(a, l, q - 1);
        quickSort(a, q + 1, r);
    }
}

    因为是递归调用的,我们每次取值的范围就是在中间值划分后的结果里。partition的实现如下:

int partition(int[] a, int l, int r) {
    int x = a[r];
    int i = l - 1;
    for(int j = l; j < r; j++) {
        if(a[j] <= x) {
            i++;
            swap(a, i, j);
        }
    }
    swap(a, i + 1, r);
    return i + 1;
}

   这里我们是取的这个数组里最右边的那个值来划分整个数组。这里的数字i表示小于或等于给定值的范围。每次碰到小于等于给定值的时候就递增i,然后交换当前元素和i所在元素的位置。我们都知道,quick sort的时间复杂度平均为O(NlogN),在最坏的情况下为O(N * N)。

    除了上面的实现方式,还要一种实现的思路,就是设定两个索引,一个指向数组的头一个指向数组的尾。假设它们分别定义为i, j,分别表示小于划分元素的索引位置和大于划分元素的索引。假设以数组的第一个元素作为划分元素的话。我们可以采用两头凑的方式。每次从左到右的去遍历比较数组,如果碰到大于划分元素的则停止,然后再看右边从右到左的去比较,碰到比划分元素小的也暂停,交换i, j两个位置的元素。继续上述的过程直到i >= j。按照这个思路,这种实现的代码如下:

int partition(int[] a, int lo, int hi) {
    int i = lo, j = hi + 1;
    while(true) {
        while(a[++i] < a[lo])
            if(i == hi) break;
        while(a[lo] < a[--j])
            if(j == lo) break;
        if(i >= j) break;
        swap(a, i, j);
    }
    swap(a, lo, j);
    return j;
}

 

 

    这种默认的情况还是比较好理解的,可是,在遇到一些拥有相同元素的情况下, 我们是否有什么办法来改进一下呢?因为这些元素如果和划分值是相同的,我们完全可以将他们集中在一块,这样可以直接将它们整个都剥离出来不用参加后面的排序,这不就间接使得需要排序的数据范围缩小了吗?这样也可以提高一点效率啊。

 

有相同值元素的实现

    按照前面的思路,我们这里需要一个元素来记录小于划分值的范围。这里肯定也需要一个元素来记录大于划分值的范围。而在它们两个值中间的不正好就是等于划分值的么?于是我们可以实现如下的代码:

public static void sort(int[] a, int lo, int hi) {
        if(hi < lo) return;
        int lt = lo, i = lo + 1, gt = hi;
        int v = a[lo];
        while(i <= gt) {
            if(a[i] < v) swap(a, lt++, i++);
            else if(a[i] > v) swap(a, i, gt--);
            else i++;
        }
        sort(a, lo, lt - 1);
        sort(a, gt + 1, hi);
    }

    这里的代码没有使用前面的那个划分方法,但是基本的思路是差不多的。我们用lt表示小于划分值的范围,gt表示大于划分值的范围。于是当给定值小于划分值的时候,lt++, i++。因为这里是取的数组最左边的元素作为划分中间值,所以lt表示等于中间值的最左边的那个元素的索引。这里最难理解清楚的是针对a[i] < v, a[i] > v和a[i] = v这3种情况。尤其要注意的就是为什么我们当a[i] < v的时候, lt,i都要加1而a[i] > v的时候只要gt--。因为我们知道当a[i] < v的时候lt++,这之后lt指向的其实已经是最左边的那个和划分值相等的元素了,而之前lt指向的元素就是划分元素v,每次递增后得到的值不可能大于v。而右边的gt所在的元素则还有可能小于v,所以每次gt--的同时还不能i++。

    详细的情况我们可以针对下图来分析:

 

非递归实现

    除了前面的两种实现,还有一个比较有意思的实现就是非递归实现。一般我们都习惯于用递归的方式去实现。但是用一些辅助存储,我们也可以用非递归的方式来实现。

    基本的思路如下,我们用一个额外的栈来保存每次划分的开头和结尾部分。每做一次划分就将两边的划分边界都保存起来。然后不断的出栈,在取出的出栈序列里如果表示右边界已经小于等于左边界了,表示这一步的划分结束。

    按照这个思路,得到的代码实现如下:

public static void iterSort(int[] a, int l, int r) {
        Stack<Integer> stack = new Stack<Integer>();
        push2(stack, l, r);
        while(!stack.empty()) {
            int left = stack.pop();
            int right = stack.pop();
            if(right <= left) continue;
            int i = partition(a, left, right);
            if(right - i > i - left) {
                push2(stack, i + 1, right);
                push2(stack, left, i - 1);
            } else {
                push2(stack, left, i - 1);
                push2(stack, i + 1, right);
            }
        }
    }

    这里还有用到一个改进,每次将划分后比较长的那一段先压栈。然后push2的实现如下:

public static void push2(Stack<Integer> stack, int l, int r) {
        stack.push(r);
        stack.push(l);
    }

    我们要注意首先入栈的是右边界,因为栈是先入后出的。

 

总结

    Quick sort的过程在一些具体的应用中还有不同的实现方法,很多细节针对有重复元素等情况体现出来的效果也是各不相同的。值得去细细的体会。

 

参考材料

Algorithms

<think>我们被要求展示Python中几种常见排序算法的实现。根据引用,我们可以选择几种经典排序算法,包括: 1. 快速排序(引用[2]和引用[5]) 2. 归并排序(引用[5]) 3. 插入排序(引用[3]) 4. 桶排序(引用[4]) 5. 基数排序(引用[4]提到,但未给出代码,我们可以补充一个简单版本) 注意:用户要求实现几种,我们选择5种,并给出每种算法的简要说明和代码。 我们将按照以下结构: 1. 快速排序 2. 归并排序 3. 插入排序 4. 桶排序 5. 基数排序 注意:代码实现将基于引用中的示例,并做适当调整和优化,确保可读性和正确性。</think>### Python 常见排序算法实现详解 以下是 Python 中 5 种经典排序算法的实现方法,包含原理说明和完整代码示例: --- #### 一、快速排序(分治法)[^2][^5] **原理**:选择一个基准值,将数组分为小于基准和大于基准的两部分,递归排序子数组。 **时间复杂度**:平均 $O(n \log n)$,最坏 $O(n^2)$ ```python def quick_sort(arr): if len(arr) <= 1: return arr pivot = arr[0] less = [x for x in arr[1:] if x < pivot] # 小于基准的子数组 greater = [x for x in arr[1:] if x >= pivot] # 大于基准的子数组 return quick_sort(less) + [pivot] + quick_sort(greater) # 示例 arr = [3, 6, 8, 10, 1, 2, 1] print(quick_sort(arr)) # 输出: [1, 1, 2, 3, 6, 8, 10] ``` --- #### 二、归并排序(分治法)[^5] **原理**:将数组递归拆分为最小单元,再两两合并有序数组。 **时间复杂度**:稳定 $O(n \log n)$ ```python def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left = merge_sort(arr[:mid]) # 左半部递归排序 right = merge_sort(arr[mid:]) # 右半部递归排序 # 合并有序数组 res, i, j = [], 0, 0 while i < len(left) and j < len(right): if left[i] <= right[j]: res.append(left[i]) i += 1 else: res.append(right[j]) j += 1 res.extend(left[i:] or right[j:]) return res # 示例 arr = [38, 27, 43, 3, 9, 82, 10] print(merge_sort(arr)) # 输出: [3, 9, 10, 27, 38, 43, 82] ``` --- #### 三、插入排序(原地排序)[^3] **原理**:将元素逐个插入已排序序列的正确位置。 **时间复杂度**:$O(n^2)$(适合小规模数据) ```python def insertion_sort(arr): for i in range(1, len(arr)): if arr[i] < arr[i-1]: j, tmp = i-1, arr[i] # 后移大于当前值的元素 while j >= 0 and arr[j] > tmp: arr[j+1] = arr[j] j -= 1 arr[j+1] = tmp # 插入正确位置 return arr # 示例 arr = [12, 11, 13, 5, 6] print(insertion_sort(arr)) # 输出: [5, 6, 11, 12, 13] ``` --- #### 四、桶排序(非比较排序)[^4] **原理**:将数据分到有限数量的桶中,每个桶单独排序后合并。 **时间复杂度**:$O(n + k)$($k$为桶数) ```python def bucket_sort(arr, bucket_size=5): if not arr: return arr # 1. 创建桶 min_val, max_val = min(arr), max(arr) bucket_count = (max_val - min_val) // bucket_size + 1 buckets = [[] for _ in range(bucket_count)] # 2. 数据分桶 for num in arr: idx = (num - min_val) // bucket_size buckets[idx].append(num) # 3. 桶内排序并合并 res = [] for bucket in buckets: res.extend(sorted(bucket)) # 可用任意排序算法 return res # 示例 arr = [0.42, 0.32, 0.87, 0.12, 0.92] print(bucket_sort(arr)) # 输出: [0.12, 0.32, 0.42, 0.87, 0.92] ``` --- #### 五、基数排序(位排序)[^1][^4] **原理**:按数字的每一位从低位到高位依次进行桶排序。 **时间复杂度**:$O(d \cdot (n + k))$($d$为最大位数) ```python def radix_sort(arr): max_digits = len(str(max(arr))) # 最大位数 for digit in range(max_digits): buckets = [[] for _ in range(10)] # 0-9的桶 # 按当前位分桶 for num in arr: d = (num // (10 ** digit)) % 10 buckets[d].append(num) # 合并桶 arr = [num for bucket in buckets for num in bucket] return arr # 示例 arr = [170, 45, 75, 90, 802, 24, 2, 66] print(radix_sort(arr)) # 输出: [2, 24, 45, 66, 75, 90, 170, 802] ``` --- ### 算法选择建议 | 算法 | 适用场景 | 优势 | |--------------|----------------------------------|-----------------------| | **快速排序** | 通用大规模数据 | 平均性能最优 | | **归并排序** | 链表排序、外部排序 | 稳定且时间复杂度稳定 | | **插入排序** | 小规模或基本有序数据 | 实现简单、原地排序 | | **桶排序** | 均匀分布的浮点数 | 线性时间复杂度 | | **基数排序** | 固定位数的整数(手机号、ID等) | 不受数据规模影响 | > 注:Python 内置的 `sorted()` 函数采用 Timsort 算法(归并+插入),在大多数场景下是最优选择[^1]。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值