算法基础一之排序算法

一、排序算法概述

1、定义

将杂乱无章的数据元素,通过一定的方法按关键字顺序排列的过程叫做排序。

2、分类

十种常见排序算法可以分为两大类:

  • 非线性时间比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此称为非线性时间比较类排序。

  • 线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

算法基础一之排序算法_第1张图片

3、比较

算法基础一之排序算法_第2张图片

4、相关概念

  • 稳定:如果a原本在b前面且a=b,排序之后a仍然在b的前面。
  • 不稳定:如果a原本在b的前面且a=b,排序之后 a 可能会出现在 b 的后面。
  • 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
  • 空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
  • 内部排序:所有排序操作都在内存中完成。本文主要介绍的是内部排序。
  • 外部排序:待排序记录的数量很大,以致于内存不能一次容纳全部记录,所以在排序过程中需要对外存进行访问的排序过程。

5、比较和非比较的区别

常见的快速排序、归并排序、堆排序、冒泡排序等属于比较排序在排序的最终结果里,元素之间的次序依赖于它们之间的比较。每个数都必须和其他数进行比较,才能确定自己的位置。
冒泡排序之类的排序中,问题规模为n,又因为需要比较n次,所以平均时间复杂度为O(n²)。在归并排序、快速排序之类的排序中,问题规模通过分治法消减为logN次,所以时间复杂度平均O(nlogn)
比较排序的优势是,适用于各种规模的数据,也不在乎数据的分布,都能进行排序。可以说,比较排序适用于一切需要排序的情况。

计数排序、基数排序、桶排序则属于非比较排序。非比较排序是通过确定每个元素之前,应该有多少个元素来排序。针对数组arr,计算arr[i]之前有多少个元素,则唯一确定了arr[i]在排序后数组中的位置。
非比较排序只要确定每个元素之前的已有的元素个数即可,所有一次遍历即可解决。算法时间复杂度O(n)
非比较排序时间复杂度底,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

二、各算法原理及实现

首先将交换方法单独抽取出来,这样每种排序方法直接调用就行。

public class SortUtil {
    public static void swap(int[] arr, int index1, int index2) {
        int tmp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = tmp;
    }

    public static void display(int[] arr) {
        if (arr != null) {
            Arrays.stream(arr).forEach(System.out::println);
        }
    }
}

1、冒泡排序(Bubble Sort)

1)算法描述

1)比较相邻的元素。如果前一个比后一个大,就交换它们两个;
2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
3)针对所有的元素重复以上的步骤,除了最后一个;
4)重复步骤1~3,直到排序完成。为了优化算法,可以设立一个布尔标识,每趟排序开始前设为false,如果该趟排序发生了交换就置为true,如果一趟排序结束标识仍为false表示该趟排序没有发生交换,即数组已经有序,可以提前结束排序。

2)动图演示

算法基础一之排序算法_第3张图片

3)代码

public class BubbleSort {
    public static int[] bubbleSort(int[] array) {
        if (array == null || array.length < 2) {
            return array;
        }
        int len = array.length;
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < len - i - 1; j++) {
                //如果前一个数组比后面的大,交换位置,经过一趟排序,会把大的数字上浮到后面
                if (array[j] > array[j+1]) {
                    SortUtil.swap(array, j, j+1);
                }
            }
        }
        return array;
    }

    public static void main(String[] args) {
        int[] arr = {8, 4, 9, 2, 1, 6, 3, 7, 5, 8};
        System.out.println("排序前:");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println("");
        System.out.println("排序后:");
        bubbleSort(arr);
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}

排序前:
8 4 9 2 1 6 3 7 5 8 
排序后:
1 2 3 4 5 6 7 8 8 9 

上面代码时未经过优化的,可以加一个标志位,如果经过一趟排序,都没有交换元素,说明数组已经有序了

