前言:📫 作者简介:小明java问道之路,专注于研究计算机底层,就职于金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的设计和架构📫
🏆 Java 领域优质创作者、阿里云专家博主、华为云享专家🏆
🔥 如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主哦
本文导读
排序基本概念数据结构和算法中,关于排序有十大算法,包括冒泡排序,简单选择排序,简单插入排序,归并排序,堆排序,快速排序、希尔排序、计数排序,基数排序,桶排序。一般在面试中最常考的是快速排序和归并排序,并且经常有面试官要求现场写出这两种排序的代码。对这两种排序的代码一定要信手拈来才行。对于其他排序可能会要求比较各自的优劣、各种算法的思想及其使用场景,还有要知道算法的时间和空间复杂度。通常查找和排序算法的考察是面试的开始,如果这些问题回答不好,估计面试官都没有继续面试下去的兴趣都没了。所以想开个好头就要把常见的排序算法思想及其特点要熟练掌握,有必要时要熟练写出代码。我们将由易到难学习这十种算法。
想放一张总结图,最后会详细总结
冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为相关的元素会经由交换慢慢“浮”到数列的顶端。
基本思路:1、比较相邻的元素。如果第一个比第二个大(小),就交换它们两个;2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大(小)的数;3、针对所有的元素重复以上的步骤,除了最后一个;重复步骤 1~2,直到排序完成。
/**
* 类说明:冒泡算法(升序)
*/
public class BubbleSort {
public static int[] sort(int[] array) {
if (array.length == 0)
return array;
/*循环数组长度的次数*/
for (int i = 0; i < array.length; i++) {
/*从第0个元素开始,依次和后面的元素进行比较
* j < array.length - 1 - i表示第[array.length - 1 - i]
* 个元素已经冒泡到了合适的位置,无需进行比较,可以减少比较次数*/
for (int j = 0; j < array.length - 1 - i; j++) {
/*如果第j个元素比后面的第j+1元素大,交换两者的位置*/
if (array[j + 1] < array[j]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
return array;
}
}
复制代码
选择排序
选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最小数的前提下才进行交换,大大减少了交换的次数。
具体步骤:首先,找到数组中最大(小)的那个元素;其次,将它和数组的第一个元素交换位置(如果第一个元素就是最大(小)元素那么它就和自己交换);再次,在剩下的元素中找到最大(小)的元素,将它与数组的第二个元素交换位置。如此往复,直到将整个数组排序。
/**
* 类说明:简单选择排序(升序)
*/
public class ChoiceSort {
public static int[] sort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++) {
int minIndex=i;/*最小数的下标,每个循环开始总是假设第一个数最小*/
for (int j = i; j < array.length; j++) {
if (array[j] < array[minIndex]) /*找到最小的数*/
minIndex = j; /*将最小数的索引保存*/
}
/*交换最小数和i当前所指的元素*/
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
}
复制代码
插入排序
插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都有过打扑克牌的经历,特别是牌数较大的。在分牌时可能要整理自己的牌,牌多的时候怎么整理呢?就是拿到一张牌,找到一个合适的位置插入。这个原理其实和插入排序是一样的。
具体步骤:1、对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。2、为了给要插入的元素腾出空间,我们需要将插入位置之后的已排序元素在都向右移动一位。
插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。
总的来说,插入排序对于部分有序的数组十分高效,也很适合小规模数组。
/**
* 类说明:简单插入排序(升序)
*/
public class InsertionSort {
public static int[] sort(int[] array) {
if (array.length == 0)
return array;
int currentValue;/*当前待排序数据,该元素之前的元素均已被排序过*/
for (int i = 0; i < array.length - 1; i++) {
int preIndex = i;/*已被排序数据的索引*/
currentValue = array[preIndex + 1];
System.out.println("待排序元素索引:"+(i + 1)+",值为:" +currentValue+
",已被排序数据的索引:"+preIndex);
/*在已被排序过数据中倒序寻找合适的位置,如果当前待排序数据比比较的元素要小,
将比较的元素元素后移一位*/
while (preIndex >= 0 && currentValue < array[preIndex]) {
//将当前元素后移一位
array[preIndex + 1] = array[preIndex];
preIndex--;
}
/*while循环结束时,说明已经找到了当前待排序数据的合适位置,插入*/
array[preIndex + 1] = currentValue;
}
return array;
}
}
复制代码
希尔排序
一种基于插入排序的快速的排序算法(请大家先学习插入排序,了解基本的插入排序的思想。对于大规模乱序数组插入排序很慢,因为元素只能一点一点地从数组的一端移动到另一端。例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要 N-1 次移动。
希尔排序为了加快速度简单地改进了插入排序,也称为缩小增量排序,同时该算法是冲破 O(n^2)的第一批算法之一。
希尔排序是把待排序数组按一定增量的分组,对每组使用直接插入排序算法排序;然后缩小增量继续分组排序,随着增量逐渐减少,每组包含的元素越来越多,当增量减至 1 时,整个数组恰被分成一组,排序便完成了。这个不断缩小的增量,就构成了一个增量序列。
希尔排序中的增量序列
在先前较大的增量下每个子序列的规模都不大,用直接插入排序效率都较高,尽管在随后的增量递减分组中子序列越来越大,由于整个序列的有序性也越来越明显,则排序效率依然较高。
从理论上说,只要一个数组是递减的,并且最后一个值是 1,都可以作为增量序列使用。有没有一个步长序列,使得排序过程中所需的比较和移动次数相对较少,并且无论待排序列记录数有多少,算法的时间复杂度都能渐近最佳呢?但是目前从数学上来说,无法证明某个序列是“最好的”。
常用的增量序列有:希尔增量序列 :{N/2, (N / 2)/2, ..., 1},其中 N 为原始数组的长度,这是最常用的序列,但却不是最好的 Hibbard 序列:{2^k-1, ..., 3,1}Sedgewick 序列:{... , 109 , 41 , 19 , 5,1} 表达式为
/**
* 类说明:希尔排序
*/
public class ShellSort {
public static int[] sort(int[] array) {
int len = array.length;
/*按增量分组后,每个分组中,temp代表当前待排序数据,该元素之前的元素均已被排序过*/
/*gap指用来分组的增量,会依次递减*/
int currentValue, gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len; i++) {
currentValue = array[i];
/*组内已被排序数据的索引*/
int preIndex = i - gap;
/*在组内已被排序过数据中倒序寻找合适的位置,如果当前待排序数据比比较的元素要小,
并将比较的元素元素在组内后移一位*/
while (preIndex >= 0 && array[preIndex] > currentValue) {
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
/*while循环结束时,说明已经找到了当前待排序数据的合适位置,插入*/
array[preIndex + gap] = currentValue;
}
System.out.println("本轮增量【"+gap+"】排序后的数组");
gap /= 2;
}
return array;
}
}
复制代码
归并(合并)排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
若将两个有序表合并成一个有序表,称为 2-路归并,与之对应的还有多路归并。
对于给定的一组数据,利用递归与分治技术将数据序列划分成为越来越小的半子表,在对半子表排序后,再用递归方法将排好序的半子表合并成为越来越大的有序序列。
为了提升性能,有时我们在半子表的个数小于某个数(比如 15)的情况下,对半子表的排序采用其他排序算法,比如插入排序。
import java.util.Arrays;
/**
* 类说明:归并排序
*/
public class MergeSort {
public static int[] sort(int[] array) {
if (array.length < 2) return array;
/*切分数组,然后递归调用*/
int mid = array.length / 2;
int[] left = Arrays.copyOfRange(array, 0, mid);
int[] right = Arrays.copyOfRange(array, mid, array.length);
return merge(sort(left), sort(right));
}
/**
* 归并排序——将两段排序好的数组结合成一个排序数组
*
* @param left
* @param right
* @return
*/
public static int[] merge(int[] left, int[] right) {
int[] result = new int[left.length + right.length];
for (int index = 0, i = 0, j = 0; index < result.length; index++) {
if (i >= left.length)/*左边数组已经取完,完全取右边数组的值即可*/
result[index] = right[j++];
else if (j >= right.length)/*右边数组已经取完,完全取左边数组的值即可*/
result[index] = left[i++];
else if (left[i] > right[j])/*左边数组的元素值大于右边数组,取右边数组的值*/
result[index] = right[j++];
else/*右边数组的元素值大于左边数组,取左边数组的值*/
result[index] = left[i++];
}
System.out.print("左子数组:");
PrintArray.print(left);
System.out.print("右子数组:");
PrintArray.print(right);
System.out.print("合并后数组:");
PrintArray.print(result);
System.out.println("--------------------");
return result;
}
}
复制代码
快速排序
快速排序被誉为 20 世纪科学和工程领域的十大算法之一。快速排序(Quicksort)是对冒泡排序的一种改进,也是采用分治法的一个典型的应用。
首先任意选取一个数据(比如数组的第一个数)作为关键数据,我们称为基准数,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序,也称为分区(partition)操作。在实际实现时,一般会在原数组上直接操作。
通过一趟快速排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
为了提升性能,有时我们在分割后独立的两部分的个数小于某个数(比如 15)的情况下,会采用其他排序算法,比如插入排序。
快速排序中的基准数
基准的选取:最优的情况是基准值刚好取在无序区的中间,这样能够最大效率地让两边排序,同时最大地减少递归划分的次数,但是一般很难做到最优。基准的选取一般有三种方式,选取数组的第一个元素,选取数组的最后一个元素,以及选取第一个、最后一个以及中间的元素的中位数(如 4 5 6 7, 第一个 4, 最后一个 7, 中间的为 5, 这三个数的中位数为5, 所以选择 5 作为基准)。
Dual-Pivot 快排:两个基准数的快速排序算法,其实就是用两个基准数, 把整个数组分成三份来进行快速排序,在这种新的算法下面,比经典快排从实验来看节省了 10%的时间。
/**
* 类说明:快速排序
*/
public class QuickSort {
public static int[] sort(int[] array, int start, int end) {
if (array.length < 1 || start < 0 || end >= array.length || start > end)
return null;
/*数据分割成独立的两部分时,从哪儿分区的指示器*/
int zoneIndex = partition(array, start, end);
if (zoneIndex > start)
sort(array, start, zoneIndex - 1);
if (zoneIndex < end)
sort(array, zoneIndex + 1, end);
System.out.println("本轮排序后的数组");
PrintArray.printIndex(array, start, end);
System.out.println("--------------------");
return array;
}
/**
* 快速排序算法——partition
*
* @param array
* @param start
* @param end
* @return
*/
public static int partition(int[] array, int start, int end) {
int pivot = (int) (start + Math.random() * (end - start + 1));
System.out.println("开始下标:" + start + ",结束下标:" + end + ",基准数下标:"
+ pivot + ",元素值:" + array[pivot]);
/*zoneIndex是分割指示器
从业务上来说:比基准数小的,放到指示器的左边,比基准数大的,放到指示器的右边,
* 但在实际实现时,通过移动比基准数小的元素和分割指示器本身也可以达到一样的效果*/
int zoneIndex = start - 1;
swap(array, pivot, end);/*将基准数和数组尾元素交换位置*/
for (int i = start; i <= end; i++) {
if (array[i] <= array[end]) {/*当前元素小于等于基准数*/
zoneIndex++;/*首先分割指示器累加*/
if (i > zoneIndex)/*当前元素在分割指示器的右边时,交换当前元素和分割指示器元素*/
swap(array, i, zoneIndex);
}
System.out.println("zoneIndex:" + zoneIndex + ",i:" + i);
PrintArray.printIndex(array, start, end);
}
System.out.println("---------------");
return zoneIndex;
}
/**
* 交换数组内两个元素
*
* @param array
* @param i
* @param j
*/
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
复制代码
堆排序
许多应用程序都需要处理有序的元素,但不一定要求他们全部有序,或者不一定要一次就将他们排序,很多时候,我们每次只需要操作数据中的最大元素(最小元素),那么有一种基于二叉堆的数据结构可以提供支持。
所谓二叉堆,是一个完全二叉树的结构,同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。在一个二叉堆中,根节点总是最大(或者最小)节点。
这就是一个典型的二叉堆。
堆排序算法就是抓住了这一特点,每次都取堆顶的元素,然后将剩余的元素重新调整为最大(最小)堆,依次类推,最终得到排序的序列。
/**
* 类说明:堆排序
*/
public class HeapSort {
//声明全局变量,用于记录数组array的长度;
private static int len;
public static int[] sort(int[] array) {
len = array.length;
if (len < 1) return array;
//1.构建一个最大堆
buildMaxHeap(array);
//2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
while (len > 0) {
swap(array, 0, len - 1);
len--;
adjustHeap(array, 0);
PrintArray.print(array);
System.out.println("--------------------");
}
return array;
}
/**
* 建立最大堆
*
* @param array
*/
public static void buildMaxHeap(int[] array) {
//从最后一个非叶子节点开始向上构造最大堆
for (int i = (len/2-1); i >= 0; i--) {
adjustHeap(array, i);
}
System.out.println("构造完成最大堆");
PrintArray.print(array);
System.out.println("============================================");
}
/**
* 调整使之成为最大堆
*
* @param array
* @param i
*/
public static void adjustHeap(int[] array, int i) {
int maxIndex = i;
int left = 2*i+1;
int right = 2*(i+1);
//如果有左子树,且左子树大于父节点,则将最大指针指向左子树
if (left < len && array[left] > array[maxIndex])
maxIndex = left;
//如果有右子树,且右子树大于父节点,则将最大指针指向右子树
if (right < len && array[right] > array[maxIndex])
maxIndex = right;
//如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
if (maxIndex != i) {
swap(array, maxIndex, i);
adjustHeap(array, maxIndex);
}
}
/**
* 交换数组内两个元素
* @param array
* @param i
* @param j
*/
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
复制代码
计数排序
计数排序是一个排序时不比较元素大小的排序算法。
计数排序对一定范围内的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序,而且待排序元素值分布较连续、跨度小的情况。
如果一个数组里所有元素都是整数,而且都在 0-K 以内。那对于数组里每个元素来说,如果能知道数组里有多少项小于或等于该元素,就能准确地给出该元素在排序后的数组的位置。
比如有个数组
对于这个数组来说,元素 5 之前有 8 个元素小于等于 5(含 5 本身),因此排序后 5 所在的位置肯定是 7.只要构造一个(5+1)大小的数组,里面存下所有对应 A 中每个元素之前的元素个数,就能在线性时间内完成排序。
import java.util.Arrays;
/**
* 类说明:CountingSort计数排序
*/
public class CountingSort {
public static int[] sort(int[] array) {
if (array.length == 0) return array;
/*寻找数组中最大值,最小值
* bias:偏移量,用以定位原始数组每个元素在计数数组中的下标位置*/
int bias, min = array[0], max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
if (array[i] < min)
min = array[i];
}
bias = 0 - min;
/*获得计数数组的容量*/
int[] counterArray = new int[max - min + 1];
Arrays.fill(counterArray, 0);
/*遍历整个原始数组,将原始数组中每个元素值转化为计数数组下标,
并将计数数组下标对应的元素值大小进行累加*/
for (int i = 0; i < array.length; i++) {
counterArray[array[i] + bias]++;
}
System.out.println("计数数组为:");
PrintArray.print(counterArray);
System.out.println("============================================");
int index = 0;/*访问原始数组时的下标计数器*/
int i = 0;/*访问计数数组时的下标计数器*/
/*访问计数数组,将计数数组中的元素转换后,重新写回原始数组*/
while (index < array.length) {
/*只要计数数组中当前下标元素的值不为0,就将计数数组中的元素转换后,重新写回原始数组*/
if (counterArray[i] != 0) {
array[index] = i - bias;
counterArray[i]--;
index++;
} else
i++;
PrintArray.print(counterArray);
PrintArray.print(array);
System.out.println("--------------------");
}
return array;
}
}
复制代码
桶排序
桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,利用某种函数的映射关系将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序)。
基本步骤是:1、根据输入建立适当个数的桶,每个桶可以存放某个范围内的元素;2、将落在特定范围内的所有元素放入对应的桶中;3、对每个非空的桶中元素进行排序,可以选择通用的排序方法,比如插入、快排;4、按照划分的范围顺序,将桶中的元素依次取出。排序完成。
桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的 f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。
import java.util.ArrayList;
/**
* 类说明:桶排序
*/
public class BucketSort {
/**
*
* @param array
* @param bucketSize BucketSize,作为每个桶所能放置多少个不同数值
* (例如当BucketSize==5时,该桶可以存放{1,2,3,4,5}这几种数字,
* 但是容量不限,即可以存放100个3);
* @return
*/
public static ArrayList<Integer> sort(ArrayList<Integer> array, int bucketSize) {
if (array == null || array.size() < 2)
return array;
int max = array.get(0), min = array.get(0);
// 找到最大值最小值
for (int i = 0; i < array.size(); i++) {
if (array.get(i) > max)
max = array.get(i);
if (array.get(i) < min)
min = array.get(i);
}
/*获得桶的数量*/
int bucketCount = (max - min) / bucketSize + 1;
/*构建桶*/
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
ArrayList<Integer> resultArr = new ArrayList<>();
for (int i = 0; i < bucketCount; i++) {
bucketArr.add(new ArrayList<Integer>());
}
/*将原始数组中的数据分配到桶中*/
for (int i = 0; i < array.size(); i++) {
bucketArr.get((array.get(i) - min) / bucketSize).add(array.get(i));
}
/*看看桶中数据的分布*/
for (int i = 0; i < bucketArr.size(); i++) {
System.out.print("第"+i+"个桶包含数据:");
PrintArray.printObject(bucketArr.get(i));
}
for (int i = 0; i < bucketCount; i++) {
if (bucketSize == 1) {
for (int j = 0; j < bucketArr.get(i).size(); j++)
resultArr.add(bucketArr.get(i).get(j));
} else {
if (bucketCount == 1)
bucketSize--;
/*对桶中的数据再次用桶进行排序*/
ArrayList<Integer> temp = sort(bucketArr.get(i), bucketSize);
for (int j = 0; j < temp.size(); j++)
resultArr.add(temp.get(j));
}
}
return resultArr;
}
}
复制代码
基数排序
常见的数据元素一般是由若干位组成的,比如字符串由若干字符组成,整数由若干位 0~9 数字组成。基数排序按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,同时每一轮排序都基于上轮排序后的结果;当我们将所有的位排序后,整个数组就达到有序状态。比如对于数字 2985,从右往左就是先以个位为关键字进行排序,然后是十位、百位、千位,总共需要四轮。基数排序不是基于比较的算法。
基数是什么意思?对于十进制整数,每一位都只可能是 0~9 中的某一个,总共 10 种可能。那 10 就是它的基,同理二进制数字的基为 2;对于字符串,如果它使用的是 8 位的扩展 ASCII 字符集,那么它的基就是 256。
基数排序 vs 计数排序 vs 桶排序
基数排序有两种方法:1、MSD 从高位开始进行排序 2、LSD 从低位开始进行排序
这三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:1、基数排序:根据键值的每位数字来分配桶 2、计数排序:每个桶只存储单一键值 3、桶排序:每个桶存储一定范围的数值
import java.util.ArrayList;
/**
* 类说明:基数排序
*/
public class RadixSort {
public static int[] sort(int[] array) {
if (array == null || array.length < 2)
return array;
/*找出最大数*/
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
/*先算出最大数的位数*/
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
/*构建桶*/
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
/*按照从右往左的顺序,依次将每一位都当做一次关键字,然后按照该关键字对数组排序,
每一轮排序都基于上轮排序后的结果*/
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
/*遍历原始数组,投入桶中*/
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
/*桶中的数据写回原始数组,清除桶,准备下一轮的排序*/
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
}
复制代码
外部排序
有时,待排序的文件很大,计算机内存不能容纳整个文件,这时候对文件就不能使用内部排序了(我们一般的排序都是在内存中做的,所以称之为内部排序,而外部排序是指待排序的内容不能在内存中一下子完成,它需要做内外存的内容交换),外部排序常采用的排序方法也是归并排序,这种归并方法由两个不同的阶段组成:
1、采用适当的内部排序方法对输入文件的每个片段进行排序,将排好序的片段(成为归并段)写到外部存储器中(通常由一个可用的磁盘作为临时缓冲区),这样临时缓冲区中的每个归并段的内容是有序的。
2、利用归并算法,归并第一阶段生成的归并段,直到只剩下一个归并段为止。
例如要对外存中 4500 个记录进行归并,而内存大小只能容纳 750 个记录,在第一阶段,我们可以每次读取 750 个记录进行排序,这样可以分六次读取,进行排序,可以得到六个有序的归并段
每个归并段的大小是 750 个记录,记住,这些归并段已经全部写到临时缓冲区(由一个可用的磁盘充当)内了,这是第一步的排序结果。
完成第二步该怎么做呢?这时候归并算法就有用处了,算法描述如下:
1、将内存空间划分为三份,每份大小 250 个记录,其中两个用作输入缓冲区,另外一个用作输出缓冲区。首先对 Segment_1 和 Segment_2 进行归并,先从每个归并段中读取 250 个记录到输入缓冲区,对其归并,归并结果放到输出缓冲区,当输出缓冲区满后,将其写道临时缓冲区内,如果某个输入缓冲区空了,则从相应的归并段中再读取 250 个记录进行继续归并,反复以上步骤,直至 Segment_1 和 Segment_2 全都排好序,形成一个大小为 1500 的记录,然后对 Segment_3 和 Segment_4、Segment_5 和 Segment_6 进行同样的操作。
2、对归并好的大小为 1500 的记录进行如同步骤 1 一样的操作,进行继续排序,直至最后形成大小为 4500 的归并段,至此,排序结束。
排序算法分析总结
算法的稳定性
稳定:如果 a 原本在 b 前面,而 a=b,排序之后 a 仍然在 b 的前面;不稳定:如果 a 原本在 b 的前面,而 a=b,排序之后 a 可能会出现在 b 的后面;排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。
复杂度
人总是贪婪的,在做一件事的时候,我们总是期望着可以付出最少的时间、精力或者金钱来获得最大的回报,这个类比到算法上也同样适用,那就是花最少的时间和最少的存储做成最棒的解决办法,所以好的算法应该具备时效高和存储低的特点。这里的「时效」是指时间效率,也就是算法的执行时间,对于同一个问题的多种不同解决算法,执行时间越短的算法效率越高,越长的效率越低;「存储」是指算法在执行的时候需要的存储空间,主要是指算法程序运行的时候所占用的内存空间。
从我们日常的经验就可以得知,一般来说,如果处理一件事情,这件事情越大,那么我们处理所花费的时间和精力就越多,在算法中同样也如此,所以我们在讨论算法复杂度时,往往需要考虑这个待处理数据的大小,我们把待处理数据的大小称之为数据的规模大小。讨论一个算法的复杂度,往往也就是寻找程序的运行时间是如何随着问题规模的变化而变化。
而有的时候算法的运行时间还取决于数据本身分布性质而不仅仅是「问题的规模大小」。对于这样的算法,我们把它们的执行情况分为「最优情况」、「最坏情况」和「平均情况」来讨论。
某个特定的数据集能让算法的执行情况极好,这就是最「最好情况」,而另一个不同的数据会让算法的执行情况变得极差,这就是「最坏情况」。不过在大多数情况下,算法的执行情况都介于这两种极端情况之间,也就是「平均情况」。
对于「最优情况」,没有什么大的价值,因为它没有提供什么有用信息,反应的只是最乐观最理想的情况,没有参考价值。「平均情况」是对算法的一个全面评价,因为它完整全面的反映了这个算法的性质,但从另一方面来说,这种衡量并没有什么保证,并不是每个运算都能在这种情况内完成。而对于「最坏情况」,它提供了一种保证,这个保证运行时间将不会再坏了,所以一般我们所算的时间复杂度是最坏情况下的时间复杂度,这和我们平时做事要考虑到最坏的情况是一个道理。。
时间复杂度:一个算法执行所耗费的时间。
空间复杂度:对一个算法在运行过程中临时占用存储空间大小的量度。
常见复杂度
在各种不同算法中,若算法中语句执行次数(占用空间)为一个常数,则复杂度为 O(1); 当一个算法的复杂度与以 2 为底的 n 的对数成正比时,可表示为 O(log n);当一个算法的复杂度与 n 成线性比例关系时,可表示为 O (n),依次类推。
由小到大:O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)
排序算法时间复杂度助记
冒泡、选择、插入排序需要两个 for 循环,每次只关注一个元素,平均时间复杂度为 O(n2 )(外循环找元素 O(n),内循环找位置 O(n))快速、归并、希尔、堆基于分治思想,log 以 2 为底,平均时间复杂度往往和 O(nlogn)(外循环找元素 O(n),内循环找位置 O(logn))相关基数排序时间复杂度为 O(N*M),其中 N 为数据个数,M 为数据位数
快速排序的优势
从平均时间来看,快速排序是效率最高的:
快速排序中平均时间复杂度 O(nlog n),这个公式中隐含的常数因子很小,比归并排序的 O(nlog n)中的要小很多,所以大多数情况下,快速排序总是优于合并排序的。
而堆排序的平均时间复杂度也是 O(nlog n),但是堆排序存在着重建堆的过程,它把根节点移除后,把最后的叶子结点拿上来,是为了重建堆,但是,拿上的值是要比它的两个叶子结点要差很多的,它要比较很多次,才能回到合适的位置。堆排序就会有很多的时间耗在堆调整上。
虽然快速排序的最坏情况为排序规模(n)的平方关系,但是这种最坏情况取决于每次选择的基准, 对于这种情况,已经提出了很多优化的方法,比如三取样划分和 Dual-Pivot 快排。
同时,当排序规模较小时,划分的平衡性容易被打破,而且频繁的方法调用超过了 O(nlog n)为 O(n2 )省出的时间,所以一般排序规模较小时,会改用插入排序或者其他排序算法。
评论