目录
一、什么是稳定性
二、七大排序
2.1基于选择的思想
2.1.1直接选择排序
2.1.2堆排序
2.2基于插入的思想
2.2.1直接插入排序
2.2.2希尔排序
2.3归并排序
2.4基于交换的思想
2.4.1冒泡排序
2.4.2快速排序
三、外部排序
排序就是将一组无序的数据经过一定的算法调整为有序的数据。
七大排序中只有直接插入排序,冒泡排序,归并排序时稳定的。
待排序的序列中,若存在值相等的元素,经过排序之后,相等元素之间原有的顺序保持不变,这种特性称为排序算法的稳定性。
在进行多轮排序(依照的属性不同)时,后面的排序不对前面的排序产生影响,我们就优先使用稳定性的排序算法。
若都是对同一属性的排序,则稳定性没有什么影响。
以下七种排序都是基于比较的排序(内部排序(数据都放在内存中))。
每次从无序区间中选择最小或者最大值,放在无序区间的最开始或最后面,直到整个数组有序。
时间复杂度为O(n^2)。不管数组有序无序正序倒序两个循环都要走到头。空间复杂度为O(1),并且不稳定。
在整个无序区间中选择最小值放在无序区间的最开始,每次都将当前区间的最小值放在数组最开始的位置,每次选一个元素,这个元素就放在了正确的位置上。
有序区间[0..i],无序区间[i..n),外层循环表示需要走的内层循环趟数,每走一次内层循环,就有一个元素(当前最小值)放在正确位置然后内层循环每次选取当前无序区间的最小值放在无序区间的最开始位置,用min保存最小值的索引,每次都假设是无序区间第一个元素,如果array中对应索引为 j对应的元素比min小,更新min的指向 min = j。 此时min就保存了最小值索引,交换array中对应索引为i和min的元素即可。
public static void selectSort(int[] array){
for(int i = 0;iarray[j]){
min = j;
}
}
int tmp = array[min];
array[min] = array[i];
array[i] = tmp;
}
}
public static void main(String[] args) {
int[] arr = new int[] {1,5,3,2,4,10,7,9,8,30,25,26,21,15};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
每次从最大堆中选取最大值放在无序区间的末尾,直到整个数组有序。
先将任意数组调整为最大堆,然后不断交换array[0]和当前为排序的最后一个元素array[i],每次交换都能将未排序区间的最大值放在最终位置,外层每循环一次,就能将当前未排序数组的最大值放在正确的位置,每次进行一趟循环,就有一个元素放在了最终位置,有序区间+1,当i==0,时,未排序区间[0]已排序区间[1...n-1] => 整个数组已经有序。
public static void heapSort(int[] arr) {
for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
siftDown(arr,i,arr.length);
}
for (int i = arr.length - 1; i > 0; i--) {
swap(arr,0,i);
siftDown(arr,0,i);
}
}
private static void siftDown(int[] arr, int k, int length) {
while (2 * k + 1 < length) {
int j = 2 * k + 1;
if (j + 1 < length && arr[j + 1] > arr[j]) {
j = j + 1;
}
if (arr[k] >= arr[j]) {
break;
}else {
swap(arr,k,j);
k = j;
}
}
}
堆为完全二叉树,每次调整的时间最坏为logn,所以其时间复杂度最坏为nlogn,时间复杂度为O(nlogn)。空间复杂度O(1),也是不稳定的(下沉过程中没法保证相等元素顺序不交换)。
灵感来源于打扑克牌,每次在无序曲建忠选择第一个元素,插入在有序区间的合适位置,直到整个数组有序。
无序区间是[i...n),有序区间是[0..i) , i就是有序区间和无序区间的分水岭,i之前的元素全部有序,i之后(包括i)是无序区间。只有当arr[j] < arr[j - 1]才进行元素交换,若arr[j] >= arr[j - 1] ,说明j已经放在正确位置,则跳出循环。
public static void insertSort(int[] array){
for (int i = 1; i < array.length; i++) {
for (int j = i; j >= 1 && array[j]
时间复杂度为O(n^2)。但是当最好的情况时,内层循环只是做了简单的元素对比且只对比一次然后全部直接退出,那么时间复杂度为O(n)。因此插入排序经常作为其他高阶排序的辅助手段。最坏情况,就是内外层都全部执行O(n^2)。空间复杂度还是O(1)。是一个稳定的排序算法。
插入排序就是在近乎有序的数组上性能非常好或者在小数据规模的集合中性能也很好,希尔排序就是针对于直接插入排序的优化。所以希尔排序的思路就是先将数组调整的近乎有序,最终在整个近乎有序的数组上来一次插入排序。
具体操作:先选定一个整数(gap),将待排序的集合中按照gap分组(所有距离为gap的元素在同一组),对同一组的元素进行排序,不断缩小这个gap的长度(gap/=2,gap/=3),重复上述的分组与排序过程,当gap==1,时整个数组已经近乎有序,最终来以此全数组的插入排序,整个集合就有序了。
当gap > 1都是分组,组内进行插入排序,当gap == 1时,其实整个数组近乎有序,整体来一次插入排序即可。组内进行的插入排序就相当于将插入排序中的1改为gap。
public static void shellSort(int[] array){
int gap = array.length/2;
while(gap>1){
insertSortByGap(array,gap);
gap = gap>>1;
}
insertSortByGap(array,1);
}
public static void insertSortByGap(int[] array,int gap){
for (int i = gap; i < array.length; i++) {
for (int j = i; j >= gap && array[j] < array[j - gap]; j-=gap) {
int tmp = array[j];
array[j] = array[j-gap];
array[j-gap] = tmp;
}
}
}
一次一次按照gap分组,第一个gap = 7,分成七组,两两一组通过选择排序组内比较。然后第二次分组再比较,最后gap= 1,此刻已经近乎有序了,整体进行选择排序。
时间复杂度为O(n^(1.2-1.3)),具体看gap分组的长度。
将待排序的数组分为如下两个阶段。
归而为一:阶段一,首先不断将数组一分为二,知道每个子数组一分为二,直到每个子数组只剩下一个元素,拆分阶段结束(此时每个子数组都是有序数组)。
并而为整:阶段二,不断地将相邻的两个有序子数组,合并为一个大的有序数组,知道合并为完整的数组,此时整个数组有序。
首先要创建一个方法mergeSortInternal用于递归分组,在这个方法中当l>=r时,则只有一个元素或者没有元素则不需要再进行递归直接返回即可,然后定义变量int mid = l + ((r - l) >> 1),存储中间值。然后先进行左半数组l-mid中继续进行归并排序,再在右半数组mid+1-r进行归并排序,将每个小数组都有序之后再将左右两个有序子数组进行排序调用merge方法。
在这个方法中有两个优化,1、首先是在方法最后调用merge方法这里的优化,只有当两个子数组部分元素还存在乱序,才需要合并。2、在小数组区间上使用插入排序进行优化,减少了很多小数组的递归合并过程,小数组即l -r <= 15的情况下(是经过实验的)。
归并排序的核心在于merge函数(合并过程)。
在merge方法中设置两个变量int i = l,int j = mid+1。(r为数组最后一个元素下标,l为数组起点下标)先创一个大小为r - l +1 的临时数组aux,将两个子数组的内容复制到aux,进行两个子数组的比较合并过程直到两个子数组完成。
情况1:i > mid说明子数组1的所有元素已经处理完毕。直接将子数组2的剩余元素覆盖即可。
情况2:j > r说明子数组2的所有元素处理完毕,将子数组1的剩余元素进行覆盖。
剩余的情况就进行两个子数组的元素内容大小比较,如果aux[i-1] <= [j-1]选择子数组1的元素进行覆盖,保证了相等元素的稳定性。
public static void mergeSort(int[] array){
mergeSortInternal(array,0,array.length-1);
}
private static void merge(int[] arr, int l, int mid, int r) {
int[] aux = new int[r - l + 1];
System.arraycopy(arr, l, aux, 0, (r - l + 1));
int i = l,j = mid+1;
for (int k = l; k <= r; k++) {
if(i > mid){
arr[k] = aux[j - l];
j ++;
}else if (j > r) {
arr[k] = aux[i - l];
i ++;
}else if (aux[i - l] <= aux[j - l]) {
arr[k] = aux[i - l];
i ++;
}else {
arr[k] = aux[j - l];
j ++;
}
}
}
private static void mergeSortInternal(int[] array, int l, int r) {
if (r - l <= 15) {
insertionSortPart(array,l,r);
return;
}
int mid = l + ((r - l) >> 1);
mergeSortInternal(array,l,mid);
mergeSortInternal(array,mid+1,r);
if(array[mid]> array[mid+1]){
merge(array,l,mid,r);
}
}
private static void insertionSortPart(int[] arr, int l, int r) {
for (int i = l + 1; i <= r; i++) {
for (int j = i; j > l && arr[j] < arr[j - 1]; j--) {
swap(arr,j,j - 1);
}
}
}
不断将原数组拆分递归的过程本质就是一颗二叉树, 递归的次数其实就是树的高度,递归的时间复杂度就是O(logn),merge方法的时间复杂度就是O(n)。创建了临时数组,空间复杂度为O(n)。
所以归并排序的时间复杂度为O(nlogn),并且是稳定的。
冒泡排序是从头开始两个两个元素之间进行比较,若前面的元素大于后面的元素的值,那么两者交换,这样最大的元素就会移动到最后面,然后重复上述步骤为了快速则忽略掉已经排好序的后面元素。
private static void bubbleSort(int[] num) {
for(int i = 0;i num[j + 1]) {
int tmp = num[j];
num[j] = num[j + 1];
num[j + 1] = tmp;
}
}
}
}
时间复杂度为O(n^2)。是稳定的排序方法。
现在无序数组中选取一个分区点(数组中的一个元素),扫描整个集合,将比当前分区点小的元素放在分区点数值的左侧,比分区点大的元素放在分区点的右侧,那么分区点数值就放在了正确的位置。=》重复上述过程继续在左半区间和右半区间进行快速排序。
快速排序的核心在于分区函数的实现:针对分区函数的实现有N种操作。
(1)Hoare方法
默认第一个元素或最后一个元素为分区点,其中i索引指向开始位置,j指向终止为止,先让j从后向前扫描,碰到第一个
(2)挖坑法=》针对Hoare方法的改进
默认将第一个位置值作为分取值,即将他取出,其中i索引指向开始位置,j指向终止为止,先让j从后向前扫描,碰到第一个
public static void quickSort(int[] arr) {
quickSortInternalByHell(arr,0,arr.length - 1);
}
public static void quickSortInternalByHell(int[] arr,int l,int r){
if(l>=r){
return ;
}
int pivot = partitionByHell(arr,l,r);
quickSortInternalByHell(arr,l,pivot-1);
quickSortInternalByHell(arr,l +1 ,r);
}
private static int partitionByHell(int[] arr, int l, int r) {
int v = arr[l];
int i = l;
int j = r;
while (i < j) {
while (i=v){
j--;
}
arr[i] = arr[j];
while (i
正常情况为nlogn,情况最坏为n^2。时间复杂度为O(nlogn)。
如果是递归算法,所递归的深度大概为二叉树的深度,即logn。如果是非递归算法,需要模拟递归的过程,即需要保存子区间的索引,每次都会成对的保存,最多保存的索引也和二叉树的高度有关:2 * logn所以空间复杂度为O(logn)。
但是快速排序在近乎有序的数组中排序时间会很慢,第一种为三数取中法,选一个最左侧元素、中间位置元素,最右侧元素,选择值在中间的作为分区点。第二种就是解决方法就是随机生成一个区间数。
大多需要借助外存。时间复杂度近乎于O(n)。
对于数据的特点要求非常高,应用场景很有限,只能那个在特定的数据集上使用。
(1)桶排序,一个省的中考或高考成绩就可以用桶排序,学生的成绩区间是固定的,因此就可以按照分数段将原先的数据先划分为若干个桶。
(2)计数排序
(3)基数排序