Day6.从排序算法看分治

179 阅读2分钟

什么是分治法

就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。

本文会用归并排序和快速排序来说明分治法。

归并排序是将“排序”(sort)问题转化为了“合并两个有序数组”(merge)。快速排序是将“排序”(sort)问题转化为了“挑选中点问题”(partition

归并排序

归并排序即将排序问题转化为了合并问题,分为自顶向下的归并和自底向上的归并。

自顶向下的归并

核心代码的结构如下

    private static void sort(int[] arr, int start, int end) {
        if (start < end) {
            int mid = (start + end) / 2;
            sort(arr, start, mid);
            sort(arr, mid + 1, end);
            merge(arr, start, mid, end);
        } else {
            return;
        }

    }

当问题规模只有1时(start>=end),问题不用继续拆分下去,可以直接解决。

实际上将sort问题转化成了多个merge问题

    public static void merge(int[] arr, int start, int mid, int end) {
        int k = start;
        int i;
        int j;
        for (i = start, j = mid + 1; i <= mid && j <= end;) {
            if (arr[i] < arr[j]) {
                aux[k] = arr[i];
                k++;
                i++;
            } else {
                aux[k] = arr[j];
                k++;
                j++;
            }
        }
        if (i > mid) {
            for (; j <= end;) {
                aux[k] = arr[j];
                k++;
                j++;
            }
        } else if (j > end) {
            for (; i <= mid;) {
                aux[k] = arr[i];
                k++;
                i++;
            }
        }
        for (k = start; k <= end; k++) {
            arr[k] = aux[k];
        }
    }

merge算法需要一个大小为n的辅助空间(aux数组)

性能分析

时间复杂度: 由于问题规模的切分是严格等分的int mid = (start + end) / 2;所以形成的二叉树是高度为logN,和输入无关,每一层的归并总时间都是O(N),所以最后的时间都是O(NlogN)

空间复杂度: 做归并时一定需要一个大小为n的辅助数组,优化方案是将辅助数组定义在全局,而不是merge的作用域中,可以避免反复申请内存。空间复杂度O(N)

全部代码

package sort;

import java.util.Random;

public class fromTopToButtonMergeSort {
    static int[] aux;

    public static void sort(int[] arr) {
        aux = new int[arr.length];
        sort(arr, 0, arr.length - 1);
    }

    private static void sort(int[] arr, int start, int end) {
        if (start < end) {
            int mid = (start + end) / 2;
            sort(arr, start, mid);
            sort(arr, mid + 1, end);
            merge(arr, start, mid, end);
        } else {
            return;
        }

    }

    public static void merge(int[] arr, int start, int mid, int end) {
        int k = start;
        int i;
        int j;
        for (i = start, j = mid + 1; i <= mid && j <= end;) {
            if (arr[i] < arr[j]) {
                aux[k] = arr[i];
                k++;
                i++;
            } else {
                aux[k] = arr[j];
                k++;
                j++;
            }
        }
        if (i > mid) {
            for (; j <= end;) {
                aux[k] = arr[j];
                k++;
                j++;
            }
        } else if (j > end) {
            for (; i <= mid;) {
                aux[k] = arr[i];
                k++;
                i++;
            }
        }
        for (k = start; k <= end; k++) {
            arr[k] = aux[k];
        }
    }

    public static void display(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + '/');
        }
    }

    public static void main(String[] args) {
        int CONST = 100000;
        int[] arr = new int[CONST];
        Random r = new Random();
        for (int i = 0; i < CONST; i++) {
            arr[i] = r.nextInt() % 4000;

        }
        System.out.println("-------自顶向下的归并排序---------");
        System.out.println("处理的数据量是:" + CONST);
        long startTime = System.currentTimeMillis();
        fromTopToButtonMergeSort.sort(arr);
        long endTime = System.currentTimeMillis();
        // display(arr);
        System.out.println("处理时间:" + (endTime - startTime) + "ms");
    }
}

自底向上的归并排序

上面的递归不同,自底向上的排序认为每一个数字都是独立有序的,两两归并,循环直到使得整个数组有序

核心代码如下

        private static void sort(int[] arr, int start, int end) {
                int length = arr.length;
                // 分组:临界条件——组长小于数组长度
                for (int size = 1; size < length; size *= 2) {
                        // 相邻合并:临界条件——中间有断开
                        for (int i = 0; i + size < length; i += size * 2) {
                                merge(arr, i, i + size - 1, Math.min(i + size + size - 1, length - 1));
                        }
                }

        }

全部代码

package sort;

import java.util.Random;

public class fromButtonToTopMergeSort {
        static int[] aux;

        public static void sort(int[] arr) {
                aux = new int[arr.length];
                sort(arr, 0, arr.length - 1);
        }

        private static void sort(int[] arr, int start, int end) {
                int length = arr.length;
                // 分组:临界条件——组长小于数组长度
                for (int size = 1; size < length; size *= 2) {
                        // 相邻合并:临界条件——中间有断开
                        for (int i = 0; i + size < length; i += size * 2) {
                                merge(arr, i, i + size - 1, Math.min(i + size + size - 1, length - 1));
                        }
                }

        }

