内部排序是在内存中进行排序,而外部排序由于待排序的数据很大,内存无法容纳全部的排序记录,在排序过程中需要访问外存,多次进行内外存的交换。
当n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序序。
直接插入排序
思路:先将序列的第1个元素看成是一个有序的子序列,然后从第2个元素逐个进行插入直到最后一个元素,直至整个序列有序为止。
插入过程中,如果碰见一个和插入元素相等的,那么待插入的元素放在相等元素的后面。所以,两个相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
private static void insertSort2(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int current = arr[i];
int j = i;
while (j > 0 && current < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = current;
}
}
复杂度分析:
如果待排序序列原本就是有序的,则时间复杂度为
O(n)
,也是插排的最好情况;最坏情况下
O(n2)
插入排序适合对小数据进行排序,jdk中有应用。
其他的插入排序有二分插入排序,2-路插入排序。
希尔排序
冒泡排序
属于交换排序。
冒泡排序的思路是每一轮都从最后一个元素开始不断与上一个相邻元素比较,若小就向上冒(交换位置)。
程序这里进行了简单的优化,针对场景是诸如{3,2,5,6,7,8}这样的特殊序列,经过1轮冒泡以后序列已经是有序状态,不再有交换操作,也就没必要再进行下一轮的比较,直接输出结果即可,这里flag标志位用来标志某轮冒泡是否存在交换交换操作。
private static void bubbleSort(int[] arr) {
System.out.println("原始数组:" + Arrays.toString(arr));
boolean flag = true;
for (int i = 0; i < arr.length && flag; i++) {
flag = false;
for (int j = arr.length - 1; j > i; j--) {
if (arr[j] < arr[j-1]) {
swap(arr, j - 1, j);
flag = true;
}
}
System.out.println("第" + i + "轮冒泡排序:" + Arrays.toString(arr));
}
System.out.println("优化版冒泡排序完成");
}
private static void swap(int[] arr, int m, int n) {
int temp = arr[m];
arr[m] = arr[n];
arr[n] = temp;
}
复杂度分析:
- 最优时间复杂度: O(n) ,对应数组已经基本有序的情形;
- 最差时间复杂度: O(n2) ,对应数组是逆序的情形;
- 平均时间复杂度: O(n2) 。
简单选择排序
思路:第i次操作就是:通过n-i次关键字间的比较,选出最小的元素并与i进行交换。
public static void selectionSort(int[] arr){
System.out.println(" 原数组:"+Arrays.toString(arr));
for (int i = 0; i <arr.length ; i++) {
int min = i;
for (int j = i+1; j < arr.length ; j++) {
if(arr[j] < arr[min]){
min = j;
}
}
if(arr[min] < arr[i]) swap(arr,i,min);
System.out.println("第"+i+"次选择排序:"+Arrays.toString(arr));
}
System.out.println("简单选择排序完成");
}
复杂度分析:
- 最好时间复杂度和最差时间比较次数都是 n(n−1))2 ,最好的时候交换次数为0,最差的时候交换次数为n-1,综合时间复杂度为 O(n2) 。
堆排序
归并排序
private static void sort(int[] data, int left, int right) {
if (left >= right)
return;
int center = (left + right) / 2; // 找出中间索引
sort(data, left, center); // 对左边数组进行递归
sort(data, center + 1, right); // 对右边数组进行递归
merge(data, left, center, right); // 合并
print(data);
}
/**
* 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序
*
* @param data 数组对象
* @param left 左数组的第一个元素的索引
* @param center 左数组的最后一个元素的索引,center+1是右数组第一个元素的索引
* @param right 右数组最后一个元素的索引
*/
public static void merge(int[] data, int left, int center, int right) {
int[] tmpArr = new int[data.length]; // 临时数组
int mid = center + 1; // 右数组第一个元素索引
int third = left; // third 记录临时数组的索引
int tmp = left; // 缓存左数组第一个元素的索引
while (left <= center && mid <= right) {
if (data[left] <= data[mid]) { // 从两个数组中取出最小的放入临时数组
tmpArr[third++] = data[left++];
} else {
tmpArr[third++] = data[mid++];
}
}
// 剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
while (mid <= right) {
tmpArr[third++] = data[mid++];
}
while (left <= center) {
tmpArr[third++] = data[left++];
}
// 将临时数组中的内容拷贝回原数组中
// (原left-right范围的内容被复制回原数组)
while (tmp <= right) {
data[tmp] = tmpArr[tmp++];
}
}
快速排序
是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
快排的优点:
- 可以实现原址排序,不占用额外空间
- 平均性能好,且常数因子很小
private static void quickSort(int arr[], int left, int right) {
if (left < right) {
int pivot = partition(arr, left, right); //得到主元元素pivot的位置下标
quickSort(arr, left, pivot - 1); //递归排序主元左边的元素
quickSort(arr, pivot + 1, right); //递归排序主元右边的元素
}
}
private static int partition(int[] n, int left, int right) {
int pivot = n[left];
while (left < right) {
while (left < right && n[right] >= pivot) {
right--;
}
if (left < right) {
n[left++] = n[right];
}
while (left < right && n[left] <= pivot) {
left++;
}
if (left < right) {
n[right--] = n[left];
}
}
n[left] = pivot;
return left;
}
private static void swap(int[] arr, int m, int n) {
int temp = arr[m];
arr[m] = arr[n];
arr[n] = temp;
}
基本思路:
选择一个主元(pivot,主元的选取方式影响算法的性能),使得主元左边的元素都比主元小,右边的元素都比主元大,编程时如何实现呢?选择两个指针,lo指针指向除主元外第一个元素,hi指针指向最后一个元素,这两个指针在函数中以参数的形式给出。
重点理解分区(Partition)的过程:
快排时间复杂度推导,都是中学很基本的推导公式了,基础还是要掌握的。。
排序稳定的意义
稳定意思是说原本键值一样的元素排序后相对位置不变。
我们学习的时候,程序里面要排序的元素都是简单类型,实际上真正使用的时候,可能是对一个复杂类型的数组排序,而排序的键实际上只是这个元素中的一个属性,对于一个简单类型,数字值就是其全部意义,即使交换了也看不出什么不同,但是对于复杂的类型,交换的话可能就会使原本不应该交换的元素交换了。
比如,一个“学生”数组,按照年龄排序,“学生”这个对象不仅含有“年龄”,还有其他很多属性,稳定的排序会保证比较时,如果两个学生年龄相同,一定不交换。
排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,第一个键排序的结果可以为第二个键排序所用。基数排序就是这样,先按低位排序,逐次按高位排序,低位相同的元素其顺序再高位也相同时是不会改变的。
参考链接:
http://blog.jobbole.com/103456/