七大排序及其比较

1 排序: 就是按照其中某个或某些关键字的大小,递增或递减排列起来的操作.通常我们是排升序.
2 稳定性(重要): 两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,我们称该算法是具有稳定性的算法. 冒泡 插入 归并 是稳定的排序.

3 七大排序算法(咋们都按升序排)及其比较:

(1) 插入排序: 每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入.

 public static void insertSort(int[] array) {
        // 通过 bound 来划分出两个区间, [0, bound)已排序区间, [bound, size)待排序区间
        for (int bound = 1; bound < array.length; bound++) {
            int v = array[bound];
            int cur = bound - 1;  // 已排序区间的最后一个元素下标
            for (; cur >= 0; cur--) {
 注意!!!! 这个条件如果写成 >= , 咱的插入排序就不是稳定排序了
                if (array[cur] > v) {
                    array[cur + 1] = array[cur];
                } else {
                    // 此时说明已经找到了合适的位置
                    break;
                }
            }
            array[cur + 1] = v;
        }
    }

(2) 希尔排序(插入排序的优化): 先确定一个整数gap为所分组数, gap=数组长度/2.
从第一个元素开始, 所有距离为 gap 的元素被 两两分为一组进行比较. 然后让 gap=gap/2.
直到最后 gap=1 时相当于插入排序, 排序就完成了.
时间复杂度 O(N^1.3).

 public static void shellSort(int[] array) {
        int gap = array.length / 2;
        while (gap > 1) {
            // 需要循环进行分组插排
            insertSortGap(array, gap);
            gap = gap / 2;
        }
        insertSortGap(array, 1);  -----> 当gap=1, 相当于插入排序.
    }
    private static void insertSortGap(int[] array, int gap) {
        // 通过 bound 来划分出两个区间, [0, bound)已排序区间, [bound, size)待排序区间
        // 当把gap 替换成 1 的时候, 理论上这个代码就和前面的插入排序代码一模一样.
        for (int bound = gap; bound < array.length; bound++) {
            int v = array[bound];
            int cur = bound - gap;  // 这个操作是在找同组中的上一个元素
            for (; cur >= 0; cur -= gap) {
                // 注意!!!! 这个条件如果写成 >= , 咱的插入排序就不是稳定排序了
                if (array[cur] > v) {
                    array[cur + gap] = array[cur];
                } else {
                    // 此时说明已经找到了合适的位置
                    break;
                }
            }
            array[cur + gap] = v;
        }
    }

(3) 选择排序: 以bound(待排区间的首元素) 元素作为擂主, 循环从待排序区间bound的下一个元素开始, 依次取出元素和擂主进行比较.
在一趟排序中, 如果打擂成功, 该元素就成为擂主元素. 然后下一个元素继续和新的擂主元素比较. 直到一趟排序到最后一个元素结束. 把最后更新的新的擂主元素和一开始的擂主元素进行交换.
一趟排序只交换一次.

    public static void selectSort(int[] array) {
        for (int bound = 0; bound < array.length-1; bound++) {
            int i = bound;
            for (int cur = bound + 1; cur < array.length; cur++) {
                if (array[cur] < array[bound]) {
                    i = cur;
                }
            }
            //交换i和bound
            int tmp = array[i];
            array[i] = array[bound];
            array[bound] = tmp;
        }
    }

