一、排序算法基本概念
排序算法分为内部排序和外部排序,内部排序把数据记录放在内存中进行排序,而外部排序因排序的数据量大,内存不能一次容纳全部的排序记录,所以在排序过程中需要访问外存。八大基本排序算法都是内部排序。
稳定性: 如果一个排序算法经过排序后相同元素(不同元素排序后位置会发生改变)的相对位置不改变则可以被称为是稳定的。反之,则是不稳定的。
对于每种排序算法,我们需要从以下几个方面入手学习:
- 算法的基本思想
- 算法的代码实现
- 算法的时间复杂度(最好、平均、最坏)
- 算法的空间复杂度
- 算法的稳定性
二、直接插入排序
基本思想:类似于接扑克牌
将数组中的所有元素依次跟前面已经排好的元素相比较,如果选择的元素比已排序的元素小,则插入到该元素之前,直到全部元素都比较过为止。
特点:越有序越快
算法描述:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果新元素小于已排序元素tmp<array[ j ],将该元素移到下一位置array[ j+1]=array[ j ]
- 重复步骤3 j - -,直到找到已排序的元素小于或者等于新元素的位置 array[ j ]<=tmp
- 将新元素tmp插入到该位置 array[ j+1] = tmp
- 重复步骤2~5
代码实现:
public static void insertSort(int[] array){
for(int i=1;i<array.length;i++){
int tmp=array[i];
int j=0;
for( j=i-1;j>=0;j--){
if(tmp<array[j]){
array[j+1]=array[j];//顺序不能改变
}else{
break;
}
}
array[j+1]=tmp;
}
}
算法效率:
平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
- 平均时间复杂度:O(N^2) 两个for循环嵌套
- 最好时间复杂度:O(N) 有序时,j不需要回退,只剩下一个for循环
三、希尔排序
希尔排序又称为缩小增量排序,是插入排序的一种高速而稳定的改进版本。
基本思想:
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
算法描述:
- 第一趟增量序列为5,把待排序列分割为5个的子序列(第一个子序列从第一个元素开始,相隔5个元素,取下一个元素,直到取不够5个间隔的元素为止),其中每个子序列包含2个元素,分别对这5个子序列进行直接插入排序。
- 第二趟增量序列为2,把待排序列分割为2个的子序列(第一个子序列从第一个元素开始,相隔2个元素,取下一个元素,直到取不够2个间隔的元素为止),其中每个子序列包含5个元素,分别对这2个子序列进行直接插入排序。
- 最后一趟增量序列为1,待排序列整体进行直接插入排序,最后一趟数据已经基本有序,效率很高。
注意:
- 每一趟排序中,第n个子序列的第一个元素从待排序列第n个元素开始取
- 每趟排序分割的子序列个数和每个子序列包含的元素个数都取决于待排序列的长度
代码实现:
public static void shell(int[] array,int gap){
for (int i = gap; i < array.length; i++) {
int tmp = array[i];
//从增量为5的第一个子序列的第二个元素开始遍历 array[5]
//从增量为2的第一个子序列的第二个元素开始遍历 array[2]
int j = 0;
for (j = i-gap; j >= 0 ; j-=gap) { //j的初始位置是i-增量
if(array[j] > tmp){
array[j+gap] = array[j];
}else {
break;
}
}
array[j+gap]= tmp;
}
}
public static void shellSort(int[] array) {
int[] drr = {5,3,1};
for (int i = 0; i < drr.length; i++) {
shell(array,drr[i]);
}
}
注意:
第一层for循环中i++而不是i+=gap,每一趟排序中第一个子序列从待排序列的第一个元素开始取,第二个子序列从待排序列的第二个元素开始取,即便写成i+=gap,结果也是正确的,因为最后一趟增量为1,此时相当于直接插入排序而不是shell
第二层for循环中j-=gap,每个子序列进行直接插入排序,j 每次向前找间隔为gap的元素
算法效率:
平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
O(nlogn) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
四、简单选择排序
基本思想:
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
算法描述:
- 从待排序列中第一个元素(i=0)开始,与下一个元素(j=i+1)进行比较,如果比第二个元素小,则交换位置,直到 j 向后走到数组最后一个元素,开始第二趟排序,这样就确定了最小的元素
- 第二趟排序从第二个元素(i=1)开始,与下一个元素(j=i+1)进行比较,如果比第二个元素小,则交换位置,这样就确定了数组中第二小的元素
- 以此类推,每一趟排序可以确定一个元素的位置,即最小的元素,第二小元素,第三小元素.....
注意:j一直向后走,每遇到比array[i] 小的元素,就交换位置,所以array[i]的不断变化,直到 j 走到数组最后一个位置为止,这样就确定了该趟比较的最小元素
代码实现:
public static void selectSort(int[] array){
for(int i=0;i<array.length;i++){
for(int j=i+1;j<array.length;j++){
if(array[i]>array[j]){ //未排序序列中找到最小的元素
int tmp=array[i];
array[i]=array[j];
array[j]=tmp;
}
}
}
}
算法效率:
平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
O(N^2) | O(N^2) | O(N^2) | O(1) | 不稳定 |
- 最坏时间复杂度:O(N^2) 无论是否有序,都必须进行两层for循环进行比较
五、冒泡排序
基本思想:类似于按身高排队
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复访问要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。访问数列的工作是重复地进行直到没有再需要交换的数据,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端,像水中的气泡从水底浮到水面。
算法描述:
- 第一趟从第一个元素array[0]开始,与下一个元素array[1]进行比较,如果array[1]小于array[ 0],则交换位置,然后比较array[1]和array[ 2],如果array[1]小于array[ 2],则交换位置,以此类推,直到比较完全部元素,就确定了最大元素
- 第二趟从第一个元素array[0]开始,与下一个元素array[1]进行比较,依次两组元素进行比较,直到比较完除了最后一个元素的全部元素,这样就确定了第二大元素
- 每一趟比较结束,就依次确定了最大元素,第二大元素,第三大元素...
代码实现:
public static void bubbleSort(int[] array) {
for(int i=0;i<array.length-1;i++){ //趟数 6个元素比较5趟
for (int j = 0; j < array.length-1-i; j++) {//每趟比较的次数 6个元素比较5次
if(array[j]>array[j+1]){
int tmp=array[j];
array[j]=array[j+1];
array[j+1]=tmp;
}
}
}
}
算法效率:
平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
O(N^2) | O(N) | O(N^2) | O(1) | 稳定 |
- 最好时间复杂度:O(N) 数据有序的情况下,不需要交换数据,即内部for循环复杂度为O(1)
六、快速排序
快速排序是对冒泡排序的一种改进,使用了分治的思想。
基本思想:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归 进行,以此达到整个数据变成有序序列。
算法描述:
- 从数列中挑出一个元素,称为"基准"(pivot)。
- 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
代码实现:
//找基准,返回第一趟排序后的基准(low)的位置
public static int partion(int[] array,int low,int high){
int tmp=array[low];
//hign向前走找比tmp小的值
while(low<high){
while(low<high&&array[high]>=tmp){
high--;
}
if(low>=high){
array[low] = tmp;
break;
}else{
array[low] = array[high];
}
//low向后走找比tmp大的值
while(low<high&&array[low]<=tmp){
low++;
}
if(low>=high){ //low与high相遇,即没有比tmp大的值了,此时需把基准放在相遇的位置
array[low] = tmp;
break;
}else{
array[high] = array[low];
}
}
return low;//此时low和high相遇,返回第一趟排序后的基准(low)的位置
}
public static void quick(int[] array,int start,int end) {
int par = partion(array,start,end); //找基准
//递归左边
if(par>start+1){ //至少保证有两个数据
quick(array,start,par-1);
}
//递归右边
if(par<end-1){
quick(array,par+1,end);
}
}
public static void quickSort1( int[] array){
quick(array,0, array.length-1);
}
算法效率:
平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
O(nlogn) | O(nlogn) | O(N^2) | O(logn)~ O(n) | 不稳定 |
最好时间复杂度:当每次划分时,算法若都能分成两个等长的子序列时,分治算法效率达到最大
最坏时间复杂度:待排序列有序时,相当于冒泡排序,递归实现会出现栈溢出的现象,时间复杂度为O(N^2)
最好空间复杂度:每次都把待排序列分为相等的两部分,2^x=n (分割x次,保存x个par) ,x = logn
最坏空间复杂度:1 2 3 4 5 6 7 N个数据就保存N个par
七、归并排序
基本思想:
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
算法描述:
- 把待排序列递归的分为长度相等的两个子序列,直到分解为1个子序列中包含1个元素为止
- 递归的将每两个子序列合并为一个有序子序列
- 最终合并的序列即为有序序列
代码实现:
public static void mergeSort(int[] array,int start,int end,int[] tmpArray){
//递归的结束条件
if(start>=end){
return;
}
int mid=(start+end)/2;
//递归左边
mergeSort(array,start,mid,tmpArray);
//递归右边
mergeSort(array,mid+1,end,tmpArray);
//递归完成之后,分解完成,每组数据包含1个元素
//开始合并
merge(array,start,mid,end,tmpArray);
}
public static void merge(int[] array,int start1,int mid,int end,int[] tmpArray){
//s和s2比较,谁小谁下来,谁小谁往后走
//int[] tmpArray = new int [array.length];//该数组用来放合并后排序好的新序列,空间复杂度
//每次递归都要创建该数组
int tmpIndex = start1;//tmp数组开始的位置 //归并的区间开始位置在哪,就把排好序的元素放在新数组的那个位置
int i =start1;
int start2 = mid+1;//第二个归并段的开始是第一个归并段结束+1
//当有两个归并段的时候才比较
while(start1<=mid &&start2<=end){//判断存在两个归并段
if(array[start1] <=array[start2]){//谁小谁下来,放在新数组中(归并段开始的位置)
tmpArray[tmpIndex++] = array[start1++];//谁小谁往后走(s1++)
}else{
tmpArray[tmpIndex++] = array[start2++];
}
}
while(start1 <=mid){
tmpArray[tmpIndex++] = array[start1++];
}
//第二个归并段有数据,把第二个归并段中剩下的元素全部放在tmpArray中
while(start2 <=end){
tmpArray[tmpIndex++] =array[start2++];
}
//把排好序的数据从tmpArray拷贝到array
// i下标表示第一个归并段开始的位置
while(i<=end){
array[i] = tmpArray[i++];
}
}
算法效率:
平均时间复杂度 | 最好时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |