快速排序及优化

冒泡排序和快速排序都属于交换排序,其中快排更像是冒泡的进化版本,我们先看冒泡排序:

思想:每轮循环将本轮最大元素移动到数组末尾,以此类推,直到长度为一。假设数组长度为 n,第一轮循环将最大元素移动到下标为 n-1 的位置,第二轮需要判断前 n -1 个元素,并将最大元素移动到 n-2 的位置,以此类推。计算最大值通过比较交换完成。

private void sort(int[] a) {
	// 长度为 n 的数组需要 n-1 轮循环
    for (int i = 1; i <= a.length - 1; i++) {
    	// 每次需要判断到第几个元素
        for (int j = 0; j < a.length - i; j++) {
            if (a[j] > a[j + 1]) {
                int k = a[j];
                a[j] = a[j + 1];
                a[j + 1] = k;
            }
        }
    }
}

快排在冒泡的基础上做了优化,它不在局限于每轮找最大的或最小的,而是找比它小的和比它大的。每次将比它小的元素放在自身左边,比它大的元素放在右边,从全局层面保证有序。

思想:以数组下标开始元素为基准,比较交换。交换后左边都是比自己小的元素,右边都是比自己大的元素。左集合和右集合再分别进行上述判断,直到集合中只有一个元素。

private void quickSort(int[] a, int left, int right) {
    if (left >= right) {
        return;
    }
    int index = sort(a, left, right);
    quickSort(a, left, index - 1);
    quickSort(a, index + 1, right);
}

private int sort(int[] a, int left, int right) {
    int key = a[left];
    while (left < right) {
        while (left < right && a[right] >= key) {
            right--;
        }
        a[left] = a[right];
        while (left < right && a[left] <= key) {
            left++;
        }
        a[right] = a[left];
    }
    a[left] = key;
    return left;
}

从代码可以看出:quickSort() 计算出的 index 越靠近中心,递归的次数越少,性能越好。如果每次计算出的中点恰好都在中间,那时间复杂度仅为 O(nlog2n)。如果每次计算出的中点恰好都在边沿,时间复杂度为 O(n^2),和冒泡排序相等。

通常情况下由于数组无序,冒泡排序的时间复杂度约处于 O(nlog2n)。但当数组本身有序时,由于快排总拿第一个元素作为基准数,这样计算出的 index 总在边沿,时间复杂度非常高。为了解决该问题,可以从基准数的选择优化快排,通常有两种方式优化:

  • 三数取中间数
  • 随机选基准数

优化后的代码如下:

// 三数取中法
int x1 = a[left], x2 = a[right], x3 = a[(left + right) / 2];
if (x1 >= x2) {
    if (x2 >= x3) {
        return x2;
    } else {
        return Math.min(x1, x3);
    }
} else {
    if (x1 >= x3) {
        return x1;
    } else {
        return Math.min(x2, x3);
    }
}
// 随机选基准数
int key = a[left + new Random().nextInt(right - left + 1)];

new Random().nextInt(n):返回小于 n 大于等于 0 的所有整数

只要保证选择的基准数尽可能的可以将数组划分为平均的两部分,就可以提高快排的性能


quickSort() 在满意 left <= right 条件时总要进行递归计算,当数组长度很小时,快排的效率可能不如冒泡排序,因为此时时间复杂度相等但快排涉及到创建栈的消耗。此时就可以优化,当数组长度很小时,直接采用冒泡排序:

// 数组长度小于等于5时不采用快排
private void quickSort(int[] a, int left, int right) {
    if (left >= right) {
        return;
    }
    if(right - left >= 5) {
    	int index = sort(a, left, right);
		quickSort(a, left, index - 1);
		quickSort(a, index + 1, right);
    } else {
    	// 采用冒泡排序
    	sort(a, left, right);
    }
}

假设数组中存在多个和基准数相同的数,目前的逻辑相等不交换,从全局维度来说,划分的并不是很干净,举个例子:

55326458
经过一次快排后,分割为:4532 5 658

从结果来说,虽然左边全部是小于等于它的,右边全部是大于等于它的,但两者都包含和相等值,后续递归过程中仍需判断。我们可以想办法让相等的值集中在中间,这样下轮循环就可以不判断这部分值了,拿上面的例子来说,理想状态为分割为 432 555 68,下次只需递归 432 和 68 集合即可,下面我给出聚合以及跳过的逻辑:

private void quickSort(int[] a, int left, int right) {
    if (left >= right) {
        return;
    }
    // 越过相等的基准数值
    int index = sort(a, left, right), l = index - 1, r = index + 1;
    while (l > left && a[l] == a[index]) {
        l--;
    }
    while (r < right && a[r] == a[index]) {
        r++;
    }
    quickSort(a, left, l);
    quickSort(a, r, right);
}

private int sort(int[] a, int left, int right) {
    int key = a[left], l = left, r = right;
    while (left < right) {
        while (left < right && a[right] >= key) {
            right--;
        }
        a[left] = a[right];
        while (left < right && a[left] <= key) {
            left++;
        }
        a[right] = a[left];
    }
    a[left] = key;
    // 交换逻辑
    int p = left - 1, q = right + 1;
    while (l < p) {
        if (a[l] == key) {
            while (p > l && a[p] == key) {
                p--;
            }
            sawp(a, l, p);
        }
        l++;
    }
    while (r > q) {
        if (a[r] == key) {
            while (q < r && a[q] == key) {
                p++;
            }
            sawp(a, r, q);
        }
        r--;
    }
    return left;
}

private void sawp(int[] n, int x, int y) {
    if (x == y) {
        return;
    }
    int z = n[x];
    n[x] = n[y];
    n[y] = z;
}

至此关于快排的三种优化介绍完毕:

  1. 基准数选择优化
  2. 小数组采用其它排序方式,如插入、冒泡
  3. 基准数相等聚合跳过

最后由于快排采用分治的思想,单次任务实际上可以分配到不同的线程执行:对于每次任务,创新新线程执行,线程体就是快排的实现逻辑,把递归改为创建新线程执行即可,充分发挥 CPU 效率:

class DemoThread implements Runnable {

    private int[] a;
    int left;
    int right;

    public DemoThread(int[] a, int left, int right) {
        this.a = a;
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        int index = sort(a, left, right);
        if (left < index - 1) {
            new Thread(new DemoThread(a, left, index - 1)).start();
        }
        if (index + 1 > right) {
            new Thread(new DemoThread(a, index + 1, right)).start();
        }
    }

    private int sort(int[] a, int left, int right) {
        int key = a[left];
        while (left < right) {
            while (left < right && a[right] >= key) {
                right--;
            }
            a[left] = a[right];
            while (left < right && a[left] <= key) {
                left++;
            }
            a[right] = a[left];
        }
        a[left] = key;
        return left;
    }
}

一般情况下,数组不大时没必要创建,因为创建线程本身也有资源消耗,并且还有上下文切换的消耗,绝大多数情况下单线程运行就可以了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值