排序算法

一、冒泡排序

  冒泡排序需要不断遍历需要排序的数组,一次比较两个数组元素,如果他们的排序不符合排序规则(例如从小到大),就将这两个元素的值进行交换。不断遍历数组直到没有数组元素需要置换则说明排序完成。

  算法步骤:

  1. 比较两个相邻的数组元素,如果他们的排序不符合排序规则就将他们的值进行交换

  2. 不断比较每对相邻的数组元素,从第一组到末尾的最后一组。遍历完成后,最后的元素会是最大值。

  3. 持续所有元素进行以上两种操作,除了每一轮遍历的最后一个数组元素。

  4. 持续每次对越来越少的元素进行以上操作,直到没有数组元素需要置换为止。

  时间复杂度: O(n^2)  空间复杂度 :O(1)  稳定性:稳定

  动图演示

  

  JAVA 实现

public void sort(int[] arr) {
        //得到数组的长度
        int len = arr.length;
        // 确定每一次进行冒泡排序遍历的数组范围,从 0 到 i
        for (int i = len - 1; i >= 0; i--) {
            // j 表示这组需要进行比较的数据中下标小的那一位,j 从 0 到 i-1
            // 比较相邻的元素,如果不符合排序规则就交换他们的值
            // 设置标志位 flag,每次开始一轮新的比较是将其设置为 true,有数据进行交换时,将其设置为 false,
            //    若一轮下来flag 仍为 true,则排序完成
            boolean flag = true;
            for (int j = 0; j < i; j++) {
                if (arr[j] > arr[j + 1]) {
                    arr[j] = arr[j] + arr[j + 1];
                    arr[j + 1] = arr[j] - arr[j + 1];
                    arr[j] = arr[j] - arr[j + 1];
flag
= false; } } if (flag) break; } }

 

二、选择排序

  算法描述

  1. 每次从未排序的数组中选取最小(大)的元素放在未排序数组的第一位

  2. 从剩下的未排序的数组中选取最小(大)的元素放在未排序数组的第一位。

  3. 重复步骤二,直至所有元素排序完成

  时间复杂度:O(n^2)  空间复杂度:O(1)  稳定性:不稳定

  动图演示

  

  JAVA 实现

public void sort(int[] arr) {
        // 得到数组的长度
        int len = arr.length;
        // 未排序数组从 i 到 len-1
        for (int i = 0; i < len; i++) {
            // min 记录未排序数组最小值的下标
            int min = i;
            // 遍历完整个未排序数组,得到最小值小标,将其与该未排序数组的第一个元素交换
            for (int j = i+1; j < len; j++) {
                if (arr[j] < arr[min])
                    min = j;
            }
            if (min > i) {
                arr[i] = arr[i] + arr[min];
                arr[min] = arr[i] - arr[min];
                arr[i] = arr[i] - arr[min];
            }
        }
    }

三、插入排序

  插入排序是一个比较有意思的排序方式,它的实现比较简单,但是又有点值得深究的东西。

  插入排序先使得 0 ~ 0 有序(这相当于废话);再往后添加一个元素,使得 0 ~ 1 有序;往后不断添加元素,当排到第 m 个元素时,前 0 ~ m-1 均为顺序排序,此时你要做的就是将 0 ~ m 变为顺序排序,怎么实现呢?

  例如以下数组 array

  

  (1)使得 0 ~ 0 有序(完成)

  

  (2)m = 1,使得 0 ~ 1 有序

  

  因为 array[0] > array[1] 不成立,所以排序完成。

  (3)m = 2 ,使得 0 ~ 2有序

  

  因为 array[1] > array[2] ,所以将 array[1] 与  array[2] 调换位置,变成

  

   因为 array[0] > array[1] ,所以将 array[0] 与  array[1] 调换位置,变成

  

   因为此时已经到达了下标为 0 的位置,因此排序完成。

  (4)往后的元素也是这样,原下标为 m 的元素,不断与 0 ~ m- 1 中的元素进行比较、交换,直到左边相邻的数小于等于它 或者 已经到达了边界,则该轮排序完成。

  JAVA 实现

  public void insertionSort(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;
        }
        // 0 ~ i-1 有序
        // 使得 0 ~ i 之间有序
        for (int i = 1; i < arr.length; i++) {
            // 比较相邻两个元素 j 和 j+1,当 arr[j] > arr[j+1] 并且 j >= 0 时将两个数交换并继续向前比较,否则排序完成
            for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--) {
                swap(arr, j, j + 1);
            }
        }
    }
    public void swap(int[] arr, int i, int j) {
        if(arr[i] == arr[j])
            return;
        arr[i] = arr[i] ^ arr[j];
        arr[j] = arr[i] ^ arr[j];
        arr[i] = arr[i] ^ arr[j];
    }

 

   插入排序的 时间复杂度为 O(n^2),但是插入排序和选择排序、冒泡排序不同的是:选择排序和冒泡排序的算法流程与数据状况无关,时间复杂度没有变化,例如

  

   这两个特殊数组,

  选择排序在两个数组中都会遍历 0~ n-1, 1~n-1,...., n-2~n-1,时间复杂度为 O(N^2);插入排序在两个数组中都会遍历 0 ~ n-1, 0 ~ n-2,... ,0~1,时间复杂度为 O(N^2)

  插入排序与之不同的是,在第一个数组中,每次在 0 ~ m 个元素中都会进行 m 次比较、交换,时间复杂度为 O(N^2);但是在第二个数组中,每次在 0 ~ m 中只需要进行一次比较,不需要交换,时间复杂度为 O(N).

  因此,如果需要排序的数组大部分是有序的,使用插入排序的效率甚至会比 O(N*logN) 级别的算法还好。例如对日志进行排序,因为日志是按时间顺序生成的,所以大部分是有序的,可能小部分在传输的过程中调换位置导致错序,这种情况下,使用插入排序效率就很高。

  插一句:我个人觉得,插入排序和冒泡排序虽然思想不一样,但是实现是差不多的。然而插入排序是在一段已经排好序的数组上进行操作,而冒泡排序虽然每次都缩短了需要操作的数组的长度,但是数组都是无序的,没有利用上一次排序的结果,所以大部分时候冒泡排序的效率更低一些也不是不能理解了。

  希尔排序 

  希尔排序是插入排序的一种拓展运用,与插入排序不同的是,一次希尔排序两个比较的值之间的距离不一定是 1,可以是任意长度,逐渐减小,但是最后都需要进行一次距离为 1 的插入排序,而插入排序两个值之间的距离一直都是 1。这里具体不展开了,只提供了代码参考。

public void ShellSort(int[] arr, int len) {
        int h = 0;
        while (h < len / 3) {
            h = h * 3 + 1; // 1,4,13,40,...  h 的规则可以自己设置
        }

        while (h > 0) {
            for (int i = h; i < len; i++) { // i : 现在正在排序的元素下标
                int temp = arr[i];
                int j = i;
                for (; j >= h && temp <= arr[j - h]; j -= h) {
                        arr[j] = arr[j - h];
                }
                arr[j] = temp;
            }
            h = h / 3;
        }
    }

 

