排序算法之——快速排序分析

引言

本篇的主题是快速排序,它可能是应用最广泛的算法了。它的平均运行时间是 O ( N l o g N ) O(NlogN) O(NlogN),最坏情形性能为 O ( N 2 ) O(N^2) O(N2),但只需做一点改进就可以使最坏情形很难出现。

public static void sort(List<Integer> items) {
     if (items.size() > 1) {
         List<Integer> smaller = new ArrayList<>();
         List<Integer> same = new ArrayList<>();
         List<Integer> larger = new ArrayList<>();
         Integer pivot = items.get(items.size() / 2);//取得哨兵
         for (Integer item : items) {
             if (item < pivot) {
                 smaller.add(item);
             } else if (item > pivot) {
                 larger.add(item);
             } else {
                 same.add(item);
             }
         }
         sort(smaller);//递归的对左半部分执行排序
         sort(larger);
         items.clear();//清空items
         //注意add的顺序
         items.addAll(smaller);
         items.addAll(same);
         items.addAll(larger);
     }
 }

上面的这种算法应用了快速排序的思想,然而会产生额外的列表。只是作为一个开胃菜,下面开始解释快排的思想。

思路

  • 取S中某个元素作为pivot(哨兵)
  • 通过pivot将序列分为两个子序列 S = S L + S R S = S_L + S_R S=SL+SR 其中 S L &lt; = p i v o t S_L&lt;=pivot SL<=pivot同时 S R &gt; = p i v o t S_R&gt;=pivot SR>=pivot
  • 在子序列分别递归地排序后,原序列自然有序 s o r t e d ( S ) = s o r t e d ( S L ) + p i v o t + s o r t e d ( S R ) sorted(S) = sorted(S_L) + pivot + sorted(S_R) sorted(S)=sorted(SL)+pivot+sorted(SR)
  • 返回条件:只剩下单个元素时,本身就是解,递归函数达到返回条件

选取哨兵

本文介绍的方法是首先将待排序序列打乱,防止待排序序列是基本有序的而产生劣质的分隔,从而导致性能偏向最坏情况( O ( N 2 ) O(N^2) O(N2))。

将序列打乱之后,选择第一个元素作为哨兵。

分割策略

分割阶段要做的是将所有小元素移到数组的左边,把大元素移到数组的右边。小和大是相对于哨兵元素而言的。

还有,如果i和j遇到等于哨兵元素的关键字,那么我们让i和j都停止。

小数组

对于很小的数组(array.length <= 20),快速排序不如插入排序。因为快排是递归分解的,总会遇到小数组情况。
因此,当是小数组时,我们采用插入排序而不是快排。

上面说小于20的数组示小数组,其实5到20之内都可以。我们定义截止范围(cutoff) 为10。

下面通过实例图解来分析一下快排。为了简单,我们只分析第一趟分割过程,并且假设不采用优化操作(打乱数组操作以及小数组转化为插入排序)。

在这里插入图片描述

原始数组如上。
在这里插入图片描述
我们选择第一个元素6作为哨兵节点,同时定义两个指针iji指向0,j指向数组最后一个元素再下一个元素(为了方便--j操作)。

接下来就开始我们的分割过程,j从右边往左开始扫描,直到扫描到小于或等于哨兵节点的元素(分割策略:等于的哨兵的元素也停止)。

在这里插入图片描述
j停止后,我们让i 从左往右扫描,直到遇到大于或等于哨兵节点的元素
在这里插入图片描述
此时i也停止了,这时需要判断i是否在j前面(i<j)。若满足,则交换ij的位置,
再从j的位置开始向左扫描。

在这里插入图片描述

重复上述过程,直到i指向9,j指向5。然后交换它们的位置。
在这里插入图片描述
继续

在这里插入图片描述

j往前移1步,就遇到了元素4,停了下来。转到i,i向右走,直到元素9,停了下来。注意,此时i>j,并且i指向的是大于哨兵元素的节点。
因此,我们只需要将j指向的元素和哨兵元素互换位置

在这里插入图片描述

这样,哨兵左边的元素都不大于它,哨兵右边的元素都不小于它。
接下来,我们只需要对左边的子数组以及右边的子数组进行同样的过程,直到只剩下单个元素时,递归返回(未优化)。

代码

交换数组元素代码:

private static <E> void swap(E[] array, int i, int j) {
    if (i == j) return;
    E tmp = array[i];
    array[i] = array[j];
    array[j] = tmp;
}

