zoukankan      html  css  js  c++  java
  • 快速排序中的分割算法的解析与应用

    一,分割(partition)算法介绍

    所谓分割算法,先选定一个枢轴元素,然后 将数组中的元素分成两部分:比枢轴元素小的部分都位于枢轴元素左边;比枢轴元素大的部分都位于枢轴元素右边

    此时,枢轴元素在数组中的位置就被“永久地确定”下来了---将整个数组排序,该枢轴元素的位置不会变化。

    另外,枢轴元素的选取对分割算法至关重要。一般而言,终极追求的是:将数组平分。因此,尽可能地让枢轴元素的选取随机化和靠近中位数。

    这里采用“三数取中”法选取枢轴元素。

    关于快速排序排序算法,可参考:排序算法总结之快速排序

    二,分割算法的实现

     1 //分割数组,将数组分成两部分. 一部分比pivot(枢轴元素)大,另一部分比pivot小
     2     private static int parition(int[] arr, int left, int right){
     3         
     4         int pivot = media3(arr, left, right);
     5         int i = left;
     6         int j = right - 1;//注意 ,在 media3()中 arr[right-1]就是 pivot
     7         
     8         for(;;)
     9         {
    10             while(arr[++i] < pivot){}
    11             while(arr[--j] > pivot){}
    12             if(i < j)
    13                 swap(arr, i, j);
    14             else
    15                 break;
    16         }
    17         
    18         swap(arr, i, right-1);//restore pivot, 将枢轴元素放置到合适位置:arr左边元素都比pivot小,右边都比pivot大
    19         return i;// 返回 pivot的 索引
    20     }

    ①第4行,枢轴元素是通过“三数取中”法选择的。在“三数取中”时,还做了一些优化:将 枢轴元素 放到 数组末尾的倒数第二个位置处。具体参考 media3()
    需要注意的是:当输入的数组中长度为1 或者 2 时, partition会出现向下越界(但对快排而言,当数组长度很小的,其实可以不用 partition,而是直接用插入排序)。因此,可加入以下的修改。

     1 //分割数组,将数组分成两部分. 一部分比pivot(枢轴元素)大,另一部分比pivot小
     2     private static int parition(int[] arr, int left, int right){
     3         
     4         int pivot = media3(arr, left, right);
     5         int i = left;
     6         int j = right - 1;//注意 ,在 media3()中 arr[right-1]就是 pivot
     7         
     8         //应对特殊情况下的数组,比如数组长度 小于3
     9         if(i >= j)
    10             return i;
    11         
    12         for(;;)
    13         {
    14             while(arr[++i] < pivot){}
    15             while(arr[--j] > pivot){}
    16             if(i < j)
    17                 swap(arr, i, j);
    18             else
    19                 break;
    20         }
    21         
    22         swap(arr, i, right-1);//restore pivot 将枢轴元素放置到合适位置:arr左边元素都比pivot小,右边都比pivot大
    23         return i;// 返回 pivot的 索引
    24     }

    再来看看,三数取中算法,这里也有个特殊情况:当数组中元素个数都没有3个时....怎么办?

     1     //三数取中,用在快排中随机选择枢轴元素时
     2     private static int media3(int[] arr, int left, int right){
     3         if(arr.length == 1)
     4             return arr[0];
     5         
     6         if(left == right)
     7             return arr[left];
     8         
     9         int center = (left + right) / 2;
    10         
    11         //找出三个数中的最小值放到 arr[left]
    12         if(arr[center] < arr[left])
    13             swap(arr, left, center);
    14         if(arr[right] < arr[left])
    15             swap(arr, left, right);
    16         
    17         //将 中间那个数放到 arr[media]
    18         if(arr[center] > arr[right])
    19             swap(arr, center, right);
    20         
    21         swap(arr, center, right-1);//尽量将大的元素放到右边--将privot放到右边, 可简化 分割操作(partition).
    22         return arr[right-1];//返回中间大小的那个数
    23     }

    其实,这里的“三数取中”的实现,与参考资料中提到的三数取中实现有一点不同。这是正常的,毕竟实现细节不同。如果有错误,需要自行调试。

    这里提下第3-7行的两个if语句:当需要 “取中”的目标数组长度为1时,或者说 对数组中某些范围内[left, right]的元素进行“取中”时,若left=right,则根本就没有3个数,违背了“三数取中”的本意(随机地选取枢轴元素),故直接 return。

    当数组中元素只有一个时,第18行会越界。为了防止这种情况,在第3-4行就先对数组长度进行判断。当数组中只有两个元素,其实就相当于 center=left,因此,程序也没问题。

    三,分割算法的应用---O(N)时间复杂度找出无序数组中第k大的元素

    给定一个数组,数组中某个元素出现的次数超过了数组大小的一半,找出这个元素。

    比如输入:[2,5,4,4,5,5,5,6,5] ,输出 5

    这个问题,其实可以转化成求解中位数问题。因为,当数组有序时,出现次数超过一半的那个元素一定位于数组的中间。

    所谓中位数,就是 假设 数组是有序的情况下,中间那个元素。即 arr[arr.length/2]

    而要求解中位数,当然可以先对数组进行排序,但排序的时间复杂度为O(NlogN),那有没有更快的算法?

    当然是有的。就是借助partition分割算法 来 实现。

     1 //找出 arr 中 第  n/2  大的那个元素
     2     public static int media_number(int[] arr){
     3         int left = 0;
     4         int right = arr.length - 1;
     5         int center = (left + right) / 2;
     6         
     7         int pivot_index = parition(arr, left, right);//枢轴元素的数组下标
     8         
     9         while(pivot_index != center)
    10         {
    11             if(pivot_index > center){
    12                 right = pivot_index - 1;
    13                 pivot_index = parition(arr, left, right);
    14             }
    15             else{
    16                 left = pivot_index + 1;
    17                 pivot_index = parition(arr, left, right);
    18             }
    19         }
    20         return arr[center];
    21     }

    上面算法不仅可以求解“找出超过一半的数字”,也可以求解任何一个数组的中位数。

    这里递归表达式 T(N)=T(N/2)+O(N),O(N)表示将数组 分成两部分所花的代价。

    故时间复杂度为O(N)

    四,参考资料

    排序算法总结之快速排序

     整个完整代码

    public class Middle_Large {
        
        //找出 arr 中 第  n/2  大的那个元素
        public static int media_number(int[] arr){
            int left = 0;
            int right = arr.length - 1;
            int center = (left + right) / 2;
            
            int pivot_index = parition(arr, left, right);
            
            while(pivot_index != center)
            {
                if(pivot_index > center){
                    right = pivot_index - 1;
                    pivot_index = parition(arr, left, right);
                }
                else{
                    left = pivot_index + 1;
                    pivot_index = parition(arr, left, right);
                }
            }
            return arr[center];
        }
        
        //分割数组,将数组分成两部分. 一部分比pivot(枢轴元素)大,另一部分比pivot小
        private static int parition(int[] arr, int left, int right){
            
            int pivot = media3(arr, left, right);
            int i = left;
            int j = right - 1;//注意 ,在 media3()中 arr[right-1]就是 pivot
            
            //应对特殊情况下的数组,比如数组长度 小于3
            if(i >= j)
                return i;
            
            for(;;)
            {
                while(arr[++i] < pivot){}
                while(arr[--j] > pivot){}
                if(i < j)
                    swap(arr, i, j);
                else
                    break;
            }
            
            swap(arr, i, right-1);//restore pivot 将枢轴元素放置到合适位置:arr左边元素都比pivot小,右边都比pivot大
            return i;// 返回 pivot的 索引
        }
        
        
        //三数取中,用在快排中随机选择枢轴元素时
        private static int media3(int[] arr, int left, int right){
            if(arr.length == 1)
                return arr[0];
            
         if(left == right)
    return arr[left];
    int center = (left + right) / 2; //找出三个数中的最小值放到 arr[left] if(arr[center] < arr[left]) swap(arr, left, center); if(arr[right] < arr[left]) swap(arr, left, right); //将 中间那个数放到 arr[media] if(arr[center] > arr[right]) swap(arr, center, right); swap(arr, center, right-1);//尽量将大的元素放到右边--将privot放到右边, 可简化 分割操作(partition). return arr[right-1];//返回中间大小的那个数 } private static void swap(int[] arr, int left, int right){ int tmp = arr[left]; arr[left] = arr[right]; arr[right] = tmp; } public static void main(String[] args) { int[] arr = {5,6,8,4,1,5,5,5,5}; int result = media_number(arr); System.out.println(result); } }

    另外,再写了一个寻找第K(K从1开始)大元素的程序:

    public class FindKLargest {
    
        public static <T extends Comparable<? super T>> T findK(T[] arr, int k) {
            k = k - 1;
    
            if (arr == null || arr.length == 0) {
                throw new IllegalArgumentException("array is null");
            }
    
            if (k < 0) {
                throw new IllegalArgumentException("k must be > 0");
            }
    
            if (k > arr.length - 1) {
                k = arr.length - 1;
            }
    
            int low = 0;
            int high = arr.length - 1;
            int pivot_index = partition(arr, low, high);
            while (pivot_index != k) {
                if (pivot_index > k) {
                    high = pivot_index - 1;
                    pivot_index = partition(arr, low, high);
                }else {
                    low = pivot_index + 1;
                    pivot_index = partition(arr, low, high);
                }
            }
            return arr[pivot_index];
        }
    
        public static <T extends Comparable<? super T>> int partition(T[] arr, int low, int high) {
            T pivot = pick(arr, low, high);
            int i = low;
            int j = high;
            for (; ; ) {
                while (arr[i++].compareTo(pivot) == -1) {
                }
                while (arr[j--].compareTo(pivot) == 1) {
                }
                if (i < j) {
                    swap(arr, i, j);
                } else {
                    break;
                }
            }
            return i - 1;
        }
    
    
        private static <T extends Comparable<? super T>> T pick(T[] arr, int low, int high) {
            return arr[(low + high) / 2];
        }
    
        private static <T extends Comparable<? super T>> void swap(T[] arr, int i, int j) {
            T tmp = arr[i];
            arr[i] = arr[j];
            arr[j] = tmp;
        }
    
    
        public static void main(String[] args) {
            String[] strings = {"abc", "bcd", "def"};
            System.out.println(findK(strings, 1));
    //        System.out.println(findK(strings, 0));
    
    
            String[] strings1 = {"abc"};
            System.out.println(findK(strings1, 2));
    
            Long[] longs = {1L, 2L, 3L, 4L, 5L};
            System.out.println(findK(longs, 5));
            System.out.println(findK(longs, 1));
            System.out.println(findK(longs, 2));
        }
    }

    原文:https://www.cnblogs.com/hapjin/p/5587014.html

  • 相关阅读:
    Coursera Algorithms Programming Assignment 2: Deque and Randomized Queue (100分)
    Coursera Algorithms week1 查并集 练习测验:3 Successor with delete
    Coursera Algorithms week1 查并集 练习测验:2 Union-find with specific canonical element
    项目选题报告答辩总结
    项目UML设计(团队)
    第七次作业--项目需求分析(团队)
    结对项目--第二次作业
    【软件工程实践 · 团队项目】 第二次作业
    第五次作业--原型设计(结对)
    团队项目系列博客 —— 在路上(之wampserver 修改根目录以及配置多站点以及修改端口号)
  • 原文地址:https://www.cnblogs.com/hapjin/p/5587014.html
Copyright © 2011-2022 走看看