(4) 堆排序: (排升序建大堆, 降序建小堆) 先建好堆, 比如大堆. 先把堆顶元素和堆尾元素交换, 堆尾就是最大元素了. 然后让堆的 size - - , 对堆顶元素进行一次向下调整, 直到size =1( 类似于优先级队列的出队列).

    public static void heapSort(int[] array) {
        createHeap(array);
        for (int i = 0; i < array.length - 1; i++) {
            // 当前堆的元素个数
            int heapSize = array.length - i;
            // 交换 堆顶元素和堆的 最后一个元素
            swap(array, 0, heapSize - i - 1);
            heapSize--;   //就把最后一个元素从堆中排除掉, 让堆的size-- 
            // [0, array.length-i-1)就是待排序区间  [array.length-i-1, array.length)就是已排序区间      
            shiftDown(array, heapSize, 0);  为了不破坏堆的构造,交换后再按照向下调整建堆.
        }
    }
     private static void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
    private static void createHeap(int[] array) {
        for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
            shiftDown(array, array.length, i);
        }
    }
    private static void shiftDown(int[] array, int heapLength, int index) {
        int parent = index;
        int child = 2 * parent + 1;
        while (child < heapLength) {
            if (child + 1 < heapLength && array[child + 1] > array[child]) {
                child = child + 1;
            }
            if (array[child] > array[parent]) {
                swap(array, child, parent);
            } else {
                break;
            }
            parent = child;
            child = 2 * parent + 1;
        }
    }

(5) 冒泡排序:从无序区间开始, 从后往前 通过两两相邻元素之间的比较找到最小值, 放到待排区间最前面. 如果是从前往后 比较的话就把找到的最小值放到待排区间最后面.
每趟排序最多交换 i-j 次.

            //注意避免下标越界异常出现
 public static void bubbleSort(int[] array) {
        // 按照每次找最小并放在最前面的方式来进行排序. (从后往前比较交换)
        for (int bound = 0; bound < array.length; bound++) {
            // [0, bound) 已排序区间 [bound, size) 待排序区间
            // cur > bound 而不是 >= , 当 bound 为 0 的时候, 如果 >= , cur 也为 0, cur - 1 也就下标越界了
            for (int cur = array.length - 1; cur > bound; cur--) {
                // 此处 cur - 1 是因为 cur 初始值是 array.length - 1. 如果取 cur + 1 下标的元素, 就越界了
                // 此处的条件如果写成 >= 同样无法保证稳定性
                if (array[cur - 1] > array[cur]) {
                    swap(array, cur - 1, cur);
                }
            }
        }
    }

(6) 快速排序(重要) 原理: partition(隔离).
第一种交换方法 (先左右交换,最后再与基准值交换):
a 通常选最左边或最右边元素作为基准值 base.
b 如果取最左侧元素为基准值,就从右边开始. 先让right从最右边找一个小于基准值的元素直到找到,然后让left从最左边开始找一个大于基准值的直到找到,然后和刚才找到的右边的元素交换位置.
c 然后重复进行以上动作,直到left和right重合. 重合后把基准值和重合元素再交换.
d 以基准值为分割点就把元素分成两部分了. 基准值左边的元素都小于(可以包含相等的)基准值, 基准值右边的元素都大于(可以包含相等的)基准值. 对接下来的两部分元素再递归执行以上动作直到一个阈值.
e 基准值的选取很重要. 通常选取一个中位数的元素放到数组首或尾. 当递归到一定程度时再递归效率就不高了,可以把剩下的基本有序的元素采取 插入排序提高效率.

另一种交换方法 (每次找到后都与基准值交换): 如果取最左侧元素为基准值,就从右边开始找小于基准值的,找到后直接与基准值交换. 然后从左边找大于基准值的,找到后也直接与基准值交换.
还有其它的方法, 省略了.

 public static void quickSort(int[] array) {
        // 辅助完成递归过程, 此处为了代码简单, 区间设定成前闭后闭.
        quickSortHelper(array, 0, array.length - 1);
    }
    private static void quickSortHelper(int[] array, int left, int right) {
        if (left >= right) {
            // 区间中有 0 个元素或者 1 个元素. 此时不需要排序,相当于阈值.
            return;
        }
        int index = partition(array, left, right);   index就是 新的基准值的位置. 
        quickSortHelper(array, left, index - 1);	 递归基准值 左侧.
        quickSortHelper(array, index + 1, right);    递归基准值 右侧.
        
    private static int partition(int[] array, int left, int right) {
        int beg = left;
        int end = right;
        // 取最右侧元素为基准值
        int base = array[right];
        while (beg < end) {
            // 从左往右找到比基准值大的元素
            while (beg < end && array[beg] <= base) {
                beg++;
            }
            // 当上面的循环结束时, beg 要么和 end 重合, 要么 beg 就指向一个大于 base 的值
            while (beg < end && array[end] >= base) {
                end--;
            }
            // 当上面的循环结束之后, beg 要门和 end 重合,要么 end 就指向一个小于 base 的值
            swap(array, beg, end);
        }
        beg 位置的元素一定 >= 基准值,然后最后一步让 beg位置的元素和 基准值再交换.
        swap(array, beg, right);    
        return beg;      ===> 注意返回的是 新的基准值的下标.
    }
    private static void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }
}

