基础算法之排序

来源:互联网 发布:360wifi网络不稳定 编辑:程序博客网 时间:2024/06/08 02:50

转载请注明出处:http://blog.csdn.net/pngfi/article/details/52154785

1.简单选择排序

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

  • 临时变量
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 。 该算法是不稳定的,很容易理解,由于交换的时候,会改变两个相等值得相对位置。

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]” 那么就不稳定了。

3.冒泡排序

第一趟在数组(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²); 冒泡排序是稳定的排序算法,这当然也与算法中的判断条件相关。

4.快速排序

快排简略来说就是一种分治的思想。对一个数组,选定一个基准元素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<right;i++){            if(arr[i]<base){                small++;                swap(arr,small,i);            }        }        small++;        swap(arr, small,right);        return small;    }

非递归版

    public static void quickSort(int[] arr){        Deque<Integer> leftStack=new ArrayDeque<Integer>();        Deque<Integer> rightStack=new ArrayDeque<Integer>();        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<right){                leftStack.addFirst(p+1);                rightStack.addFirst(right);            }        }    }

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

5.归并排序

把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]<arr[rightPosition]){                tempArr[tempPositon++]=arr[leftPostion++];            }else {                tempArr[tempPositon++]=arr[rightPosition++];            }        }        while(leftPostion<=leftEnd){            tempArr[tempPositon++]=arr[leftPostion++];        }        while(rightPosition<=rightEnd){            tempArr[tempPositon++]=arr[rightPosition++];        }        //复制到原数组        for(int i=begin;i<=rightEnd;i++){            arr[i]=tempArr[i];        }    }private static void mergeSort(int[] arr, int[] tempArr, int left, int right) {            if(left<right){//如果只有一个子数组只有一个元素就直接返回                int mid=(left+right)/2;                mergeSort(arr,tempArr,left,mid);                mergeSort(arr,tempArr,mid+1,right);                merge(arr, tempArr, left, mid+1, right);            }    }public static void mergeSort(int[] arr) {        int[] tempArr=new int[arr.length];        mergeSort(arr,tempArr,0,arr.length-1);    }

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

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

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

6.堆排序

将初始数组构造成最大堆,第一趟排序,将堆顶元素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)。
一趟排序可以确定一个元素的最终位置,堆排序是不稳定的排序算法。

7.希尔排序

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

插入排序的性质:

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

  • 所以希尔排序每趟做的工作,会对下一趟的排序有帮助,并且希尔排序能一次移动消除多个逆序对。

这里还是选用很常见的序列 ht=N/2,hk=hk+1/2,选用这个增量序列,算法复杂度为O(n2)。
代码如下:

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排序是不稳定的。

0 0
原创粉丝点击