排序算法

133 阅读2分钟

选择排序

  • 从数组中选择最小元素,将它与数组的第一个元素交换位置。再从数组剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。

  • 选择排序需要 (n^2)级别次比较和 (n)级别次交换。

    public void selectSort(int[] arr) {
            int n = arr.length;
            for (int i = 0; i < n - 1; i++) {
                int min = i;
                for (int j = i + 1; j < n; j++){
                    if(arr[j] < arr[min])
                        min = j;
                }
                int t = arr[i];
                arr[i] = arr[min];
                arr[min] = t;
            }
        }
    
  • 不稳定

冒泡排序

  • 从左到右不断交换相邻逆序的元素,在一轮的循环之后,可以让未排序的最大元素到右侧。

  • 在一轮循环中,如果没有发生交换,那么说明数组已经是有序的,此时可以直接退出。

      public void bubbleSort1(int[] arr){
            int n = arr.length;
            boolean isSorted;
            for(int i = 0;i < n;i ++){
                isSorted = true;
                for(int j = 0;j < n - i - 1;j ++){
                    if(arr[j] > arr[j + 1]){
                        int t = arr[j + 1];
                        arr[j + 1] = arr[j];
                        arr[j] = t;
                        isSorted = false;
                    }
                }
                if(isSorted){
                    break;
                }
            }
        }
    
  • 稳定

插入排序

  • 每次都将当前元素插入到左侧已经排序的数组中。

  • 平均情况下插入排序需要 (n^2)级别次 比较;最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。

    public void insertSort(int[] arr){
            int n  = arr.length;
            int j;
            int v;
            for(int i = 1;i < n;i ++){
                j = i;
                v = arr[j];
                while(j > 0 && v < arr[j - 1])
                    arr[j] = arr[ -- j];
                arr[j] = v;
            }
        }
    
  • 稳定

希尔排序

  • 是插入排序的一种,又称为缩小增量排序。时间复杂度 o(n^1.3),空间复杂度 o(1),不稳定。

归并排序

  • 归并排序的思想是将数组分成两部分,对两部分进行递归排序,然后归并起来。

  • 时间复杂度是O(nlogn),空间复杂度是O(n)

     public void mergeSort(int[] arr){
            if(arr == null || arr.length == 0)
                return;
            mergeSort(arr,0,arr.length - 1);
        }
        public void mergeSort(int[] arr,int l,int r){
            if(l == r)
                return;
            int mid = l + (r - l)/2;
            //对左边排序
            mergeSort(arr,l,mid);
            //对右边排序
            mergeSort(arr,mid + 1,r);
            //合并
            merge(arr,l,mid,r);
        }
        private void merge(int[] nums, int l, int mid, int r) {
            int[] newArr = new int[r - l + 1];
            for(int i = l;i <= r;i ++){
                newArr[i - l] = nums[i];
            }
    
            int idx1 = l,idx2 = mid + 1;
            int idx = l;
            while(idx1 <= mid && idx2 <= r){
                if(newArr[idx1 - l] <= newArr[idx2 - l]){
                    nums[idx ++] = newArr[idx1 - l];
                    idx1 ++;
                }else{
                    nums[idx ++] = newArr[idx2 - l];
                    idx2 ++;
                }
            }
            while(idx1 <= mid){
                nums[idx ++] = newArr[idx1 - l];
                idx1 ++;
            }
            while(idx2 <= r){
                nums[idx ++] = newArr[idx2 - l];
                idx2 ++;
            }
        }
    
  • 稳定

快速排序

  • 对一个未排序数组,假设从该序列中的元素中取一个基准值pivotkey,将小于pivotkey放左边,大于pivotkey放右边;接着以该值为中间,左右两边的分割作为新的序列,重新进行上面的操作,直到成为一个有序的数组。

    //普通快排,O(nlogn),数组近乎有序时退化成O(n^2),用随机化种子实现快排在近乎有序时的优化。
        public void QuickSort(int arr[]) {
            int n = arr.length;
            Qsort(arr, 0, n - 1);
        }
        public void Qsort(int arr[], int left, int right) {
            if (left < right) {
                int mid = partition(arr, left, right);
                Qsort(arr, left, mid - 1);
                Qsort(arr, mid + 1, right);
            }
        }
        public int partition(int arr[], int left, int right) {
            int key = arr[left];
            while (left < right) {
                while (left < right && arr[right] > key)
                    right--;
                arr[left] = arr[right];
                while (left < right && arr[left] <= key)
                    left++;
                arr[right] = arr[left];
            }
            arr[left] = key;
            return left;
        }
    
  • 不稳定

  • 复杂度分析

    • 最坏情况:待排序为正序或逆序,这样每次分割后的子序列一个之比上一次序列少一个元素,一个为空。如 1 2 3 4 5 pivotkey=1;分割后一个序列为 2 3 4 5 一个为空,第i次需要(n-i)次比较,最坏时间复杂度是O(n^2)
    • 最好情况:每一次分割都能平分,很均匀 O(nlogn)
  • 平均情况:O(n*logn) 数学归纳法

  • 改进

    • 切换到插入排序
      • 因为快速排序在小数组中也会递归调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。
    • 随机枢轴
      • 为了避免快排时间复杂度退化到O(n^2),可以使用随机枢轴。
    • 三路快排
      • 对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。
      • 三路快排对于有大量重复元素的随机数组可以在线性时间内完成排序。

堆排序

  • 从小到大排序的话

    • 首先构造大顶堆。
      • 构建一个大顶堆。大顶堆是指一个结点的值比他的孩子结点的值都大。堆顶元素就是最大元素。
      • 从完全二叉树最下层最右边的非终端节点开始进行下沉操作。如果一个节点的两个子节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点不需要进行下沉操作。
    • 再将堆顶元素和数组最后一个元素调换位置,最后一个元素就是最大元素,然后将剩余的n-1个元素重新构造成一个堆,就会得到次大值,反复执行,最终得到一个有序序列。
    public void heapSort(int[] arr){
            makeHeap(arr,arr.length - 1);
            for(int i = arr.length - 1;i > 0;i --){
                //一次堆调整的时间复杂度是O(logn),需要调整n-1次,所以堆排序的时间复杂度是O(nlogn)
                swap(arr,i,0);
                shiftDown(arr,0,i - 1);
            }
        }
        //构建堆的时间复杂度是O(n)
        private void makeHeap(int[] arr,int end){
            for(int i = end/2 - 1;i >= 0;i --){
                shiftDown(arr,i,end);
            }
        }
        private void shiftDown(int[] arr,int i,int end){
            while(2 * i + 1 <= end){
                int j = 2 *i + 1;
                if(j + 1 <= end && arr[j] < arr[j + 1])
                    j ++;
                if(arr[j] <= arr[i])
                    break;
                swap(arr,i,j);
                i = j;
            }
        }
        private void swap(int[] arr,int i,int j){
            int t = arr[i];
            arr[i] = arr[j];
            arr[j] = t;
        }
    
  • 不稳定

  • 时间复杂度:主要消耗在初始构建堆和重建堆的反复筛选上。建堆的时间复杂度是O(n),堆调整的时间复杂度是O(logn),并且需要调整n-1次,所以重建堆的时间复杂度是O(nlogn),所以总体时间复杂度是O(nlogn)。

  • 由于初始构建堆所需比较的次数较多,所以不适合于待排序序列个数较少的情况。