public class BubbleSort_opt1 {
    public static int[] bubbleSort(int[] array) {
        if (array == null || array.length < 2) {
            return array;
        }
        int len = array.length;
        for (int i=0; i array[j+1]) {
                    swap(array, j, j+1);
                    //经过一趟排序,没有交换数据,说明数据已经有序
                    isSwap = true;
                }
            }
            if (!isSwap) {
                break;
            }
        }
        return array;
    }
}

4)时间复杂度

冒泡排序平均时间复杂度为O(n2),最好时间复杂度为O(n),最坏时间复杂度为O(n2)。
最好情况:如果待排序元素本来是正序的,那么一趟冒泡排序就可以完成排序工作,比较和移动元素的次数分别是 (n - 1) 和 0,因此最好情况的时间复杂度为O(n)。
最坏情况:如果待排序元素本来是逆序的,需要进行 (n - 1) 趟排序,所需比较和移动次数分别为 n * (n - 1) / 2和 3 * n * (n-1) / 2。因此最坏情况下的时间复杂度为O(n2)。

5)空间复杂度

冒泡排序使用了常数空间,空间复杂度为O(1)

6)稳定性

当 array[j] == array[j+1] 的时候,我们不交换 array[i] 和 array[j],所以冒泡排序是稳定的。

7) 算法拓展

鸡尾酒排序,又称定向冒泡排序、搅拌排序等,是对冒泡排序的改进。在把最大的数往后面冒泡的同时,把最小的数也往前面冒泡,同时收缩无序区的左右边界,有序区在序列左右逐渐累积。

动图如下:

算法基础一之排序算法_第4张图片

public class CocktailSort {
    public static int[] bubbleSort(int[] array) {
        if (array == null || array.length < 2) {
            return array;
        }
        int left = 0;
        int right = array.length-1;
        while (left < right) {
            //从左向右,上浮最大值
            for (int i = left; i < right; i++) {
                if (array[i] > array[i + 1]) {
                    SortUtil.swap(array, i, i + 1);
                }
            }
            right--;
            //从右向左,下沉出最小值
            for (int j = right; j > left; j--) {
                if (array[j] < array[j - 1]) {
                    SortUtil.swap(array, j, j - 1);
                }
            }
            left++;
        }
        return array;
    }
}

鸡尾酒排序是稳定的。它的平均时间复杂度为O(n2),最好情况是待排序列原先就是正序的,时间复杂度为O(n),最坏情况是待排序列原先是逆序的,时间复杂度为O(n2)。空间复杂度为O(1)。

2、简单选择排序(Selection Sort)

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n^2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 

1)算法描述:

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • n-1趟结束,数组有序化了。

2)动图演示

算法基础一之排序算法_第5张图片

3)代码

public class SelectionSort {
    public static int[] selectionSort(int[] array) {
        if (array == null || array.length < 2) {
            return array;
        }
        int len = array.length;
        for (int i = 0; i < len - 1; i++) {
            int minIndex = i;
            //每次找到最小值,插入有序数列中
            for (int j = i + 1; j < len; j++) {
                if (array[j] < array[minIndex]) {
                    minIndex = j;
                }
            }
            SortUtil.swap(array, minIndex, i);
        }
        return array;
    }

    public static void main(String[] args) {
        int[] arr = {8, 4, 9, 2, 1, 6, 3, 7, 5, 8};
        System.out.println("排序前:");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println("");
        System.out.println("排序后:");
        selectionSort(arr);
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}

 排序前:
8 4 9 2 1 6 3 7 5 8 
排序后:
1 2 3 4 5 6 7 8 8 9 

4)时间复杂度

    简单选择排序平均时间复杂度为O(n2),最好时间复杂度为O(n2),最坏时间复杂度为O(n2)。
最好情况:如果待排序元素本来是正序的,则移动元素次数为 0,但需要进行 n * (n - 1) / 2 次比较。
最坏情况:如果待排序元素中第一个元素最大,其余元素从小到大排列,则仍然需要进行 n * (n - 1) / 2 次比较,且每趟排序都需要移动 3 次元素,即移动元素的次数为3 * (n - 1)次。
需要注意的是,简单选择排序过程中需要进行的比较次数与初始状态下待排序元素的排列情况无关。

