经典排序算法--快速排序
一、快速排序的基本思想:
快速排序使用了分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
二、快速排序的三个步骤
1) 选择基准:在待排序列中,按照某种方式挑出一个元素,作为 “基准”(pivot);
2) 分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。如果为升序,则此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大;而基准则在排序后正确的位置上。
3) 递归地对两个序列进行快速排序,直到序列为空或者只有一个元素;
三、选择基准元的方式
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。
最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列。
方法一:固定基准元(基本的快速排序)
选取第一个元素或最后一个元素作为基准元。
1 public static void sortCommon(int[] data,int low,int high){ 2 if(high>low){ 3 //进行一次排序,将基准元放置在正确顺序的位置上,并确定当前基准元位置 4 int key = getStandard(data, low, high); 5 //分别将基准元左侧和基准元右侧当做一个序列从新进行排序,知道序列的长度为1 6 sortCommon(data, low, key-1); 7 sortCommon(data, key+1, high); 8 } 9 } 10 11 public static int getStandard(int[] data,int low,int high){ 12 //将基准元提取出来 13 int tmp = data[low]; 14 while (low<high) { 15 //从右往左查询,查找第一个小于基准元的元素,并将其放置在data[low]位置 16 while(low<high&&data[high]>=tmp){ 17 high--; 18 } 19 data[low] = data[high]; 20 //从左往右查询,查找第一个大于基准元的元素,并将其放置在data[high]位置 21 while(low<high&&data[low]<=tmp){ 22 low++; 23 } 24 data[high] = data[low]; 25 } 26 //基准元归为 27 data[low] = tmp; 28 return low; 29 }
方法二:随机基准元
思想:取待排序列中任意一个元素作为基准元。
引入的原因:在待排序列是部分有序时,固定选取基准元使快排效率底下,要缓解这种情况,就引入了随机选取基准元。
1 public static void sortRandom(int[] data,int low,int high){ 2 if(high>low){ 3 //进行一次排序,将基准元放置在正确顺序的位置上,并确定当前基准元位置 4 getStandardRandom(data, low, high); 5 int key = getStandard(data, low, high); 6 //分别将基准元左侧和基准元右侧当做一个序列从新进行排序,知道序列的长度为1 7 sortRandom(data, low, key-1); 8 sortRandom(data, key+1, high); 9 } 10 } 11 12 private static void getStandardRandom(int[] data, int low, int high) { 13 Random r = new Random(); 14 int ran = low+r.nextInt(high-low); 15 //将基准元提取出来 16 swap(data, low, ran); 17 }
方法三:三数取中
引入的原因:虽然随机选取基准时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取基准。
分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为基准元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准元。显然使用三数中值分割法消除了预排序输入的不好情形,并且减少快排大约14%的比较次数。
举例:待排序序列为:8 1 4 9 6 3 5 2 7 0
左边为:8,右边为0,中间为6
我们这里取三个数排序后,中间那个数作为枢轴,则枢轴为6
注意:在选取中轴值时,可以从由左中右三个中选取扩大到五个元素中或者更多元素中选取,一般的,会有(2t+1)平均分区法(median-of-(2t+1),三平均分区法英文为median-of-three。
具体思想:对待排序序列中low、mid、high三个位置上数据进行排序,取他们中间的那个数据作为基准,并用0下标元素存储基准。
即:采用三数取中,并用0下标元素存储基准。
1 public static void sortMiddleOfThree(int[] data,int low,int high){ 2 if(high>low){ 3 //三数去中,并将中间数换到low位置上 4 middleOfThree(data, low, high); 5 //进行一次排序,将基准元放置在正确顺序的位置上,并确定当前基准元位置 6 int key = getStandard(data, low, high); 7 //分别将基准元左侧和基准元右侧当做一个序列从新进行排序,知道序列的长度为1 8 sortMiddleOfThree(data, low, key-1); 9 sortMiddleOfThree(data, key+1, high); 10 } 11 } 12 private static void middleOfThree(int[] data, int low, int high) { 13 int middle = (high-low)/2+low; 14 if (data[middle] > data[high]) 15 { 16 swap(data, middle, high); 17 } 18 if (data[low] > data[high]) 19 { 20 swap(data, low, high); 21 } 22 if (data[middle] > data[low]) 23 { 24 swap(data, middle, low); 25 } 26 }
四. 两种优化的方法
优化一:当待排序序列的长度分割到一定大小后,使用插入排序
原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排。
截止范围:待排序序列长度N = 10,虽然在5~20之间任一截止范围都有可能产生类似的结果,这种做法也避免了一些有害的退化情形。
—-摘自《数据结构与算法分析》Mark Allen Weiness 著
1 public static void sortThreeInsert(int[] data, int low, int high){ 2 if(high-low+1<10){ 3 insertSort(data); 4 return; 5 }else{ 6 //三数去中,并将中间数换到low位置上 7 middleOfThree(data, low, high); 8 //进行一次排序,将基准元放置在正确顺序的位置上,并确定当前基准元位置 9 int key = getStandard(data, low, high); 10 //分别将基准元左侧和基准元右侧当做一个序列从新进行排序,知道序列的长度为1 11 sortThreeInsert(data, low, key-1); 12 sortThreeInsert(data, key+1, high); 13 } 14 }
优化二:在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
举例:
待排序序列 1 4 6 7 6 6 7 6 8 6
三数取中选取基准:下标为4的数6
转换后,待分割序列:6 4 6 7 1 6 7 6 8 6
基准key:6
本次划分后,未对与key元素相等处理的结果:1 4 6 6 7 6 7 6 8 6
下次的两个子序列为:1 4 6 和 7 6 7 6 8 6
本次划分后,对与key元素相等处理的结果:1 4 6 6 6 6 6 7 8 7
下次的两个子序列为:1 4 和 7 8 7
经过对比,我们可以看出,在一次划分后,把与key相等的元素聚在一起,能减少迭代次数,效率会提高不少
具体过程:在处理过程中,会有两个步骤
第一步,在划分过程中,把与key相等元素放入数组的两端
第二步,划分结束后,把与key相等的元素移到枢轴周围
1 public static void sortThreeInsertGather(int[] data, int low, int high){ 2 if(high-low+1<10){ 3 insertSort(data); 4 return; 5 }else{ 6 //三数去中,并将中间数换到low位置上 7 middleOfThree(data, low, high); 8 //进行一次排序,确定基准元位置,并将和基准元相等的元素,一直数组的两侧 9 //first 和last 用来计算基准元 10 int first = low; 11 int last = high; 12 //left和right 用来记录和基准元相等的元素 13 int left = low; 14 int right = high; 15 //leftLength和rightLength用来记录左右两侧各有多少和基准元相等的元素 16 int leftLength = 0; 17 int rightLength = 0; 18 int tmp = data[first]; 19 while(first<last){ 20 while(first<last&&data[last]>=tmp){ 21 if(data[last]==tmp){ 22 swap(data, last, right); 23 right--; 24 rightLength++; 25 } 26 last--; 27 } 28 data[first] = data[last]; 29 while(first<last&&data[first]<=tmp){ 30 if(data[first]==tmp){ 31 swap(data, first, left); 32 left++; 33 leftLength++; 34 } 35 first++; 36 } 37 data[last] = data[first]; 38 } 39 data[first] = tmp; 40 41 //一次排序完成,将两侧与基准元相等的元素移到基准元旁边 42 int i= first-1; 43 int j = low; 44 while (j < left && data[i] != tmp) 45 { 46 swap(data, i, j); 47 i--; 48 j++; 49 } 50 i = last + 1; 51 j = high; 52 while (j > right && data[i] != tmp) 53 { 54 swap(data, i, j); 55 i++; 56 j--; 57 } 58 sortThreeInsert(data, low, first-1-leftLength); 59 sortThreeInsert(data, first+1+rightLength, high); 60 } 61 } 62
以下是全部的测试程序源码:
1 package sortDemo; 2 3 import java.util.Random; 4 5 public class QuickSort { 6 7 public static void main(String[] args) { 8 Random r = new Random(); 9 int[] data = new int[100]; 10 for (int i = 0; i < data.length; i++) { 11 data[i] = r.nextInt(10); 12 } 13 Long startTime = System.currentTimeMillis(); 14 print(data); 15 Long endTime = System.currentTimeMillis(); 16 //sortCommon(data, 0, data.length-1); 17 //sortRandom(data, 0, data.length-1); 18 //sortMiddleOfThree(data, 0, data.length-1); 19 //sortThreeInsert(data, 0, data.length-1); 20 sortThreeInsertGather(data, 0, data.length-1); 21 print(data); 22 System.out.println("排序用时:"+(endTime-startTime)); 23 24 25 26 } 27 28 public static void sortCommon(int[] data,int low,int high){ 29 if(high>low){ 30 //进行一次排序,将基准元放置在正确顺序的位置上,并确定当前基准元位置 31 int key = getStandard(data, low, high); 32 //分别将基准元左侧和基准元右侧当做一个序列从新进行排序,知道序列的长度为1 33 sortCommon(data, low, key-1); 34 sortCommon(data, key+1, high); 35 } 36 } 37 38 public static int getStandard(int[] data,int low,int high){ 39 //将基准元提取出来 40 int tmp = data[low]; 41 while (low<high) { 42 //从右往左查询,查找第一个小于基准元的元素,并将其放置在data[low]位置 43 while(low<high&&data[high]>=tmp){ 44 high--; 45 } 46 data[low] = data[high]; 47 //从左往右查询,查找第一个大于基准元的元素,并将其放置在data[high]位置 48 while(low<high&&data[low]<=tmp){ 49 low++; 50 } 51 data[high] = data[low]; 52 } 53 //基准元归为 54 data[low] = tmp; 55 return low; 56 } 57 58 public static void sortRandom(int[] data,int low,int high){ 59 if(high>low){ 60 //进行一次排序,将基准元放置在正确顺序的位置上,并确定当前基准元位置 61 getStandardRandom(data, low, high); 62 int key = getStandard(data, low, high); 63 //分别将基准元左侧和基准元右侧当做一个序列从新进行排序,知道序列的长度为1 64 sortRandom(data, low, key-1); 65 sortRandom(data, key+1, high); 66 } 67 } 68 69 private static void getStandardRandom(int[] data, int low, int high) { 70 Random r = new Random(); 71 int ran = low+r.nextInt(high-low); 72 //将基准元提取出来 73 swap(data, low, ran); 74 } 75 76 public static void sortMiddleOfThree(int[] data,int low,int high){ 77 if(high>low){ 78 //三数去中,并将中间数换到low位置上 79 middleOfThree(data, low, high); 80 //进行一次排序,将基准元放置在正确顺序的位置上,并确定当前基准元位置 81 int key = getStandard(data, low, high); 82 //分别将基准元左侧和基准元右侧当做一个序列从新进行排序,知道序列的长度为1 83 sortMiddleOfThree(data, low, key-1); 84 sortMiddleOfThree(data, key+1, high); 85 } 86 } 87 private static void middleOfThree(int[] data, int low, int high) { 88 int middle = (high-low)/2+low; 89 if (data[middle] > data[high]) 90 { 91 swap(data, middle, high); 92 } 93 if (data[low] > data[high]) 94 { 95 swap(data, low, high); 96 } 97 if (data[middle] > data[low]) 98 { 99 swap(data, middle, low); 100 } 101 } 102 103 public static void sortThreeInsert(int[] data, int low, int high){ 104 if(high-low+1<10){ 105 insertSort(data); 106 return; 107 }else{ 108 //三数去中,并将中间数换到low位置上 109 middleOfThree(data, low, high); 110 //进行一次排序,将基准元放置在正确顺序的位置上,并确定当前基准元位置 111 int key = getStandard(data, low, high); 112 //分别将基准元左侧和基准元右侧当做一个序列从新进行排序,知道序列的长度为1 113 sortThreeInsert(data, low, key-1); 114 sortThreeInsert(data, key+1, high); 115 } 116 } 117 118 public static void sortThreeInsertGather(int[] data, int low, int high){ 119 if(high-low+1<10){ 120 insertSort(data); 121 return; 122 }else{ 123 //三数去中,并将中间数换到low位置上 124 middleOfThree(data, low, high); 125 //进行一次排序,确定基准元位置,并将和基准元相等的元素,一直数组的两侧 126 //first 和last 用来计算基准元 127 int first = low; 128 int last = high; 129 //left和right 用来记录和基准元相等的元素 130 int left = low; 131 int right = high; 132 //leftLength和rightLength用来记录左右两侧各有多少和基准元相等的元素 133 int leftLength = 0; 134 int rightLength = 0; 135 int tmp = data[first]; 136 while(first<last){ 137 while(first<last&&data[last]>=tmp){ 138 if(data[last]==tmp){ 139 swap(data, last, right); 140 right--; 141 rightLength++; 142 } 143 last--; 144 } 145 data[first] = data[last]; 146 while(first<last&&data[first]<=tmp){ 147 if(data[first]==tmp){ 148 swap(data, first, left); 149 left++; 150 leftLength++; 151 } 152 first++; 153 } 154 data[last] = data[first]; 155 } 156 data[first] = tmp; 157 158 //一次排序完成,将两侧与基准元相等的元素移到基准元旁边 159 int i= first-1; 160 int j = low; 161 while (j < left && data[i] != tmp) 162 { 163 swap(data, i, j); 164 i--; 165 j++; 166 } 167 i = last + 1; 168 j = high; 169 while (j > right && data[i] != tmp) 170 { 171 swap(data, i, j); 172 i++; 173 j--; 174 } 175 sortThreeInsert(data, low, first-1-leftLength); 176 sortThreeInsert(data, first+1+rightLength, high); 177 } 178 } 179 180 181 182 public static void insertSort(int[] a){ 183 //从下标为1开始比较,知道数组的末尾 184 for (int i = 1; i < a.length; i++) { 185 int j; 186 //将要比较的元素,拿出待比较过后再插入数组 187 int tmp = a[i]; 188 //一次与前一元素比较,如果前一元素比要插入的元素大,则互换位置 189 for (j = i-1; j >=0&&a[j]>tmp; j--) { 190 a[j+1] = a[j]; 191 } 192 //将比较的元素插入 193 a[j+1] = tmp; 194 } 195 } 196 197 public static void print(int[] a){ 198 for (int i = 0; i < a.length; i++) { 199 System.out.print(a[i]+" "); 200 } 201 System.out.println(); 202 } 203 204 private static void swap(int[] data,int i,int j){ 205 if(i==j){ 206 return; 207 } 208 data[i] = data[i] + data[j]; 209 data[j] = data[i] - data[j]; 210 data[i] = data[i] - data[j]; 211 } 212 }
学习整理自:http://www.cnblogs.com/HuoAA/p/4338424.html