基本算法之排序

把我上学时候在csdn上的笔记搬过来了

简单选择排序

选择排序要用到交换,在开始之前不妨说下数值交换的三种方法

临时变量

public static void swap(int[] arr, int i, int j) {
        if (i != j) {
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }

加法

public static void swap(int[] arr, int i, int j) {
        if (i != j) {
            arr[i] = arr[i] + arr[j];
            arr[j] = arr[i] - arr[j];
            arr[i] = arr[i] - arr[j];
        }

    }

异或

public static void swap(int[] arr, int i, int j) {
            if(i!=j){
                arr[i] = arr[i] ^ arr[j];
                arr[j] = arr[i] ^ arr[j];
                arr[i] = arr[i] ^ arr[j];
            }

}

简单选择排序思路很简单,就是每次遍历数组获得最小值得位置,然后将最小值与数组中第一个值交换,然后从第二个值开始遍历获得最小值与第二个元素交换,以此类推。

public static void selectSort(int[] arr) {
        int min;
        for (int i = 0; i < arr.length; i++) {
            min = i;
            for (int j = i + 1; j < arr.length; j++) {
                if (arr[j] < arr[min]) {
                    min = j;
                }
            }
            swap(arr, min, i);
        }
    }

最好最、坏时间复杂度都是O(n²)。内层for循环执行次数依次为n-1,n-2,n-3,……,1。加起来为n(n-1)/2 。 该算法是不稳定的,很容易理解,由于交换的时候,会改变两个相等值得相对位置。

直接插入排序

将序列中的第一个元素作为一个有序序列,然后将剩下的n-1个元素按照关键字大小依次插入该有序序列,每插入一个元素后依然保持该序列有序,经过n-1趟排序后即成为有序序列。

public static void insertSort(int[] arr) {
        for (int i = 1; i < arr.length; i++) {
            int j = i;
            int temp = arr[i];
            while (j > 0 && temp < arr[j - 1]) {
                arr[j] = arr[j - 1];
                j--;
            }
            arr[j] = temp;
        }
    }

算法必须进行n-1趟。最好情况下,每趟都比较一次,时间复杂度为O(n);最坏情况下的时间复杂度为O(n²)。 插入排序是稳定的,当然这与循环条件中”temp < arr[j - 1]”密切相关,如果改为”temp < =arr[j - 1]” 那么就不稳定了。

冒泡排序

第一趟在数组(arr[0]-arr[n-1])中从前往后进行两个相邻元素的比较,若后者小,则交换,比较n-1次;第一趟排序结束,最大的元素到 arr[n-1] 中 ;下一趟排序在子数组arr[0]-arr[n-2]中进行;如果在某一趟排序中未交换元素,说明子序列已经有序,不需要再进行下一趟排序。

public static void bubbleSort(int[] arr) {
        int i = arr.length - 1;
        int last;//记录某趟最后交换的位置,其后的元素都是有序的
        while (i > 0) {
            last=0;
            for (int j = 0; j < i; j++) {
                if (arr[j] > arr[j + 1]) {
                    swap(arr, j, j + 1);
                    last= j;
                }
            }
            i=last;
        }
    }

其中变量last主要是要记录,某趟遍历时最后一次交换的位置。在last之后的元素其实都已经在排序结果中正确的位置。后面再进行下一趟时,只要从arr[0]到arr[last] 进行操作即可。最终当last=0时,就说明arr[0]之后的元素都已经在正确的位置了,那么只剩arr[0]肯定也是在正确的位置。
该算法最多进行n-1趟。冒泡排序在已排序的情况下是最好情况,只用进行一趟,比较n-1次,时间复杂度为O(n) ; 最坏情况下要进行n-1趟,第i每次比较n-i次,移动3(n-i)次,时间复杂度为O(n²); 冒泡排序是稳定的排序算法,这当然也与算法中的判断条件相关。

快速排序

快排简略来说就是一种分治的思想。对一个数组,选定一个基准元素x,把数组划分为两个部分,低端部分都比x小,高端元素都比x大,那么此时基准元素就在正确的位置了。然后再分别都低端部分和高端部分,分别进行上面的步骤,直到子数组中只有一个元素为止。

递归版

public static void quickSort(int[] arr) {
        qSort(arr, 0, arr.length - 1);
    }

    private static void qSort(int[] arr, int left, int right) {
        //如果分割元素角标p为子数组的边界,那么分割后就只有低端序列或者高端序列,此时判断防止角标越界
        if(left==right){
            return;
        }
        int p = partition(arr, left, right);
        qSort(arr, left, p - 1);
        qSort(arr, p+1,right);
    }

    /**
     * 把数组分为两部分
     * 
     * @param arr
     * @param left
     *            子数组最小角标
     * @param right
     *            子数组最大角标
     * @return 返回分割元素的角标
     */
    private static int partition(int[] arr, int left, int right) {
        int base = arr[right];
        int small=left-1;//用来记录小于base的数字放到左边第几个位置
        for(int i=left;i

非递归版

    public static void quickSort(int[] arr){
        Deque leftStack=new ArrayDeque();
        Deque rightStack=new ArrayDeque();
        leftStack.addFirst(0);
        rightStack.addFirst(arr.length-1);
        while(leftStack.size()!=0){
            Integer left = leftStack.removeFirst();
            Integer right = rightStack.removeFirst();
            int p = partition(arr, left, right);
            if(p>left){
                leftStack.addFirst(left);
                rightStack.addFirst(p-1);
            }
            if(p

上面partition函数中每次选择子数组中第一个元素作为基准元素,当然你也可以选择其他的。
当初始数组有序(顺序或逆序)时,快排效率最低,因为每次分割后有一个子数组是空,时间复杂度是O(n²);在平均情况下可以证明快排的时间复杂度是O(nlogn)。
在最坏的情况下空间复杂度是O(n),最好的情况下是O(logn)。
我们可以看出无论是递归还是非递归,快排至少O(logn)的空间复杂度,这个是省不掉的。
快排是不稳定的。

归并排序

把n个长度的数组看成是n个长度为1的数组,然后两两合并时排序,得到n/2个长度为2的数组(可能包含一个1); 继续两两合并排序,依次类推。

归并排序算法的核心是合并,我具体来说一下合并的思路。比如我们有数组A,B,此时我们要在合并时排序,需要一个临时数组C。用leftPosition,rightPosition和tempPositon 分别来指示数组元素,它们的起始位置对应数组的始端。A[leftPostion]和B[rightPosition]中较小者被拷贝到C中tempPostion的位置。相关的指示器加1。当A和B中有一个用完时,另一个数组中的剩余部分直接拷贝到C中。

代码如下:

    /**
     * @param arr
     *            数组
     * @param tempArr
     *            临时数组,用于临时存放合并后的结果
     * @param leftPostion
     *            第一个子数组的开始
     * @param rightPosition
     *            第二个字数组的开始位置
     * @param rightEnd
     *            第二个子数组的结束位置
     */
    private static void merge(int[] arr, int[] tempArr, int leftPostion,
            int rightPosition, int rightEnd) {
        int leftEnd = rightPosition - 1;
        int tempPositon = leftPostion;

        int begin=leftPostion;
        while (leftPostion <= leftEnd && rightPosition <= rightEnd) {
            if(arr[leftPostion]

下面我们来证明一下归并排序的时间复杂度,我们假设元素个数n是2的幂,这样总是能将数组分为相等的两部分。
当n=1,归并排序的时间 T(1)=1 ;
对于任意n个元素归并排序的时间是两个n2n2大小归并排序的时间加上合并的时间。容易看出合并的时间是线性的,因为合并连个数组时,最多进行N-1比较,即每比较依次肯定会有一个数加入到临时数组中去的。
T(n)=T(n2n2)+n
等式两边同除以n,
T(n)nT(n)n=T(n/2)n/2T(n/2)n/2+1
该方程对2的幂的任意n都是成立的,我们每次除2可以得到下面的一系列等式:
T(n/2)n/2T(n/2)n/2=T(n/4)n/4T(n/4)n/4+1
T(n/4)n/4T(n/4)n/4=T(n/8)n/8T(n/8)n/8+1

T(2)2T(2)2=T(1)1T(1)1+1

明显这些等式一共有logn个。
然后把所有这些等式相加,消去左边和右边相等的项,所得结果为T(n)nT(n)n=T(1)1T(1)1+logn
稍作变为T(n)=nlogn+n=O(nlogn)
另外归并排序需要O(n)的空间复杂度,是一种稳定的排序算法。

堆排序

将初始数组构造成最大堆,第一趟排序,将堆顶元素arr[0]和堆底元素arr[n-1]交换位置,然后将再将arr[0]往下调整,使得剩余的n-1个元素还是堆;第i趟时,arr[0]与arr[n-i]交换,arr[0]往下调整,使得剩余的n-i个元素还是堆;直到堆中只剩一个元素结束。

    public static void heapSort(int[] arr) {
        int n = arr.length;
        // 构建初始堆
        for (int i = (n - 1) / 2; i >= 0; i--)
            percDown(arr, i, n - 1);
        // 排序,其实相当于每次删除最大元素,然后将剩下的最后一个元素换到0位置,重新调整
        for (int i = 1; i < n; i++) {
            swap(arr, 0, n - i);
            percDown(arr, 0, n - i - 1);
        }
    }

    /**
     * @param arr
     *            数组
     * @param i
     *            要调整的那个元素的位置
     * @param n
     *            堆最后一个元素的角标
     */
    private static void percDown(int[] arr, int i, int n) {
        int temp = arr[i];
        int child = 2 * i + 1;
        while (child <= n) {
            if (child < n && arr[child] < arr[child + 1])
                child++;
            if (temp < arr[child]) {
                arr[i] = arr[child];
                i = child;// 让i指向当前child的位置
                child = 2 * i + 1;// 获得新的child
            }else{
                break;
            }
        }
        arr[i] = temp;
    }

percDown函数时间复杂度不超过O(logn),构造堆的最多时间为O(nlogn)。排序部分进行n-1趟,也是O(nlogn),所以总的时间复杂度还会O(nlogn)。
一趟排序可以确定一个元素的最终位置,堆排序是不稳定的排序算法。

希尔排序

简单来说就是分组插入排序,它通过比较相距一定间隔的元素来工作;各趟比较所用的距离随着算法的进行而减小,直到只比较相邻元素的最后一趟排序为止,因此也叫作缩减增量排序。
关于增量序列h1,h2,h3,….,ht 只要是h1=1等任何增量序列都是可行的,因为最后一趟h1=1,那么进行的工作就是插入排序啊。
看到这里你也许会好奇,其实希尔排序最后一趟和插入排序做的工作就是一模一样啊,为什么在选取某些增量序列,比如Hibbard增量(1,3,7,…,2k2k-1)的情况下,时间复杂度为O(n32n32) 。

插入排序的性质:

插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

所以希尔排序每趟做的工作,会对下一趟的排序有帮助,并且希尔排序能一次移动消除多个逆序对。
这里还是选用很常见的序列 ht=N/2,hk=hk+1/2,选用这个增量序列,算法复杂度为O(n2n2)。
代码如下:

public static void shellSort(int[] arr) {
        for (int gap = arr.length / 2; gap > 0; gap = gap / 2) {
            for (int i = gap; i < arr.length; i++) {
                int j = i;
                int temp = arr[i];
                while (j >= gap && temp < arr[j - gap]) {
                    arr[j] = arr[j - gap];
                    j = j - gap;
                }
                arr[j] = temp;
            }
        }
  }

我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的

你可能感兴趣的:(基本算法之排序)