1、基本概念
内部排序和外部排序
根据排序过程中,待排序的数据是否全部被放在内存中,分为两大类:
内部排序:指的是待排序的数据存放在计算机内存中进行的排序过程;
外部排序:指的是排序中要对外存储器进行访问的排序过程。
内部排序是排序的基础,在内部排序中,根据排序过程中所依据的原则可以将它们分为5类:插入排序、交换排序、选择排序、归并排序;根据排序过程的时间复杂度来分,可以分为简单排序、先进排序。冒泡排序、简单选择排序、直接插入排序就是简单排序算法。
评价排序算法优劣的标准主要是两条:一是算法的运算量,这主要是通过记录的比较次数和移动次数来反应;另一个是执行算法所需要的附加存储单元的的多少。
注:
本文算法使用Java实现。
代码中使用了很多注释和打印输出语句,为的是看的清楚,使用的时候可以自行移除。
为了便于理解,增加了图来说明算法的实现。
2、简单排序算法实现
2.1、冒泡法
顾名思义,两两比较交换,如同水泡咕咚咕咚往上冒,这种思路非常直观。
思路:
使用升序来说明。
第一次扫描,数据两两比较,右值大不交换,若是左值大,则左右值两两交换位置。直至n-1和n比较完。这样本次扫描完后,最大值已经在最右边了。
第二轮,从左到右继续重复上一轮的过程,只不过比起上一轮,将最右端筛选的最大值排除在比较范围内就行。其他轮次类似。
代码部分
package sort; import java.util.Arrays; public class BubbleSort { public static void main(String[] args) { int[][] tests = new int[][] { { 1, 9, 8, 5, 6, 7, 4, 3, 2 }, { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, { 9, 8, 7, 6, 5, 4, 3, 2, 1 } }; int[] test; for (int i = 0; i < tests.length; i++) { test = Arrays.copyOf(tests[i], tests[i].length); bubbleSort1(test); } for (int i = 0; i < tests.length; i++) { test = Arrays.copyOf(tests[i], tests[i].length); bubbleSort2(test); } } /** * 每扫描一次,将最大值推向右端 再次重新扫描可以减少一次和右端值的比较 */ public static void bubbleSort1(int[] test) { int swapValue; int len = test.length; int count = 0; int count_exchange = 0; System.out.println("========"); System.out.println("排序前:" + Arrays.toString(test)); // 大数在右 for (int i = 0; i < len - 1; i++) { for (int j = 0; j < len - i - 1; j++) { if (test[j] > test[j + 1]) { swapValue = test[j]; test[j] = test[j + 1]; test[j + 1] = swapValue; count_exchange++; } count++; } } System.out.printf("比较次数为%d\n交换次数为%d\n排序后:%s\n", count, count_exchange, Arrays.toString(test)); System.out.println("=========="); } /** * 改进: 增加一个标志 如果本次扫描,未做任何交换,说明数据就是所要的顺序,直接退出 */ public static void bubbleSort2(int[] test) { boolean flag = false; int swapValue; int len = test.length; int count = 0; int count_exchange = 0; System.out.println("========"); System.out.println("排序前:" + Arrays.toString(test)); // 大数在右 do { len--; flag = false; for (int i = 0; i < len && len > 0; i++) { if (test[i] > test[i + 1]) { swapValue = test[i]; test[i] = test[i + 1]; test[i + 1] = swapValue; flag = true; count_exchange++; } count++; } } while (flag); System.out.printf("比较次数为%d\n交换次数为%d\n排序后:%s\n", count, count_exchange, Arrays.toString(test)); System.out.println("=========="); } }
总结:
冒泡法要一轮一轮比较。
但是,冒泡法可以设置一个变量判断是否这一轮有交换没有交换就表明数据顺序符合要求,可以立即结束比较。
所以最差的情况就是完全和目标顺序反向,最好的情况,就是目标顺序,立即结束返回。
最差遍历n(n-1)/2,最好遍历n-1。
时间复杂度为O(n^2)。
2.2、简单选择排序
思路:
扫描给定的一组顺序排列数据(从左到右),找出一个极值(最大值或者最小值)放到一组的某一端(左端或者右端),这样就找到了一个数据,将其排除后,进行第二次扫描。也就是说第二次是在排除已经放置找出极值的某一端数据来扫描排序。
这里思考用找到最大值的方案。
假设有n个数,定义一个临时变量放置最大值的索引,第一次扫描后,扫描完后将位置n和最大值索引交换,让最大值处在某一端。以后每一次扫描都会排除已经找到放到端部的数值。
改进——二元选择排序
在每一次扫描数据的时候,既然都要比较所有数,确定一个极值,是否可以同时确定最大值和最小值呢?
完全可以。而且这样减少了迭代次数。
问题一、迭代次数问题
迭代次数看似减半,那么怎么确定呢?元素个数是奇数或者偶数,减半是多少。通过分析,减半的公式就是——元素个数/2。
注意,这里是整数相除得整数,结果是取整。
问题二、位置修正
最大值和最小值交换同一个索引处的值时,注意修正找到的索引。
代码部分
package sort; import java.text.MessageFormat; import java.util.Arrays; /** * 简单选择排序 */ public class SimpleSelectionSort { public static void main(String[] args) { int[][] tests = new int[][] { { 1, 9, 8, 5, 6, 7, 4, 3, 2 }, { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, { 9, 8, 7, 6, 5, 4, 3, 2, 1 } }; simpleSelctSort1(tests[0]); simpleSelctSort2(tests[1]); simpleSelctSort3(tests[2]); } /** * 简单排序 假定元素个数为n 遍历次数为 1+2+...+(n-1)=n(n-1)/2 一次都不能少 */ public static void simpleSelctSort1(int[] test) { // 从左到右扫描,最大值放到左边 int maxIdx, swapValue, count = 0, round_count = 0; System.out.println("==简单排序1=="); for (int i = 0; i < test.length - 1; i++) { System.out.println("运行第" + (i + 1) + "轮"); maxIdx = i; round_count = 0; for (int j = i + 1; j < test.length; j++) { if (test[j] > test[maxIdx]) { maxIdx = j; } round_count++; } if (maxIdx != i) { swapValue = test[i]; test[i] = test[maxIdx]; test[maxIdx] = swapValue; System.out.printf("本轮索引 %d 和 %d 交换位置\n", i, maxIdx); } else { System.out.println("本轮不用交换位置"); } count += round_count; System.out.println("本轮比较" + round_count + "次。\n本轮结果为" + Arrays.toString(test)); System.out.println(); } System.out.println(String.format("比较次数为%d,排序后结果为%s", count, Arrays.toString(test))); System.out.println("=========="); // 还有从右到左扫描和大数在左、在右的组合,这里就不写代码了 } /** * simpleSort1的另一种实现 从左到右扫描,最大值放到右边 这样就是有点别扭,锻炼一下思维,但是比较的次数一次都没有少 */ public static void simpleSelctSort2(int[] test) { int maxIdx, swapValue, count = 0, round_count = 0; int origin; System.out.println("==简单排序2=="); for (int i = 0; i < test.length - 1; i++, System.out.println("运行第" + i + "轮,本轮比较" + round_count + "次")) { maxIdx = origin = test.length - 1 - i; round_count = 0; for (int j = 0; j < origin; j++) { if (test[j] > test[maxIdx]) { maxIdx = j; } round_count++; } if (maxIdx != origin) { swapValue = test[origin]; test[origin] = test[maxIdx]; test[maxIdx] = swapValue; } count += round_count; } System.out.printf("比较次数为%d,排序后结果为%s\n", count, Arrays.toString(test)); System.out.println("=========="); } /** * 二元选择排序 每次扫描是否能够一次搞定2个值,最大值和最小值 从左到右扫描,最大值放到左边,最小值放右边 */ public static void simpleSelctSort3(int[] test) { int maxIdx, minIdx, minOrigin; int swapValue; int count, round_count; count = round_count = 0; int len = test.length; System.out.println("==二元选择排序=="); for (int i = 0; i < test.length / 2; i++, len--) { maxIdx = i; minOrigin = minIdx = len - 1; round_count = 0; for (int j = i; j < len; j++) { if (test[j] > test[maxIdx]) { maxIdx = j; } else if (test[j] < test[minIdx]) { minIdx = j; } round_count++; } if (maxIdx != i) { swapValue = test[i]; test[i] = test[maxIdx]; test[maxIdx] = swapValue; // 如果数组为1, 9, 2, 8, 5, 6, 7, 4, 3 // 这种情况下9和1交换后,下面执行交换就会把9和3交换,所以这里如果交换了最小值,要修正 if (i == minIdx) minIdx = maxIdx; } if (minIdx != minOrigin) { swapValue = test[minOrigin]; test[minOrigin] = test[minIdx]; test[minIdx] = swapValue; } count += round_count; System.out.println("运行第" + (i + 1) + "轮,本轮比较" + round_count + "次"); System.out.println(Arrays.toString(test)); } System.out.println(MessageFormat.format("比较次数为{0},排序后结果为{1}", count, Arrays.toString(test))); System.out.println("=========="); } }
总结:
简单排序,都必须完成所有数据的扫描n-1次。一次都不能少。不管是否已经是所要求的顺序了,还要继续比较。
遍历次数为 1+2+...+(n-1)=n(n-1)/2,时间复杂度O(n^2)。
选择排序减少了交换的次数,提高了效率。
性能略优于冒泡法。
2.3、插入排序
对一个有序数列,要求在其中插入一个数,要求插入此数后数列依然有序。
插入排序的基本思路:
每步将一个待排序的纪录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
1、假设从左至右,从小到大排列。构建一个数列A,其索引0处置空,索引从1开始放置待排序数列。
2、将当前索引 i 处的数值A[i]放置到索引0位置作为哨兵值X,比较当前索引的前一个 i-1 处数值A[i-1]和哨兵值X。
3、如果A[i-1]>X不成立,则进行第5步。
4、如果A[i-1]>X成立,它则将 i-1 处数值右移到 i 的位置,就是覆盖。
继续向左取 i-2 位置的数A[i-2]和X比较大小,大于则右移覆盖,不大于则停止比较。
5、将哨兵值放到数据右移后的空档中。
代码部分
package sort; import java.util.Arrays; public class InsertionSort { /** * 插入排序 最终排序为从左到右,从小到大 思路: 索引0位置设置哨兵,即本轮待比较的值的副本 * 依次从待比较值的前一个位置开始取值,与哨兵值比较,如果大于哨兵值就右移,然后继续比较; * 如果不大于则停止比较 将索引0的位置值放置到右移后的空位处 */ private static void insertSort(int[] arr) { if (arr == null || arr.length < 2) { return; } int i, j; for (i = 2; i < arr.length; i++) { // 当前位置的值放置在第一个位置作为哨兵 arr[0] = arr[i]; // 如果前一个值大于哨兵值,则右移。然后继续查看左边的值是否大于哨兵值 j = i - 1; // 如果是小于就直接下一轮 if (arr[j] > arr[0]) { for (; arr[j] > arr[0]; j--) arr[j + 1] = arr[j]; // 将哨兵值放入左侧的空位 arr[j + 1] = arr[0]; // 尤其要注意j+1,因为j--多减了一个1 } System.out.println(Arrays.toString(arr)); } } public static void main(String[] args) { int b[] = new int[] { 0, 3, 8, 5, 7, 2, 4, 9, 6, 1 }; insertSort(b); } }
总结:
哨兵的作用:
保留每轮比较中待比较数的值
和哨兵比较可以防止查询条件的索引越界,而且提高了循环中条件判断的效率。
时间复杂度为O(n^2)的稳定排序方法。性能略优于前两种算法。
本文简单讲述了简单排序算法的思路和算法的实现,这只是一个开端,后续会继续完成其他算法的博客。
最终,引出海量数据排序的问题,将基于Hadoop的MapReduce实现。