下列选项中,不可能是快速排序第2趟排序结果的是 ( C ).
A.2,3,5,4,6,7,9
B.2,7,5,6,4,3,9
C.3,2,5,4,7,6,9
D.4,2,3,5,7,6,9
快排的阶段性排序结果的特点是: 第 i 趟完成时,会有 i及i以上 的数出现在它最终将要出现的位置,即它左边的数都比它小, 它右边的数都比它大. 题目问第二趟排序的结果,即要找不存在2个这样的数的选项。A选项中2、3、6、7、9均符合,所以A排除;B选项中,2、9均符合,所以B排除;D选项中5、9均符合,所以D选项排除;最后看C选项,只有9一个数符合, 所以选C.

(7) 归并排序(重要)
a 适用于外部排序(数据在磁盘上), 也适用于链表排序.
b 过程: 先把一个区间分成两份, 然后把每个子区间继续分成两份, 直到每个区间都是一个元素. 当某个区间只有一个元素时,这个区间一定是有序的, 最后合并每两个相邻区间(同时进行排序),直到成为一个区间.
c 注意和希尔排序的区别, 那个是每一步都在排序, 这个是先把所有元素分成单个的一份, 然后两两进行排序. 然后把两两排序好的小组当成两个新的小组继续排序. 是一个 递归过程.

    public static void mergeSort(int[] array) {
        mergeSortHelper(array, 0, array.length);
    }
     private static void mergeSortHelper(int[] array, int low, int high) {
        if (high - low <= 1) {
            return;
        }
        int mid = (low + high) / 2;
        // 这个方法执行完, 就认为 low, mid 已经排序ok
        mergeSortHelper(array, low, mid);
        // 这个方法执行完, 就认为, mid, high 也已经排序ok
        mergeSortHelper(array, mid, high);
        // 当把左右区间已经归并排序完了, 说明左右区间已经是有序区间了.
        // 接下来就可以针对两个有序区间进行合并了.
        merge(array, low, mid, high);
    }  
    
       public static void merge(int[] array, int low, int mid, int high) {
        int[] output = new int[high - low];
        int outputIndex = 0;  // 记录当前 output 数组中被放入多少个元素了
        int cur1 = low;
        int cur2 = mid;
        while (cur1 < mid && cur2 < high) {
            // 这里写成 <= 才能保证稳定性.
            if (array[cur1] <= array[cur2]) {
                output[outputIndex] = array[cur1];
                outputIndex++;
                cur1++;
            } else {
                output[outputIndex] = array[cur2];
                outputIndex++;
                cur2++;
            }
        }
        // 当上面的循环结束的时候, 肯定是 cur1 或者 cur2 有一个先到达末尾, 另一个还剩下一些内容
        // 把剩下的内容都一股脑拷贝到 output 中
        while (cur1 < mid) {
            output[outputIndex] = array[cur1];
            outputIndex++;
            cur1++;
        }
        while (cur2 < high) {
            output[outputIndex] = array[cur2];
            outputIndex++;
            cur2++;
        }
        // 把output中的元素再搬运回原来的数组
        for (int i = 0; i < high - low; i++) {
            array[low + i] = output[i];
        }
    }

总结:在这里插入图片描述
4 七大排序的 时间复杂度, 空间复杂度, 稳定性.
在这里插入图片描述
5 其他 非基于 比较 的排序: 计数排序 基数排序 桶排序.

