程序猿必备排序算法及其时间复杂度分析

经常使用的时间复杂度

常数阶\(O(1)\)

说明: 只要代码中没有复杂的循环条件,不管代码的函数是多少,一概为常数阶\(O(1)\)java

int i=1;
    int j=3;
    int m=0;
    m=i+j;
    ....

对数阶 \(O(log_2n)\)

说明: 存在循环体,在不考虑循环体的代码执行状况,该循环体本该执行n次,可是循环体将改变循环变量i的大小,每执行一次,i就扩到两倍,即i=i*2,这样就致使循环体会加速趋近终止条件n;假设通过x次后,退出循环体,则有\(2^x>=n\),所以,\(x=log_2n\);同理,当\(i=i*3时,x=log_3n\),退出循环的速度更快,时间复杂度更小。git

while(i<n){
        i=i*2;
    }

线性阶\(O(n)\)

说明: 存在循环体。循环体内代码执行的次数随着规模n的变化而变化,而且是线性变化程序员

for(int i=0;i<n;i++){
    int j=o;
    j++;
}

线性对数阶\(O(nlog_2n)\)

说明: 将时间复杂度为\(O(log_2n)\)的代码重复执行n次,即为线性对数阶\(O(nlog_2n)\)算法

//n为一个固定的常数
    //i和j均为循环变量
    while(i<n){//线性节阶
        while(j<n){//对数阶
            j=j*2;
        }
    }

平方阶\(O(n^2)\)

说明: 复杂度为\(O(n)\)的代码执行了n次。数组

for(int i=0;i<n;i++){//执行n次
        for(int j=0;j<n;j++){//执行n次
            int m=0;
            m++;
        }
    }

立方阶O(n^3)

说明 和平方阶原理同样,时间复杂度为\(O(n^2)\)的代码执行n次缓存

for(int i=0;i<n;i++){//执行n次
        for(int j=0;j<n;j++){//执行n次
           for(int k=0;k<n;k++){
               int m=0;
               m++;
           }
        }
    }

综上分析不难发现:时间复杂度的大小关系为:\(O(1)<O(log_2n)<O(n)<O(nlog_2n)<O(n^2)<O(n^k)\)函数

程序员必备排序算法

程序猿必备的排序算法有:优化

  • 插入排序
    • 简单直接插入
    • 希尔排序
  • 选择排序
    • 简单选择排序
    • 堆排序
  • 交换排序
    • 冒泡排序
    • 快速排序
  • 归并排序
  • 基数排序(桶排序)

冒泡排序(Bubble Sort)

基本原理: 将待排序的数组从左到右(下标从小到大), 依次选取元素,并和其相邻的元素比较大小,若是逆序,则交换两个元素的位置,每进行一轮,数组较大的元素会移动到数组的尾部,如同水中泡泡逐渐上浮同样。重重复进行多轮操做,直到数组有序为止。
图解案例
以3 9 -1 10 20 为例进行讲解
冒泡排序
冒泡排序的几个关键点ui

  • 进行arr.length-1趟排序
  • 每一趟排序均会将前面未被排序的最大元素“冒泡”到后半部分
  • 每一趟进行两两元素的比较,n个元素须要比较n-1次便可
  • 因为每一趟的任务都是将前面未经排序的部分中较大的元素“冒泡”到后面,也就是说通过i趟以后,数组的后面的i个元素都是已经排好序的。所以第i趟只须要进行arr.lenth-1-i次比较便可

Java代码实现部分编码

/**
 * 优化的地方,当某一趟中,发现没有进行过任何交换操做,则代表数组已经有序了,此时能够当即结束操做,没必要要进行循环操做。
 * 
 * */
