目前在面试阶段,所以当前版本是面向面试的版本(即,不会谈不太常考的排序)
先来个结论:
选择排序、快速排序、希尔排序、堆排序不是稳定的排序算法,而冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。
再说个注意事项:
排序算法中交换两个元素的时候能用temp尽量用temp。如果非要用异或的话,一定一定一定要对两个交换的元素的下标进行判断,如果是相同下标直接return!
稳定排序着重谈一谈 冒泡、插入、归并
不稳定排序主要看一下 选择、快速
讨论的角度主要为:
- 为何稳定或不稳定
- 时间空间复杂度
- 最好最差情况
- 适用场景
- 代码实现
#稳定排序
归并
为什么把稳定排序放在前面,并且有把归并放在稳定排序的第一个呢。
一方面,稳定排序被问的比较多,在不追求速度的情况下优先考虑稳定排序
另一方面,归并又是稳定排序里面常考的最难的排序。。。
归并排序是什么?
将一个长度为n的数组彻底打散成长度为1的n个小数组,然后相邻的数组合并为有序的数组,然后再排序合并… 直到整个数组合并且排序完成。
(图片来自别人的文章)
这里面我们可以将这些步骤拆分成不同的方法块。
- 将数组分割成子数组的方法
- 管理所有子数组的方法
- 将相邻子数组排序并合并的方法
为何稳定?
我只知道它稳定咯…
时间复杂度 O(nlgn)
空间复杂度 O(n) 在排序的时候,每一对相邻数组都要制造大小相同的数组以供复制。
最好情况: O(nlgn)
最差情况: O(nlgn)
适用场景
归并有一个快排没有的优点,就是归并排序是稳定的。你对于整型数浮点数,稳定不稳定大概看不出来,对于对象么,呵呵。
代码实现
非递归实现
public class MergeSort {
public static void main(String[] args) {
int[] array = {
9, 1, 5, 3, 4, 2, 6, 8, 7
};
MergeSort x = new MergeSort();
System.out.print("排序前:\t");
x.print(array);
x.mergeSort(array);
System.out.print("排序后:\t");
x.print(array);
}
//总方法
public void mergeSort(int[] arr){
for(int gap=1;gap<arr.length;gap=gap*2){ //粒度从1开始合并
mergeArrays(arr,gap,arr.length);
System.out.print("gap="+gap+":\t");
print(arr);
}
}
//不断的合并所有子数组的方法
public void mergeArrays(int [] arr,int gap,int length){
int i=0; //整个方法共享
for(i=0;i+2*gap-1<length;i=i+2*gap){
merge(arr,i,i+gap-1,i+2*gap-1); //把相邻两段长度均为gap的排序好的数组合并
}
//如果剩下一个数组不够gap
if(i+gap-1<length){
merge(arr,i,i+gap-1,length-1);
}
}
//将lo~mid mid+1~hi 这两段排序 然后重新赋值进arr
public void merge(int [] arr, int lo, int mid, int hi){
int i=lo; //第一段数组下标
int j=mid+1; //第二段数组下标
int k=0; //临时数组下标
int [] arrayTemp = new int[hi-lo+1]; //临时数组的长度
while(i<=mid&&j<=hi){
if(arr[i]<=arr[j]){
arrayTemp[k++] = arr[i++];
}else{
arrayTemp[k++] = arr[j++];
}
}
//下面两个while只有可能进入一个
while(i<=mid){ //后半段用完的情况
arrayTemp[k++]=arr[i++];
}
while(j<=hi){ //前半段用完的情况
arrayTemp[k++]=arr[j++];
}
//!!!!! 最后要把这个临时数组的值复制到数组中
for(int x=0;x<arrayTemp.length;x++){
arr[lo++] = arrayTemp[x];
}
}
public void print(int[] list){
for(int i: list){
System.out.print(i+"\t");
}
System.out.println();
}
}
递归实现
public void mergeSort(int[] a, int low, int high) {
MergeSort tool = new MergeSort(); //调用上面刚才实现好的合并数组的方法
int mid = low+(high-low)/2; //试问为什么不用 mid = (low+high)/2
if (low < high) {
// 右边
mergeSort(a, mid + 1, high); //先排左侧还是先排右侧并无影响
// 左边
mergeSort(a, low, mid);
// 左右归并
tool.merge(a, low, mid, high);
System.out.println(Arrays.toString(a));
}
}
插入
插入排序每次都将未排序的元素插入到左侧排序完成的数组中的合适位置。
要点是:
- 左侧为排序完成的数列(默认从小到大)
- 右侧每次只拿一个元素插入到左侧
- 关键词: 平移数组,交换元素
为何稳定?
每次只插入一个数,如果有两个相同的数,那么这两个数也是分两次插入且相等的元素会默认插入到已有元素的右侧,相对顺序并未发生改变。
时间复杂度 O(n^2)
空间复杂度 O(1)
最好情况: 所有元素全部排好序,即代码不走内循环只有单层的循环。故时间复杂度O(n)
最差情况: 所有元素倒序,O(n^2)
适用场景
插入排序是对已经存在的一个有序的数据序列,在这个已经排好的数据序列中插入一个数,这个时候用插入排序比较好
代码实现
public static void insertSort(int [] arr){
int location = 0;
for(int i=1;i<arr.length;i++){ //数组大小小于2则不进行任何操作
//标的, 标的左侧为排序好的内容。
location = i;
int target = arr[i]; //将标的存储起来
for(int j=i-1;j>=0;j--){
if(target>=arr[j])
break; //这个地方算是我个人的改进
else{
//如果 i位置的元素小于j位置的,记录i位置的元素该插入的位置location,同时将数组向后平移
arr[j+1]=arr[j];
location = j;
}
} //内层循环确定了location的位置,然后外层循环将存储的值赋给location位置
arr[location] = target;
}
}
优化了一下。 当target元素已经大于等于左侧第一个元素的时候,其实内层循环就应该直接跳出了,没有必要继续比较。
或者这样写
public void sortArr(int[] arr) { //这个和上面的方法没有区别
for(int i=1;i<arr.length;i++){
int j = i-1;
int temp = arr[i];
while(j>=0&&temp<arr[j]){ //如果arr[i] 大于等于某个前半部分的值 此次插入结束
arr[j+1] = arr[j];
j--;
}
arr[j+1]=temp;
}
}
冒泡
评价: 最广为人知的排序算法
冒泡排序:
每轮排序都将数组中最大的数冒泡到最末尾,然后在下一轮忽略它。
要点:
相邻元素比大小,然后交换
为何稳定?
每次相邻两个元素比较,相等的不会交换位置。那么两个大小相同的元素最终会相邻,然后永远不发生相对位置(这个特性说不定可以利用)的改变。故稳定
时间复杂度 O(n^2)
空间复杂度 O(1)
最好情况: 所有元素全部排好序,时间复杂度O(n)
最差情况: 所有元素倒序,O(n^2)
适用场景
冒泡排序是一种用时间换空间的排序方法,n小的时候适用。
代码实现:
public static void bubble(int [] arr){
for(int i=0;i<arr.length-1;i++){
for(int j=0;j<arr.length-1-i;j++){
if(arr[j]>arr[j+1]){ //这个地方可以放心大胆的使用异或,因为j和j+1永远不可能相等
arr[j+1] = arr[j]^arr[j+1];
arr[j] = arr[j]^arr[j+1];
arr[j+1] = arr[j]^arr[j+1];
}
}
}
}
#不稳定排序
快速
具体的参考百度百科吧,我这里是简略的
快速排序的特点就是快。。。
大致的思路是两个游标i,j, i在数组头部,j在数组尾部。每次选一个基准元素记录下来(比如第一个),j从数组末尾开始往前,直至遇到比基准元素小的停下来交换i的元素,然后i从数组开头往后遇到比基准元素大的停下来和j的元素交换。。。 直至基准元素的左侧均为小于基准元素,右侧均为大于基准元素。然后左右两侧递归进行操作。。。
快速排序探讨起来比较复杂,这里的探讨是简略的。
详见
为何不稳定?
基准元素在和别的交换的时候会打乱左右的顺序。
时间复杂度: O(nlogn)
空间复杂度: O(nlogn)~O(n) 不稳定
最好情况: O(nlogn) 最好的情况是每次都能均匀的划分序列,O(nlog2n)
最坏情况: O(n^2) 划分之后一边是一个,一边是n-1个,这种极端情况的时间复杂度就是O(n^2)
适用场景
(1)n大时好,快速排序比较占用内存,内存随n的增大而增大,但却是效率高不稳定的排序算法。
(2)快速排序空间复杂度只是在通常情况下才为O(log2n),如果是最坏情况的话,很显然就要O(n)的空间了。当然,可以通过随机化选择pivot来将空间复杂度降低到O(log2n)。
private static int partition(int []array,int lo,int hi){
//固定的切分方式
int key=array[lo];
while(lo<hi){
while(array[hi]>=key&&hi>lo){//从后半部分向前扫描 直到找到右侧小于基准数停止
hi--;
}
array[lo]=array[hi];
while(array[lo]<=key&&hi>lo){//从前半部分向后扫描
lo++;
}
array[hi]=array[lo];
}
array[hi]=key;
return hi;
}
private static void sort(int[] array,int lo ,int hi){
print(array); //打印的,这里不用在意
if(lo>=hi){
return ;
}
int index=partition(array,lo,hi);
sort(array,lo,index-1);
sort(array,index+1,hi);
}
public static void quickSort(int[] arr) {
sort(arr,0,arr.length-1); //调用方式类似 Arrays.sort
}
选择
选择排序选择的是每个位置的最小元素。 从第一个位置开始选,每轮选出后面最小的元素放在该位置上,并在下一轮选择下一个位置的合适元素,一直选到倒数第二个位置为止
要点:
- 每轮只是比较和记录下标,只在最后一步操作才产生元素交换的操作(交换操作少, 为n-1次)
为何不稳定?
在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么 交换后稳定性就被破坏了。
比较拗口,举个例子,序列5 8 5 2 9, 我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,
所以选择排序是一个不稳定的排序算法。
时间复杂度 平均,最好,最差均为O(n^2)
空间复杂度 O(1)
适用场景
原始序列不一定有序且N比较小又不要求稳定,这种情况下用选择排序比较好。
交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。
这个是我自己想的: 如果待排序的内容过长,而实际可能只取排序的前段的话可以考虑选择排序,先排序出前面一部分拿来用…
代码实现
public void selectionSort(int[] arr) {
int location = 0; // 尽量不要在for循环中初始化变量
int temp = 0;
for (int i = 0; i < arr.length - 1; i++) { // 第一个位置~倒数第二个位置的选址 一共n-1轮
location = i;
for (int j = i + 1; j < arr.length; j++) { // 每一轮记录选择最小元素的地址
if (arr[j] < arr[location]) { // 与记录的最小值进行比较,并视情况更新最小值的下标
location = j;
}
}
temp = arr[i];
arr[i] = arr[location];
arr[location] = temp;
}
}