Android开发学算法-排序

467 阅读4分钟

排序常见的可以分为三大类

  • 冒泡排序、选择排序
  • 插入排序、希尔排序
  • 归并排序、快速排序

因为排序中涉及到很多的元素比较、元素交换位置、元素打印等,所以方便起见,先封装一个工具类,如下所示:


public class Utils {
    
    /**
     * 打印数组
     */
    public static void printArray(int[] array) {
        for (int i = 0; i < array.length; i++) {
            System.out.print(array[i] + "  ");
        }
        System.out.println();
    }

    /**
     * 交换数组 下标a与下标b的位置
     */
    public static void swap(int[] array, int a, int b) {
        int temp = array[a];
        array[a] = array[b];
        array[b] = temp;
    }

    /**
     *  返回 位置a的元素 是否 小于位置b的元素
     */
    public static boolean less(int[] array, int a, int b) {
        return array[a] < array[b];
    }
}

冒泡排序

思路

  • 排序过程,两两比较,i指针每一趟都会确定一个最大值
  • 因为总会确定一个位置,那么j指针的右区间也在逐渐缩小
  • i指针每一趟总是把最大值排好,那么j指针右区间逐渐变小,左区间肯定都是从0开始。因为右边排好了,左边还没排
  public void sort(int[] nums) {
        for (int i = 0; i < nums.length - 1; i++) {
            for (int j = 0; j < nums.length - 1 - i; j++) {
                if (Utils.less(nums, j + 1, j)) {
                    Utils.swap(nums, j + 1, j);
                }
            }
        }
        Utils.printArray(nums);
    }

比如输入 5,4,3,2,1 会经过下面一系列转换过程

5,4,3,2,1
4 3 2 1 5
3 2 1 4 5
2 1 3 4 5
1 2 3 4 5

选择排序

思路

  • 排序过程,i指针每一趟都会选择一个最小值,然后跟当前i交互元素
  • 与冒泡排序不同的是,选择排序在比较过程中,不着急交换位置,而是j遍历完了,再把最小值与当前i的交换元素的值
    public void sort(int[] nums) {
        for (int i = 0; i < nums.length - 1; i++) {
            int minIndex = i;
            for (int j = i + 1; j < nums.length; j++) {
                if (Utils.less(nums, j, minIndex)) {
                    minIndex = j;
                }
            }
            if (minIndex != i) {
                Utils.swap(nums, i, minIndex);
            }
        }
        Utils.printArray(nums);
    }

比如输入 5,4,3,2,1 会经过下面一系列转换过程

5,4,3,2,1
1,4,3,2,5
1,2,3,4,5
1,2,3,4,5
1,2,3,4,5

总结一下,选择排序是对冒泡排序的改进,排序思路都很相似,n条数据的话都需要n-1趟,每一趟呢都把最值给安排好,放在相应位置。不同的是,冒泡排序在比较的过程中需要两两比较,交换位置,以达到目的。而选择排序是先记录更新最新小值的下标,第i趟结束之后才选择是否交换。还需要注意的是,==冒泡排序实际上是先把最大值排好,所以右边是有序的左边无序,所以j每次要从0开始;而选择排序则相反,每次是选出最小值,放在左边,所以左边右序而右边无序,所以j要从i+1开始。==


插入排序

思路

  • 插入排序适用于,大部分数据是有序的了,通过插入排序使得最终有序
  • i指针向右 j指针向左,插入排序不会访问索引右侧的元素
   public void sort(int[] nums) {
        for (int i = 1; i < nums.length; i++) {
            for (int j = i; j > 0; j--) {
                if (Utils.less(nums, j, j - 1)) {
                    Utils.swap(nums, j, j - 1);
                }
            }
        }
        Utils.printArray(nums);
    }

归并排序

思路

  • 归并排序使用的算法思想是分治法
  • 两个过程,先分 再合并
  • 分的过程使用递归,合并过程借助一个辅助数组,将两个数组合并到辅助数组中去

  /**
     * 归并排序
     * 两个过程
     * 1.先分 再 2.合并
     */
    public void sort(int[] nums) {
        Utils.printArray(sort(nums, 0, nums.length - 1));
    }

    public int[] sort(int[] nums, int lo, int hi) {
        if (lo == hi) {
            return new int[]{nums[lo]};
        } else {
            int middle = (lo + hi) / 2;
            return merge(sort(nums, lo, middle), sort(nums, middle + 1, hi));
        }
    }

    private int[] merge(int[] a, int[] b) {
        int[] result = new int[a.length + b.length];
        int i = 0;
        int j = 0;
        int k = 0;
        while (i < a.length && j < b.length) {
            if (a[i] < b[j]) {
                result[k] = a[i];
                i++;
            } else {
                result[k] = b[j];
                j++;
            }
            k++;
        }
        if (i < a.length) {
            while (i < a.length) {
                result[k++] = a[i++];
            }
        }
        if (j < b.length) {
            while (j < b.length) {
                result[k++] = a[j++];
            }
        }
        return result;
    }

这种写法是在分的过程中就返回数组,这样在合并的过程中,直接合并俩数组就好。另外一种写法在分的过程中不返回数组,在合并的时候开辟多个辅助数组来解决,代码如下:

 /**
     * 归并排序
     * 两个过程
     * 1.先分 再 2.合并
     */
    public void sort(int[] nums) {
        Utils.printArray(nums);
        sort(nums, 0, nums.length - 1);
        Utils.printArray(nums);
    }

    public void sort(int[] nums, int lo, int hi) {
        if (lo >= hi) {
            return;
        }
        int middle = (lo + hi) / 2;
        sort(nums, lo, middle);
        sort(nums, middle + 1, hi);
        merge(nums, lo, middle, hi);
    }

    private void merge(int[] nums, int lo, int mid, int hi) {
        int[] copyA = new int[mid - lo + 1];
        int[] copyB = new int[hi - mid];
        int[] auxArray = new int[hi - lo + 1]; // 辅助数组,最后再复制到原数组nums中

        for (int i = lo; i <= mid; i++) {
            copyA[i - lo] = nums[i];
        }
        for (int i = mid + 1; i <= hi; i++) {
            copyB[i - mid - 1] = nums[i];
        }

        int i = 0;
        int j = 0;
        int k = 0;
        while (i < copyA.length && j < copyB.length) {
            if (copyA[i] < copyB[j]) {
                auxArray[k] = copyB[i];
                i++;
            } else {
                auxArray[k] = copyB[j];
                j++;
            }
            k++;
        }
        if (i < copyA.length) {
            while (i < copyA.length) {
                auxArray[k++] = copyA[i++];
            }
        }
        if (j < copyB.length) {
            while (j < copyB.length) {
                auxArray[k++] = copyB[j++];
            }
        }

        for (int l = 0; l < auxArray.length; l++) {
            nums[lo + l] = auxArray[l];
        }
    }


快速排序

思路

  • 快速排序首先要选取一个标定点作为参考,比它小的放在左边,比它大的放在右边
  • 主要是一个切分的过程partition函数,partition方法执行步骤为:假如选取的是第一个元素,那么j指针就要先向左走;反之,如果选取的是最后一个元素,那么i指针就要先向右走
  • partition过程,如图所示:
第一步 j开始向左走

image

j遇到第一个比标定点6小的数,停止脚步;i开始向右走,找到了第一个比标定点大的元素,也停止脚步

image

这时候 i 与 j交换元素,交换完成之后,j继续向左边走,重复上面步骤

image

再次交换位置

image

交换完成之后,继续j向右

image

i== j,两者相遇

image

标定点的位置与j或者i交换一下

image

完成这次切分过程,返回j或者i,就是partition的切分点

image

代码实现如下:

 public void sort(int[] nums) {
        Utils.printArray(nums);
        sort(nums, 0, nums.length - 1);
        Utils.printArray(nums);
    }

    private void sort(int[] nums, int lo, int hi) {
        if (lo >= hi) return;
        int p = partition(nums, lo, hi);
        sort(nums, lo, p);
        sort(nums, p + 1, hi);
    }

    private int partition(int[] nums, int lo, int hi) {
        int v = nums[lo];   // 标定点,假如就取第一位作为参考
        int i = lo;
        int j = hi;
        while (i < j) {
            while (i < j && nums[j] >= v) {
                j--;
            }
            // while循环结束,表示右侧大的元素都走完了,剩下是比标定点小的,等待交换元素
            
            while (i < j && nums[i] <= v) {
                i++;
            }
             // while循环结束,表示左侧小的元素都走完了,剩下是比标定点大的,等待交换元素
             
            if (i < j) {
                Utils.swap(nums, i, j);
            }
            
            // i大的元素与j小元素交换
        }
        Utils.swap(nums, lo, j);  // 最后执行完了,标定点的位置需要跟j或者i(j==i)交换
        return j;
    }

// 输   入 2 1 3 5 4
// 排序后  1 2 3 4 5

总结一下快速排序与归并排序,二者都是分治法思想,时间复杂度是O(nlogn),极端情况会退化成O(n2)。归并排序比较耗费空间,因为归并两个数组的过程,需要开辟一个辅助数组。快速排序比较不稳定,理想情况是选取一个数作为参考,每次都能一分为二,假如这个参考选择的不好,那么每次不能分为两块,就会退化成O(n2)