public class BubbleSort{
    public static void bubbleSort(int arr[]){
        int temp = 0;
        boolean flag = false;// 表示是否进行过交换
        for (int i = 0; i < arr.length - 1; i++) {//进行arr.length-1趟排序
            for (int j = 0; j < arr.length - 1 - i; j++) {//每一趟排序都通过arr.length-1-i次两两比较
                if (arr[j] > arr[j + 1]) {
                    // 交换
                    flag = true;
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
            //优化代码
            if (flag == false) {
                // 没有发生过交换,代表是有序的
                break;
            } else {
                flag = false;// 重置flag,进行下次判断
            }
        }
    }
}

时间复杂度分析
很显然,两个嵌套for循环,每一层for循环的时间复杂度均为线性复杂度\(O(n)\),故冒泡排序算法的复杂度为\(O(n^2)\)


快速排序(QuickSort)

基本原理:快速排序属于交换排序,从数组中一个元素做为基准元素(首元素或者中间元素),根据基准元素将数组分红两个部分,前半部分数组的元素均小于该基准值,后半部分元素均大于该基准值;而后使用递归的思想,对这两个部分使用一样的方法进行快速排序。
图解案例

  • 以[4,7,6,5,3,2,8,1]数组为例
    快速排序
    快速排序的几个关键点
  • 选基准元素(首、尾元素或者中间元素)
  • 根据基准元素划分数组,使得基准的前面元素均大于基准,基准后面的元素均小于该值
    Java代码实现
public void QuickSort(int[] array,int left,int right){

        int l=left;
        int r=right;
        int pivot=array[(right+left)/2];//选取中间值为基准值
        while(l<r){
            //从左边开始找,直到找到大于或等于基准元素的位置为止
            while(array[l]<pivot){
                l++;
            }
            //当找到了大于或等于基准元素的位置时,跳到右边开始寻找
            while(array[r]>pivot){
                r++;
            }
            //此时,array[r]<=pivot<=array[l],所以交换顺序
            int tmp=array[r];
            array[r]=array[l];
            array[l]=tmp;
            //注意:当array[r]==pivot或者array[l]==pivot,移动l或者r,否者
            //在这种状况下会进入死循环
            if(array[l]==pivot){
                r--;
            }
            if(array[r]==pivot){
                l++;
            }
        }
        //当l==r时,须要错开l和r,
        if(l==r){
            l++;
            r--;
        }
        if(left<r){
            QuickSort(array,left,r);
        }
        if(right>r){
            QuickSort(array,r,right);
        }
    }

时间复杂度: 显然选择排序的时间复杂度为\(O(nlon^2n)\),


选择排序(Selection Sort)

基本原理: 从array[0]array[n-1]中选择一个最小的元素与array[0]交换位置,此时array[0]为最小的元素;而后从array[1]array[n-1]中选取最小的元素与array[1]交换位置;选择的范围由全局全部变成0;因此须要通过n-1次选择和交换的过程。
图解案例

  • 思路图解:
  • 初始数组:101,34,119,1
  • 第一趟:1,34,119,101
  • 第二趟:1,34,119,101
  • 第三趟:1,34,101,119

选择排序的几个关键点

  • 对于长度为n的元素须要通过n-1次选择的过程,由于最后一个元素不须要选择,它是惟一,固然最小。
  • 在每以选择的时候须要找到该次选择的最小值,随着选择次数的递增,所选择的数组范围逐渐变小

java代码

public void SelectionSort(int[] array){
        
        for(int i=0;i<array.length-1;i++){//通过n-1轮选择
            int minIndex=i;//初始最小值对应的索引
            int min=array[minIndex];//定义一个初始最小值
            for(int j=i;j<array.length;j++){//选择最小值[i,length-1];索引从0开始
                if(array[j]<min){//找出当前选择范围的最小值
                    min=array[j];//更新最小值
                    minIndex=j;//更新最小值对应的索引
                }
            }
            //交换最小值和array[i]的值
            if(minIndex!=i){//不然不必交换
                //交换两个数组元素的标准步骤
                array[minIndex]=array[i];
                array[i]=min;
            }
        }
    }

时间复杂度: 显然选择排序的时间复杂度为\(O(n^2)\),缘由很简单:两个嵌套的for循环均为\(O(n)\),注意:\(O(n-1)=O(n)\)


插入排序

基本原理: 将待排序数组array[0]~array[n-1]从逻辑上分为有序数组和无序数组,初始条件:(array[0])为有序数组,(array[1],..array[n-1])为无序数组;则插入排序的基本过程是:逐一将无序数组中的元素插入到有序数组中去,每完成一次插入操做,有序数组中的元素加一,无序数组中的元素减一,直到全部的无序数组变成有序数组。
图解案例

  • 原始数组:3,1,2,5,4
    • 第一次插入:3 1 2 5 4--->1 3 2 5 4
    • 第二次插入:1 3 2 5 4--->1 2 3 5 4
    • 第三次插入:1 2 3 5 4---->1 2 3 5 4 (实际上没有进行任何插入操做)
    • 第4次操做 1 2 3 5 4 --->1 2 3 4 5

插入排序的几个关键点

  • 从array[1]元素做为有序数组和无序数组的分割点
  • 插入操做的代码实现,首先肯定待插入的元素insertVal,对应的索引index,再肯定须要插入的位置insertIndex=index-1,插入操做为:array[inserIndex]向后移动一位;
public void insertSort(int[] array){
        for(int i=1;i<array.length;i++){//从第一个元素开始
            int insertVal=array[i];//待插入的值为当前索引指向的值(在寻找待插入位置时会覆盖这个值,须要记录下来)
            int insertIndex=i-1;//即将要插入的位置为当前索引的前一个位置
            ////将当前元素与有序数组中的元素逐一比较,找到要插入的位置
            while(insertIndex>=0 && array[insertIndex]>insertVal){
                array[insertIndex+1]=array[insertIndex];//空出待插入的位置出来
                insertIndex--;
            }
            //开始执行插入操做,同时有个优化点,当插入的位置为当前索引i时,不须要插入
            //因为退出while时,insertIndex-1了,故真实的insertIndex=insertIndex+1;
            if(insertIndex!=i){
                array[insertIndex+1]=insertVal;
            }
        }
    }

时间复杂度: 显然选择排序的时间复杂度为\(O(n^2)\)


希尔排序

基本原理: 将待排序的数组按照某个预约的增量gap分组,对每一个分组都采用直接插入排序的方式进行排序;随后逐步按照某个策略缩小gap(如:gap=gap/2)并进行分组,使用直接插入排序算法分别对这些数组进行排序,当gap=1时,只有一个分组,使用希尔排序完成最后的一次排序。
图解案例
希尔排序
希尔排序的关键步骤

  • 分组(gap逐渐缩小)
  • 对每一个分组使用直接插入排序

Java代码实现部分

public void ShellSort(int[] array){
        //初始化gap为数组长度的一半,每一次分组长度缩小一半
        for(int gap=array.lentgh/2;gap>0;gap=gap/2){
            //对当前分组中的全部组元素进行直接插入排序
            for(int i=gap;i<array.length;i++){ 
                int j=i;
                int tmp=array[j];//暂时保存待插入的值
                if(array[j]<array[j-gap]){//优化,其余状况不须要进行插入
                    while(j-gap>=0 && tmp<array[j-gap]){
                        array[j]=array[j-gap];//空出待插入的位置
                        j-=gap;
                    }
                    //退出while循环时,证实找到了合适的插入位置
                    array[j+gap]=tmp;
                }
               
            }
        }
    }

时间复杂度
第一层for循环为分组功能,其时间复杂度为:\(log_2n\),内部希尔排序最差为\(O(n^2)\)


归并排序

基本原理:采用分治思想和递归思想。归并排序主要分为两大过程:分解和合并过程,分解是将数组二分至只有一个元素为止。合并的过程当中同时进行排序,两边元素逐一比较,较小的元素填充到缓冲数组中。另外归并排序须要额外的缓存数组。
图解案例
分解过程使用了递归思想。
递归分解
合并过程为:
合并
java代码

public void mergeSort(int[] array,int left,int right,int[] temp){
	//递归分解
	//那么递归的结束结束条件:left>=right
	if(left>=right){
		return;
	}
	int mid=(left+rightight)/2;
	//向左边递归
	mergeSort(array,int left,mid,temp);
	//向右递归
	mergeSort(array,int mid+1,right,temp);
	merge(array,left,mid,right,temp);
	
}
/**
合并过程
**/
public void merge(int[] array,int left,int mid,int right,int[] temp){
	int i=left;//索引i范围为[left,mid];
	int j=mid+1;//索引j的范围为[mid+1,right];
	int t=0;//tmp额外数组的索引
	while(i<=mid && j<=right){
		if(array[i]<array[j]){
			temp[t]=array[i];
			t++;
			i++;
		}else{
			temp[t]=array[j];
			t++;
			j++;
		}
	}
	//当该循环退出的时候,有两种种状况(不可能同时超出边界,最多同时到达边界,但最后仍是有一个先)
	//超出边界:
	//1.i>mid,右边数组有剩余
	//2.j>right,左边数组有剩余
	if(i>mid){
		while(j<=right){
			temp[t]=array[j];
			t++;
			j++;
		}
	}
	if(j>right){
		while(i<=mid){
			temp[t]=array[i];
			t++;
			i++;
		}
	}
	//将temp数组中的元素复制到array中
	//注意,temp中的有效部分并非从0开始,而是从[left,right]
	//所以只要复制的范围是[left,right];
	t=0;//特别注意
	int tmpLeft=left;
	while(tmpLeft<=right){
		array[tmpLeft]=temp[t];
		t++;
		tmpLeft++;
	}
}

时间复杂度
第一层for循环为分组功能,其时间复杂度为:\(log_2n\),内部希尔排序最差为\(O(nlog_2n)\)


桶排序

图解
直接看图解,语言难于生动的描述此过程
桶排序1
桶排序2
基本步骤:
(1)初始化1个二维数组10*array.length;表示10个桶,标号为:0-9,每一个桶(用一个数组表示)最多容纳array.length个元素。
(2)得到待排序数组中最大元素的最高位,依次按个位、十位、...最高位进行排序。
(3)第一轮,按个位进行选取元素,将个位放进对应的桶编号中,当全部的元素均放进桶中后,按顺序读出每一个桶中的元素,读取到的序列做为该轮排序的结果。再以该轮排序的结果做为输入条件,依照十位进行排序;最后直到全部的位数都完成。
编码过程当中的几个关键点

  • 获取元素的个位、十位、百位:array[i]/1%10 array[i]/10%10 array[i]/100%10
  • 获取某个元素的最多有几位数:(n+"").length(),将整型强制转化为字符串类型,而后使用String的length()方法。
  • 因为每完成一次“装桶”操做,须要从每一个桶中读取序列,每一个桶中个数不一,所以,能够定义一个数组用于记录每一个桶中存放了多少个元素。
  • 每一排序过程当中:桶的编号和位值是一一对应的关系.
    java代码
public class RadixSort {
    public static void radixSort(int[] arr){
        //第一轮:针对每一个元素的个位进行排序
        
        //定义一个2维数组,表示10个桶,每一个桶都是一个一维数组
        //为了防止数据的溢出,则每一个一维数组,大小定为arr.length
        //很明显,基数排序是使用空间换时间的经典排序算法
        int[][] bucket=new int[10][arr.length];
        //每一个桶都须要一个游标指针。
        //所以咱们能够定义一个数组来记录每一个桶的游标
        int[] bucketElementCounts=new int[10];
        //获得数组中最大数的位数
        int max=arr[0];
        for(int i=1;i<arr.length;i++){
            if(arr[i]>max){
                max=arr[i];
            }
        }
        //技巧:获取整数的最大位数
        int maxLength=(max+"").length();
        for(int i=0,n=1;i<maxLength;i++,n*=10){
            for (int j = 0; j < arr.length; j++) {
                // 取出每一个元素的个位、十位、百位。。。
                int digitOfElement = arr[j] / n % 10;
                bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
                bucketElementCounts[digitOfElement]++;
            }
            // 按照桶的顺序依次取出数据,并放入原来的数组
            int index = 0;
            // 遍历每个桶
            for (int k = 0; k < bucketElementCounts.length; k++) {
                // 若是桶中有数据,咱们才放入数据到原数组
                if (bucketElementCounts[k] != 0) {
                    // 取出该个桶中的数据
                    for (int l = 0; l < bucketElementCounts[k]; l++) {
                        // 取出元素放入到原数组中
                        arr[index] = bucket[k][l];
                        index++;
                    }
                }
                // 第一轮处理后,须要将每一个桶的清零
                bucketElementCounts[k] = 0;
            }
        }  
    }
}
相关文章
相关标签/搜索