洗牌打乱数组代码:

/**
 * 打乱数组a中的元素
 * <p>
 * 从0到i之间生成一个随机数,然后交换i与这个随机数的位置
 * @param a
 */
private static void shuffle(Object[] a) {
    int N = a.length;
    for (int i = 0; i < N; i++) {
        int r = uniform(i + 1);//从0到i之间生成一个随机数,然后交换i与这个随机数的位置
        swap(a, i, r);
    }
}

/**
 * 返回[0,n)之间的整数
 *
 * @param n
 * @return
 */
private static int uniform(int n) {
    return (int) (Math.random() * n);
}

经典版本


public static <E extends Comparable<? super E>> void quickSort(E[] a) {
     shuffle(a);//洗牌
     quickSort(a, 0, a.length - 1);//注意传入的是a.length - 1
}

/**
 * @param a
 * @param left
 * @param right 指向待排序子数组末端元素
 * @param <E>
 */
private static <E extends Comparable<? super E>> void quickSort(E[] a, int left, int right) {
    if (left < right) {//条件不能是 left <= right

        int i = left, j = right + 1;//左边第一个元素作为哨兵节点:a[left]  j为right + 1是为了--j的写法
        while (true) {
            /**
             * 此算法先从左往右还是从右往左都没关系!
             */
            //从右边往左开始扫描,直到扫描到小于或等于哨兵节点的元素
            while (a[--j].compareTo(a[left]) > 0) {
                /*if (j == left) { //该端点检查是多余的,因为哨兵在左端,当j指向哨兵时不会大于哨兵自己,循环也就跳出了
                    break;
                }*/
            }
            // 从左往右扫描,直到遇到大于或等于哨兵节点的元素 若条件是left <= right a[++i]会报索引越界异常
            while (a[++i].compareTo(a[left]) < 0) {
                if (i == right) {//端点检查
                    break;
                }
            }

            if (i < j) {
            	//交换元素,继续扫描
                swap(a, i, j);
            } else {
                //i、j交错则退出最外层的while循环
                break;
            }
        }
        //哨兵节点放入此位置即可 此时a[j]左边的元素都比它要小,右边的要大
        //该算法以下只能用j而不是i ,因为i最后会扫描到比哨兵大的元素而停止,不能用比哨兵大的元素和left交换位置,而j是比哨兵小的元素
        //System.out.println("a[j=" + j + "] = " + a[j] + ",a[i=" + i + "]=" + a[i]);
        swap(a, j, left);
        //j元素已经就位了
        quickSort(a, left, j - 1);
        quickSort(a, j + 1, right);
    }
}

优化版本

主要是针对小数组采用直接插入排序:

private static <E extends Comparable<? super E>> void insertionSort(E[] a, int left, int right) {
    int j;
    for (int i = left + 1; i <= right; i++) { //注意i的初始值以及i的判断条件
        E tmp = a[i];//对于位置i,先把i位置对应的值保存起来,然后把i挖空
        for (j = i; j > left && tmp.compareTo(a[j - 1]) < 0; j--) {
            a[j] = a[j - 1];//若发现i对应的值小于某个位置的值,则将该位置的值往后移动一位;
        }
        a[j] = tmp;//最后将tmp填入空位
    }
}

关于插入排序可以看博客插入排序分析

/**
 * 对于很小的数组(比如元素个数小于10),快速排序不如插入排序
 */
private static final int CUTOFF = 10;

/**
 * @param a
 * @param left
 * @param right 指向带排序子数组末端元素
 * @param <E>
 */