计数排序 是 稳定排序. 当输入的元素是 n个 0到k 之间的整数时, 时间复杂度是 O(n+k), 空间复杂度也是 O(n+k).
其排序速度 快于任何比较排序算法. 适用于: 当k不是很大并且序列比较集中时.

6 TopK问题.

一: 对k个数进行排序的情况.
(1) 全局排序, O(n^2). 对所有数排序.
(2) 局部排序, 如冒泡排序, O(n*k). 只排序TopK.
(3) 分治法: 快速排序, O(n*lg(n)). 对所有数排序(每个分支都要递归).
(4) 使用堆: (优先级队列建堆), O(n * lg(k)). 只排序TopK.
当找 topk最大的数时, 先用前k个元素生成一个 小堆,这个小堆用于存储 当前最大的k个元素. 从第k+1个元素开始扫描,和堆顶(堆中最小的元素)比较,如果被扫描的元素大于堆顶,则替换堆顶的元素并调整堆, 以保证堆内的k个元素总是当前最大的k个元素.
找topk大的数, 使用 优先级队列建k个按 升序排的小堆.
找topk小的数, 使用 优先级队列建k个按 降序排的大堆.

例: 找topK小的数.

import java.util.ArrayList;
import java.util.Collections;
import java.util.PriorityQueue;

public class DemoB {
    public ArrayList<Integer> topK(int[] input, int k) {
        ArrayList<Integer> list = new ArrayList<>();
        if (input == null || k <= 0 || k > input.length) {
            return list;
        }
 我们使用优先级队列建堆.
 优先级队列默认是升序排, 我们现在传入参数让它 降序排k个元素,是一个大堆.
        PriorityQueue<Integer> queue=new PriorityQueue(k, Collections.reverseOrder());
        for(int i=0;i<input.length;i++){
            if(i<k){
                queue.offer(input[i]);    // queue 会自动排序.
            }else {
                if(input[i]<queue.peek()){
                    queue.poll();
                    queue.offer(input[i]);   queue会自动更新排序.
                }
            }
        }
        for(int i=0;i<queue.size();i++){
            list.add(queue.poll());
        }
        return list;
    }
}

二: 找出topK, 这k个数没有排序的情况.
(5) 减治法: 快排+选择, O(n). (只进行一次partition, 只递归一个分支).

例: 找topK小的数.

public class TopK1 {
    public static void findK(int[] a, int n, int K) {
        findKth(a, 0, n - 1, K);
    }

    public static void findKth(int[] a, int left, int right, int k) {
        int index = partition(a, left, right);
        if (k == index - left + 1) {
            for (int i = 0; i <= index; i++) {
                System.out.print(a[i] + " ");
            }
        } else if (k > index - left + 1) {
            findKth(a, index + 1, right, k - index + left - 1);
        } else findKth(a, left, index - 1, k);
    }

    排升序.
    public static int partition(int[] a, int left, int right) {
        int beg = left;
        int end = right;
        int base = a[right];
        while (beg < end) {
            while (beg < end && a[beg] <= base) {
                beg++;
            }
            while (beg < end && a[end] >= base) {
                end--;
            }
            swap(a, beg, end);
        }
        swap(a, beg, right);
        return beg;
    }

    private static void swap(int[] a, int i, int j) {
        int tmp = a[i];
        a[i] = a[j];
        a[j] = tmp;
    }
}

过程: 按照降序排序,然后只进行一次partition,找到中轴index. 然后判断 k 与 index - left + 1 的关系. (1) 如果 k==index - left + 1,说明中轴index即为第k大元素.
(2) 如果 k>index - left + 1,说明第k大元素在index右边(因为是按降序排的).
(3) 如果 k<index - left + 1,说明第k大元素在index左边.
(4) 找到了第k大, 说明k前面的元素都比k大. 也就是同时找到了最大的k个元素.
(5) 此方法也可以找 第k个数.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值