四、归并排序

  归并排序的 时间复杂度为 O(N*logN),空间复杂度为 O(N);

  1. 算法流程

  (1)实际上是一个简单递归,从数组的中间 mid 将数组分开,使之左边排好序,右边排好序,再将其整体排序

  (2)将其整体排序的过程里使用了排外序方法。

  2. 所谓排外序方法,就是创建一个与排序数组等大的数组空间,将其从中间分为两段,分别从两端的第一个元素(设为 p1,p2)开始比较,若 p1 指向元素小于等于 p2指向元素,则将 p1 放入新数组,p1++,反之放入 p2,p2++

  不断比较放置,直至两组元素中一组的元素已全部拷进新数组空间,则将另一组的剩下所有元素拷进新数组空间。

  最后,将新数组空间中的值全部拷贝回原数组的原数组区间中即可。

  3. JAVA 实现

  public static void mergeSort(int[] arr) {
        if(arr == null || arr.length <2) {
            return;
        }
        process(arr, 0, arr.length-1);
    }
    // 在 [L,...,R]上,排好序
    public static void process(int[] arr, int L, int R) {

        // 终止条件:当需要排序的数组区间中只有一个元素时,说明排序完成,返回
        if (L == R)
            return;
        // 排序数组中位数的下标
        int mid = L + ((R - L) >> 1);
        // 得到以 mid 为中电划分的两个数组的有序数组
        process(arr, L, mid);
        process(arr, mid + 1, R);
        // 将该排序数组区间的元素进行整体的排序
        merge(arr, L, mid, R);

    }

    public static void merge(int[] arr, int L, int M, int R) {
        // 先开辟一个与传进来的需排序数组等大的空间
        int[] space = new int[R - L + 1];

        int i = 0;// 新数组的下标
        int p1 = L;// 左边数组的下标
        int p2 = M + 1;// 右边数组的下标
        // 当以mid 划分的两个数组都没有越界时
        while (p1 <= M && p2 <= R) {
            // 当 arr[p1] <= arr[p2] ,将arr[p1] 赋值给 arr[i],接着 p1 ++,i++
            space[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }
        // 当左边数组越界时
        //由于上一个 while 的跳出条件必须为一边越界,所以右边没有越界,说明左边一定越界,
        //因此可以省略判断左边是否越界的 p1 > M 这个条件,下同
        while ( p2 <= R) {
            space[i++] = arr[p2++];
        }
        // 当右边数组越界时
        while ( p1 <= M) {
            space[i++] = arr[p1++];
        }
        // 将新开辟的空间中的数组拷贝回原数组
        for (i = 0; i < space.length; i++) {
            arr[L + i] = space[i];
        }
    }

  4. 特别注意,归并排序的时间复杂度只有 O(N*logN),你可能一下子没有明白为什么它就比选择排序,冒泡排序好呢?

  首先先不提它为什么好,先来说说为什么选择排序这个 O(N^2),为什么不好。

  选择排序第一步在 0 ~ n-1 中比较 n-1 次 选择最小值将其放进 arr[0],从而完成第一个数的排序;第二部在 1 ~ n-1 中比较 n-2 次选择最小的值放进 arr[1],.....

  也就是说,选择排序每轮会比较 n 次以完成一个数的排序任务,并且每轮之间的比较是独立的,很显然,这大大的浪费了比较资源,导致效率降低。

  而归并排序每一次的比较都会完成一个元素的排序,并且每一轮排序的结果(数组),会和下一个数组 merge,并没有浪费比较资源。

   5. 我们都知道,递归都是可以写成循环的,在归并排序中,我们可以用 p 进行控制,

  当 p = 1 时表示相邻一个数之间 进行merge,相当于废话,因为相邻一个属即只有一个数;

  下一步,p = 2 表示相邻两个数之间进行 merge(mid = L + p/2 - 1);

  下一步,p = 4 mid = L + p/2 - 1,也就是 p 的一半。

  左右数组的长度为 p/2,如果没有右数组,则不进行 merge。

  ( 这个算法是我根据自己的理解写出来,如果有不完备的地方,希望可以得到指点)

    public static void mergeSort(int[] arr) {
        int len = arr.length;
        int p = 2;

        // 当由数组至少还有一个数时,都要进行排序
        while (p / 2 < len) {
            int L = 0;
            int M = L + p / 2 - 1;
            int R = p - 1;
            while (R <= len - 1) {
                merge(arr, L, M, R); // 和递归算法中的 merge 相同
                L += p;
                M += p;
                R += p;
            }
            if (R > len - 1 && M < len - 1) {
                merge(arr, L, M, len - 1);
            }
            p = (p << 1);
       }
  }

 

  从循环实现可以很明显得看出为什么归并排序的时间复杂度是 O(N*logN)

  因为 p 取值为 2,4,8,16,.......,log2(N)

  每一轮循环都会进行 N 进比较。

  所以就是 O(N*logN)

五、堆排序

  详解 堆结构及堆排序:https://www.cnblogs.com/lyw-hunnu/p/12762698.html

    // 堆排序
    public static void heapSort(int[] arr) {

        if (arr == null || arr.length < 2) {
            return;
        }
        // 形成大根堆:
        // O(N)
        for (int index = arr.length - 1; index >= 0; index--) {
            heapify(arr, index, arr.length);
        }

        // 将堆中最大值逐个放在 arr.length-1,arr.length-2,...,1 的位置
        // O(N*logN)
        int heapSize = arr.length;// 记录堆中元素的个数,如果一个数排序完成(放在堆数组的最后),就将他从堆数组中除去(heapSize--);
        swap(arr, 0, --heapSize);// 将堆中的最大值放在数组的最后,此时最大值排序完成,堆数组的个数减一
        while (heapSize > 0) {// O(N)
            heapify(arr, 0, heapSize);// O(logN)
            swap(arr, 0, --heapSize);// O(1)
        }
    }

六、快速排序

  快速排序采用分治的思想来进行排序,将一个序列分为较小和较大两个子序列,在递归地排序两个子序列,

  算法步骤

  1. 在数组中寻找一个”基准“,选取”基准“有很多方法,可以直接将数组地第一个元素当作”基准“

  2. 对数组进行分割:将数组中比”基准“小的值都排到”基准“的前面,比”基准“大的值都排到”基准“的后面。

  3. 递归排序子序列:递归地将小于基准值的序列 和 大于基准值的序列进行排序。

  时间复杂度:O(nlogn )  空间复杂度:O(nlogn)  稳定性:不稳定

  JAVA 实现

    public void sort(int[] arr) {
        quick(arr, 0, arr.length - 1);
    }

    /**
     * 快速排序的递归方法,当数组序列中的元素个数 大于等于 1 时调用
     * @param arr  需要排序的数组
     * @param start 数组序列的开始的下标
     * @param end  数组序列的结束的下标
     */
    public void quick(int[] arr, int start, int end) {

        if (1 == start - end)
            return;
        // 保存参数值
        int left = start;
        int right = end;
        // 选取基准数
        int base = arr[start];
        // 将数组序列分割为两个子序列
        while (left < right) {
            // 从right开始向左遍历,找到比 基准小的元素的下标
            while (left < right && arr[right] > base) {
                right--;
            }
            // 从 left 向有遍历,找到比基准大的元素的下标
            while (left < right && arr[left] < base) {
                left++;
            }
            // 根据设置的条件可知,left > right 在这里不会存在
            // 当两边分别找到与 基准相同的值而停止时,打破僵局
            // 否则,将下标为 left 和 right 的值进行交换,
            if (arr[left] == arr[right] && left < right) {
                left++;
            } else {
                // 不可以用这种置换方式,最后一次内循环,到达这一步时, left == right
                // arr[left] = arr[left] + arr[right];
                // arr[right] = arr[left] - arr[right];
                // arr[left] = arr[left] - arr[right];
                int temp = arr[left];
                arr[left] = arr[right];
                arr[right] = temp;
            }
        }
        // 递归排序两个子序列
        if (start < left - 1)
            quick(arr, start, left - 1);
        if (end > right + 1)
            quick(arr, right + 1, end);

    }

 

posted @ 2020-04-16 02:41  葡萄籽pp  阅读(132)  评论(0编辑  收藏  举报