private static <E extends Comparable<? super E>> void quickSort(E[] a, int left, int right) {
    if (left + CUTOFF <= right) {//判断是否为小数组

        int i = left, j = right + 1;//左边第一个元素作为哨兵节点:a[left] j为right + 1是为了--j的写法
        while (true) {
            /**
             * 此算法先从左往右还是从右往左都没关系!
             */
            //从右边往左开始扫描,直到扫描到小于或等于哨兵节点的元素
            while (a[--j].compareTo(a[left]) > 0) {
                /*if (j == left) { //该端点检查是多余的,因为哨兵在左端,当j指向哨兵时不会大于哨兵自己,循环也就跳出了
                    break;
                }*/
            }
            // 从左往右扫描,直到遇到大于或等于哨兵节点的元素
            while (a[++i].compareTo(a[left]) < 0) {
                if (i == right) {//端点检查
                    break;
                }
            }

            if (i < j) {
                swap(a, i, j);
            } else {
                break;
            }
        }
        //哨兵节点放入此位置即可 此时a[j]左边的元素都比它要小,右边的要大
        //该算法以下只能用j而不是i ,因为i最后会扫描到比哨兵大的元素而停止,不能用比哨兵大的元素和left交换位置,而j是比哨兵小的元素
        System.out.println("a[j=" + j + "] = " + a[j] + ",a[i=" + i + "]=" + a[i]);
        swap(a, j, left);
        quickSort(a, left, j - 1);
        quickSort(a, j + 1, right);
    } else {
        //小数组直接用插入排序
        insertionSort(a, left, right);
    }
}

测试代码

检查有序性代码

private static <E extends Comparable<? super E>> void checkSorted(E[] a) {
    if (a.length == 1) return;

    for (int i = 0; i <= a.length - 2; i++) {
        if (a[i].compareTo(a[i + 1]) > 0) {
            System.out.println("a[" + i + "]=" + a[i] + " larger than a[" + (i + 1) + "]=" + a[i + 1]);
            throw new IllegalStateException("Not ordered.");
        }
    }
}

对随机数组进行排序:

private static void sortRandArray() {
    Random rand = new Random();
    Integer[] array = rand.ints(1000, 1, 300000).boxed()
            .collect(Collectors.toList()).toArray(new Integer[]{});
    System.out.println(Arrays.toString(array));
    quickSort(array);
    System.out.println(Arrays.toString(array));
    checkSorted(array);
}

对有序数组进行排序:

private static void sortFixedArray() {
	Integer[] array = IntStream
                        .range(1,100)
                        .boxed()
                        //.sorted(Collections.reverseOrder())  //解除掉这个注释就是逆序数组
                        .toArray(Integer[]::new);
    System.out.println(Arrays.toString(array));
    quickSort(array);
    checkSorted(array);
    System.out.println(Arrays.toString(array));
}

测试:

public static void main(String[] args) {
    sortFixedArray();//同时测试了顺序数组和逆序数组
    for (int i = 0; i < 10; i++) {
        sortRandArray();
    }
}

经过测试,快排代码没有问题。请放心使用,若有问题,请留言指出。

选择问题

快速找出第k小元素的问题。

利用快速排序获取哨兵节点的代码来写。可以不用真正排序而得到第k小元素,平均运行时间为 O ( n ) O(n) O(n)

思路

  • 首先得到哨兵节点的索引j
  • 如果j > k 说明一定在小于哨兵节点的左边子数组中,因此急需在左边查找
  • 否则如果j < k则在右边查找
  • 否则j == k 则直接返回

代码

/**
 * 找到第k小的元素
 * @param a
 * @param k 从0开始 0表示第1小的元素,1表示第2小的元素
 * @param <E>
 * @return
 */
public static <E extends Comparable<? super E>> E quickSelect(E[] a, int k) {
    if (k < 0 || k > a.length - 1) {
        throw new IllegalStateException("invalid k");
    }
    shuffle(a);
    int left = 0, right = a.length - 1;
    while (left < right) {
        int j = partition(a, left, right);//左边的比a[j]小,右边的比a[j]大
        if (j > k) {
            right = j - 1;//在左边找
        } else if (j < k) {
            left = j + 1;//在右边找
        } else {
            //j == k
            return a[k];
        }
    }
    //当left == right == k时
    return a[k];
}

/**
 * 抽取获取哨兵节点过程
 *
 * @param a
 * @param left
 * @param right
 * @return 哨兵节点的位置
 */
private static <E extends Comparable<? super E>> int partition(E[] a, int left, int right) {
    int i = left, j = right + 1;//左边第一个元素作为哨兵节点:a[left] j为right + 1是为了--j的写法
    E pivot = a[left];
    while (true) {
        while (a[--j].compareTo(pivot) > 0) {
        }
        // 从左往右扫描,直到遇到大于或等于哨兵节点的元素 若条件是left <= right a[++i]会报索引越界异常
        while (a[++i].compareTo(pivot) < 0) {
            if (i == right) {//端点检查
                break;
            }
        }

        if (i < j) {
            swap(a, i, j);
        } else {
            break;
        }
    }
    swap(a, j, left);
    return j;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值