5)空间复杂度

简单选择排序使用了常数空间,空间复杂度为O(1)

6)稳定性

简单选择排序不稳定,比如序列 2、4、2、1,我们知道第一趟排序第 1 个元素 2 会和 1 交换,那么原序列中 2 个 2 的相对前后顺序就被破坏了,所以简单选择排序不是一个稳定的排序算法。

3、直接插入排序(Insertion Sort)

1)算法描述

一般来说,直接插入排序都采用in-place(原地算法)在数组上实现。具体算法描述如下:
1)从第一个元素开始,该元素可以认为已经被排序;
2)取出下一个元素,在已经排序的元素序列中从后向前扫描;
3)如果该元素(已排序)大于新元素,将该元素移到下一位置;
4)重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5)将新元素插入到该位置后;
6)重复步骤2~5。

2)动图演示

算法基础一之排序算法_第6张图片

3)代码

package com.zy.practice.class02;

import java.util.Arrays;

public class InsertSort {
    public static void main(String[] args) {
        int[] arr = {8, 4, 9, 2, 1, 6, 3, 7, 5, 8};
        System.out.println("排序前:");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println("");
        System.out.println("排序后:");
        insertSort(arr);
        System.out.println(Arrays.toString(arr));
    }
    public static void insertSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        for (int i = 1; i < arr.length; i++) {
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                swap(arr, j , j + 1);
            }
        }

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

排序前:
8 4 9 2 1 6 3 7 5 8 
排序后:
1 2 3 4 5 6 7 8 8 9 

4)时间复杂度

直接插入排序平均时间复杂度为O(n2),最好时间复杂度为O(n),最坏时间复杂度为O(n2)。
最好情况:如果待排序元素本来是正序的,比较和移动元素的次数分别是 (n - 1) 和 0,因此最好情况的时间复杂度为O(n)。
最坏情况:如果待排序元素本来是逆序的,需要进行 (n - 1) 趟排序,所需比较和移动次数分别为 n * (n - 1) / 2和 n * (n - 1) / 2。因此最坏情况下的时间复杂度为O(n2)。

5)空间复杂度

直接插入排序使用了常数空间,空间复杂度为O(1)

6) 稳定性

直接插入排序是稳定的。

7)算法拓展

在直接插入排序中,待插入的元素总是在有序区线性查找合适的插入位置,没有利用有序的优势,考虑使用二分查找搜索插入位置进行优化,即二分插入排序

public class BinaryInsertionSort {
    public static int[] binaryInsertionSort(int[] array) {
        if (array == null || array.length == 0) {
            return array;
        }
        for (int i = 1; i < array.length; i++) {
            int curVal = array[i];
            int left = 0;
            int right = i - 1;

            //搜索有序区中第一个大于 current 的位置,即为 current 要插入的位置
            while (left <= right) {
                int mid = left + (right - left) / 2;
                if (array[mid] > curVal) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            }

            for (int j = i - 1; j >= left; j --) {
                array[j + 1] = array[j];
            }
            array[left] = curVal;
        }
        return array;
    }

    public static void main(String[] args) {
        int[] arr = {8, 4, 9, 2, 1, 6, 3, 7, 5, 8, 16, 12};
        System.out.println("排序前:");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println("");
        System.out.println("排序后:");
        binaryInsertionSort(arr);
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
}

二分插入排序是稳定的。它的平均时间复杂度是O(n2),最好时间复杂度为O(nlogn),最坏时间复杂度为O(n2)。

4、希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n^2)的排序算法,是直接插入排序的改进版。它与直接插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

1) 算法描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k 趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
  • 增量d 的范围: 1<= d < 待排序数组的长度 (d 需为 int 值)
    增量的取值: 一般的初次取序列(数组)的一半为增量,以后每次减半,直到增量为1。
    第一个增量=数组的长度/2,
    第二个增量= 第一个增量/2,
    第三个增量=第二个增量/2,
    以此类推,最后一个增量=1。

2) 动图演示

3)代码