        public static void merge(int[] arr, int start, int mid, int end) {
                int k = start;
                int i;
                int j;
                for (i = start, j = mid + 1; i <= mid && j <= end;) {
                        if (arr[i] < arr[j]) {
                                aux[k] = arr[i];
                                k++;
                                i++;
                        } else {
                                aux[k] = arr[j];
                                k++;
                                j++;
                        }
                }
                if (i > mid) {
                        for (; j <= end;) {
                                aux[k] = arr[j];
                                k++;
                                j++;
                        }
                } else if (j > end) {
                        for (; i <= mid;) {
                                aux[k] = arr[i];
                                k++;
                                i++;
                        }
                }
                for (k = start; k <= end; k++) {
                        arr[k] = aux[k];
                }
        }

        public static void display(int[] arr) {
                for (int i = 0; i < arr.length; i++) {
                        System.out.print(arr[i] + '/');
                }
        }

        public static void main(String[] args) {
                int CONST = 100000;
                int[] arr = new int[CONST];
                Random r = new Random();
                for (int i = 0; i < CONST; i++) {
                        arr[i] = r.nextInt() % 4000;

                }
                System.out.println("-------自底向上的归并排序---------");
                System.out.println("处理的数据量是:" + CONST);
                long startTime = System.currentTimeMillis();
                fromTopToButtonMergeSort.sort(arr);
                long endTime = System.currentTimeMillis();
                // display(arr);
                System.out.println("处理时间:" + (endTime - startTime) + "ms");
        }
}

快速排序

快速排序将排序问题转化成了挑选中点问题,核心代码结构如下:

    private static void sort(int[] arr, int l, int r) {
        if (l < r) {
            int p = partition(arr, l, r);
            sort(arr, l, p - 1);
            sort(arr, p + 1, r);
        } else {
            return;
        }

    }

理想情况下树的高度是logN,每一层去挑选中点的总时间都是N(跟全部元素比较),时间复杂度是NlogN

单路快速排序的时间复杂度受输入的影响,当输入高度有序或者有大量重复时,会使得左右子树不均匀,树的高度变成N,时间复杂度会从NlogN退化成N^2。

所以为了解决这个问题有了两路和三路快速排序,可以解决有大量重复元素时的性能影响。(如果时正序或者乱序,两路或者三路都没有办法解决,时间复杂度都还是N^2)

两路快速排序

两路快速排序解决大量重复元素的核心是partitionarr[++i] < varr[--j] > v都不能带等于号,虽然这样会增加与标杆元素相等项的交换成本,但是可以避免相等元素都在树的一侧(比如想象一下快排10000000000)

package sort;

import java.util.Random;

public class quickSort2Way {
    private static int partition(int[] arr, int l, int r) {

        int v = arr[l];
        int i = l, j = r + 1;
        while (true) {
            while (arr[++i] < v) {
                if (i == r)
                    break;
            }
            while (arr[--j] > v) {
                if (j == l)
                    break;
            }
            if (i >= j)
                break;
            swap(arr, i, j);
        }
        swap(arr, l, j);
        return j;
    }

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

    private static void sort(int[] arr, int l, int r) {
        if (l < r) {
            int p = partition(arr, l, r);
            sort(arr, l, p - 1);
            sort(arr, p + 1, r);
        } else {
            return;
        }

    }

    public static void main(String[] args) {
        int CONST = 100000;
        int[] arr = new int[CONST];
        Random r = new Random();
        for (int i = 0; i < CONST; i++) {
            arr[i] = r.nextInt() % 4000;
        }
        System.out.println("-------两路快速排序-----------");
        System.out.println("处理的数据量" + CONST);
        long startTime = System.currentTimeMillis();
        quickSort2Way.sort(arr, 0, arr.length - 1);
        long endTime = System.currentTimeMillis();
        System.out.println("处理时间:" + (endTime - startTime) + "ms");

    }

}

三路快速排序

三路快速排序维护三个数组,小于/大于/等于标志元素,可以进一步优化有大量相等元素的情况,降低树的高度。

package sort;

import java.util.Random;

public class quickSort2Way {
    private static int partition(int[] arr, int l, int r) {

        int v = arr[l];
        int i = l, j = r + 1;
        while (true) {
            while (arr[++i] < v) {
                if (i == r)
                    break;
            }
            while (arr[--j] > v) {
                if (j == l)
                    break;
            }
            if (i >= j)
                break;
            swap(arr, i, j);
        }
        swap(arr, l, j);
        return j;
    }

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

    private static void sort(int[] arr, int l, int r) {
        if (l < r) {
            int p = partition(arr, l, r);
            sort(arr, l, p - 1);
            sort(arr, p + 1, r);
        } else {
            return;
        }

    }

    public static void main(String[] args) {
        int CONST = 100000;
        int[] arr = new int[CONST];
        Random r = new Random();
        for (int i = 0; i < CONST; i++) {
            arr[i] = r.nextInt() % 4000;
        }
        System.out.println("-------两路快速排序-----------");
        System.out.println("处理的数据量" + CONST);
        long startTime = System.currentTimeMillis();
        quickSort2Way.sort(arr, 0, arr.length - 1);
        long endTime = System.currentTimeMillis();
        System.out.println("处理时间:" + (endTime - startTime) + "ms");

    }

}

性能分析

时间复杂度:平均时间复杂度是O(NlogN),最差情况是O(N^2),无论是优化后的两路还是三路快排都没有办法解决这个问题(高度有序时依旧时N^2)

空间复杂度:快速排序是交换排序的一种,即可以实现原地排序,不需要额外的辅助空间,空间复杂度是取决于递归调用的深度,最好情况是O(logN),最差是O(N).