public class ShellSort {
    public static void main(String[] args) {
        int[] arr = {8, 4, 9, 2, 1, 6, 3, 7, 5, 8, 16, 12};
        System.out.println("排序前:");
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println("");
        System.out.println("排序后:");
        shellSort(arr);
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
    }
    public static int[] shellSort(int[] array) {
        if (array == null || array.length < 2) {
            return array;
        }
        int gap = array.length >> 1;
        while (gap > 0) {
            for (int i = gap; i < array.length; i++) {
                int curVal = array[i];
                int preIndex = i - gap;
                while (preIndex >= 0 && curVal < array[preIndex]) {
                    array[preIndex + gap] = array[preIndex];
                    preIndex = preIndex - gap;
                }
                array[preIndex + gap] = curVal;
            }
            gap = gap >> 1;
        }
        return array;
    }
}

排序前:
8 4 9 2 1 6 3 7 5 8 16 12 
排序后:
1 2 3 4 5 6 7 8 8 9 12 16 

4)时间复杂度
希尔排序平均时间复杂度为O(nlogn),最好时间复杂度为O(nlog2n),最坏时间复杂度为O(nlog2n)。希尔排序的时间复杂度与增量序列的选取有关。

5) 空间复杂度
希尔排序使用了常数空间,空间复杂度为O(1)

6) 稳定性
由于相同的元素可能在各自的序列中插入排序,最后其稳定性就会被打乱,比如序列 2、4、1、2,所以希尔排序是不稳定的。

5、归并排序(Merge Sort)

1) 算法描述
1)把长度为 n 的输入序列分成两个长度为 n / 2 的子序列;
2)对这两个子序列分别采用归并排序;
3)将两个排序好的子序列合并成一个最终的排序序列。

2)动图演示

算法基础一之排序算法_第7张图片

递归方法:

public class MergeSort {
    public static int[] mergeSort(int[] array) {
        if (array == null || array.length < 2) {
            return array;
        }
        
        mergeSort(array, 0, array.length - 1);
        return array;
    }

    private static void mergeSort(int[] array, int left, int right) {
        if (left >= right) {
            return;
        }
        int mid = left + ((right - left) >> 1);
        //分
        mergeSort(array, left, mid);
        mergeSort(array, mid + 1, right);
        //治
        merge(array, left, mid, right);
    }

    private static void merge(int[] array, int left, int mid, int right) {
        int[] helpArr = new int[right - left + 1];
        int low = left;
        int heigh = mid + 1;
        int helpIndex = 0;
        while (low <= mid && heigh <= right) {
            if (array[low] <= array[heigh]) {
                helpArr[helpIndex++] = array[low++];
            } else {
                helpArr[helpIndex++] = array[heigh++];
            }
        }
        while (low <= mid) {
            helpArr[helpIndex++] = array[low++];
        }
        while (heigh <= right) {
            helpArr[helpIndex++] = array[heigh++];
        }

        for (int i = 0; i < right - left + 1; i++) {
            array[left + i] = helpArr[i];
        }
    }
}

非递归方法:

package com.zy.base.class003;

public class Code01_MergeSort1 {
    public static void main(String[] args) {
        int[] arr = {10, 2, 5, 6, 1, 3, 9};
        new Code01_MergeSort1().mergeSort(arr);
        System.out.println();
    }

    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        procss(arr, 0, arr.length - 1);
    }

    private static void procss(int[] arr, int start, int end) {
        if (start == end) {
            return;
        }
        int len = arr.length;
        int mergeSize = 1;
        while (mergeSize < len) {
            int left = 0;
            while (left < len) {
                int m = left + mergeSize - 1;
                if (m >= len) {
                    break;
                }
                int right = Math.min(m + mergeSize, len - 1);
                merge(arr, left, m, right);
                left = right + 1;
            }
            if (mergeSize > len / 2) {
                break;
            }
            mergeSize <<= 1;
        }



    }

    private static void merge(int[] arr, int start, int mid, int end) {
        int[] helper = new int[end - start + 1];
        int left = start;
        int right = mid + 1;
        int helperIndex = 0;
        while (left <= mid && right <= end) {
            helper[helperIndex++] = arr[left] <= arr[right] ? arr[left++] : arr[right++];
        }

        while (left <= mid) {
            helper[helperIndex++] = arr[left++];
        }

        while (right <= end) {
            helper[helperIndex++] = arr[right++];
        }

        for (int i = 0; i < helper.length; i++) {
            arr[start + i] = helper[i];
        }
    }
}

常见面试题:

在一个数组中,一个数左边比它小的数的总和,叫数的小和,所有数的小和累加起来,叫数组小和。求数组小和。

例如:[1, 3, 4, 2, 5]

1左边比1小的数:

3左边比3小的数:1

4左边比4小的数:1、3

2左边比2小的数:1

5左边比5小的数:1、3、4、2

所以数组的小和为1+1+3+1+1+3+4+2=16

算法基础一之排序算法_第8张图片

上代码:

package com.zy.base.class003;

public class Code02_SmallSum {
    public static void main(String[] args) {
        int[] arr = {1, 3, 4, 2, 5};
        int i = Code02_SmallSum.smallSum(arr);
        System.out.println(i);
    }
    public static int smallSum(int[] arr) {
        if (arr == null || arr.length < 2) {
            return 0;
        }
        return process(arr, 0, arr.length - 1);
    }

    private static int process(int[] arr, int left, int right) {
        if (left == right) {
            return 0;
        }
        int mid = left + (right - left) / 2;
        return process(arr, left, mid) + process(arr, mid + 1, right) + merge(arr, left, mid, right);
    }

    private static int merge(int[] arr, int start, int mid, int end) {
        int[] helper = new int[end - start + 1];
        int res = 0;
        int left = start;
        int right = mid + 1;
        int helperIndex = 0;
        while (left <= mid && right <= end) {
            res += arr[left] < arr[right] ? (end - right + 1) * arr[left] : 0;
            helper[helperIndex++] = arr[left] < arr[right] ? arr[left++] : arr[right++];
        }
        while (left <= mid) {
            helper[helperIndex++] = arr[left++];
        }
        while (right <= end) {
            helper[helperIndex++] = arr[right++];
        }

        for (int i = 0; i < helper.length; i++) {
            arr[start + i] = helper[i];
        }
        return res;
    }
}

求逆序对

剑指 Offer 51. 数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。

示例 1:

输入: [7,5,6,4]
输出: 5
 

限制:

0 <= 数组长度 <= 50000

package com.zy.offer;

/**
 * 剑指 Offer 51. 数组中的逆序对
 * 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
 *
 *
 *
 * 示例 1:
 *
 * 输入: [7,5,6,4]
 * 输出: 5
 *
 *
 * 限制:
 *
 * 0 <= 数组长度 <= 50000
 */
public class Offer51 {
    public int reversePairs(int[] nums) {
        if (nums == null || nums.length < 2) {
            return 0;
        }
        return process(nums, 0, nums.length - 1);

    }

    private int process(int[] nums, int start, int end) {
        if (start == end) {
            return 0;
        }
        int mid = start + (end - start) / 2;

        return process(nums, start, mid) + process(nums, mid + 1, end) + merge(nums, start, mid, end);

    }

    private int merge(int[] nums, int start, int mid, int end) {
        int[] help = new int[end - start + 1];
        int left = start;
        int right = mid + 1;
        int index = 0;
        int res = 0;
        while (left <= mid && right <= end) {
            res += nums[left] > nums[right] ? (mid - left + 1) : 0;
            help[index++] = nums[left] > nums[right] ? nums[right++] : nums[left++];
        }
        while (left <= mid) {
            help[index++] = nums[left++];
        }

        while (right <= end) {
            help[index++] = nums[right++];
        }

        for (int i = 0; i < help.length; i++) {
            nums[start + i] = help[i];
        }
        return res;
    }
}

5、快速排序

你可能感兴趣的:(算法基础,算法,排序算法,数据结构)