说明: 只要代码中没有复杂的循环条件,无论代码的函数是多少,一律为常数阶 O ( 1 ) O(1) O(1)
int i=1;
int j=3;
int m=0;
m=i+j;
....
说明: 存在循环体,在不考虑循环体的代码执行情况,该循环体本该执行n次,但是循环体将改变循环变量i的大小,每执行一次,i就扩到两倍,即i=i*2,这样就导致循环体会加速趋近终止条件n;假设经过x次后,退出循环体,则有 2 x > = n 2^x>=n 2x>=n,因此, x = l o g 2 n x=log_2n x=log2n;同理,当 i = i ∗ 3 时 , x = l o g 3 n i=i*3时,x=log_3n i=i∗3时,x=log3n,退出循环的速度更快,时间复杂度更小。
while(i<n){
i=i*2;
}
说明: 存在循环体。循环体内代码执行的次数随着规模n的变化而变化,并且是线性变化
for(int i=0;i<n;i++){
int j=o;
j++;
}
说明: 将时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n)的代码重复执行n次,即为线性对数阶 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
//n为一个固定的常数
//i和j均为循环变量
while(i<n){//线性节阶
while(j<n){//对数阶
j=j*2;
}
}
说明: 复杂度为 O ( n ) O(n) 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 2 ) O(n^2) O(n2)的代码执行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 ( l o g 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n k ) O(1)
程序猿必备的排序算法有:
基本原理: 将待排序的数组从左到右(下标从小到大), 依次选取元素,并和其相邻的元素比较大小,如果逆序,则交换两个元素的位置,每进行一轮,数组较大的元素会移动到数组的尾部,如同水中泡泡逐渐上浮一样。重重复进行多轮操作,直到数组有序为止。
图解案例
以3 9 -1 10 20 为例进行讲解
冒泡排序的几个关键点
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) O(n),故冒泡排序算法的复杂度为 O ( n 2 ) O(n^2) O(n2)
基本原理:快速排序属于交换排序,从数组中一个元素作为基准元素(首元素或者中间元素),根据基准元素将数组分成两个部分,前半部分数组的元素均小于该基准值,后半部分元素均大于该基准值;然后使用递归的思想,对这两个部分使用同样的方法进行快速排序。
图解案例
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 ( n l o n 2 n ) O(nlon^2n) O(nlon2n),
基本原理: 从array[0]~array[n-1]中选择一个最小的元素与array[0]交换位置,此时array[0]为最小的元素;然后从array[1]-array[n-1]中选取最小的元素与array[1]交换位置;选择的范围由全局所有变成0;所以需要经过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 ) O(n^2) O(n2),原因很简单:两个嵌套的for循环均为 O ( n ) O(n) O(n),注意: O ( n − 1 ) = O ( n ) O(n-1)=O(n) O(n−1)=O(n)
基本原理: 将待排序数组array[0]~array[n-1]从逻辑上分为有序数组和无序数组,初始条件:(array[0])为有序数组,(array[1],…array[n-1])为无序数组;则插入排序的基本过程是:逐一将无序数组中的元素插入到有序数组中去,每完成一次插入操作,有序数组中的元素加一,无序数组中的元素减一,直到所有的无序数组变成有序数组。
图解案例
插入排序的几个关键点
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 ) O(n^2) O(n2)
基本原理: 将待排序的数组按照某个预定的增量gap分组,对每个分组都采用直接插入排序的方式进行排序;随后逐步按照某个策略缩小gap(如:gap=gap/2)并进行分组,使用直接插入排序算法分别对这些数组进行排序,当gap=1时,只有一个分组,使用希尔排序完成最后的一次排序。
图解案例
希尔排序的关键步骤
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循环为分组功能,其时间复杂度为: l o g 2 n log_2n log2n,内部希尔排序最差为 O ( n 2 ) O(n^2) O(n2)。
**基本原理:**采用分治思想和递归思想。归并排序主要分为两大过程:分解和合并过程,分解是将数组二分至只有一个元素为止。合并的过程中同时进行排序,两边元素逐一比较,较小的元素填充到缓冲数组中。另外归并排序需要额外的缓存数组。
图解案例
分解过程使用了递归思想。
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循环为分组功能,其时间复杂度为: l o g 2 n log_2n log2n,内部希尔排序最差为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
图解
直接看图解,语言难于生动的描述此过程
基本步骤:
(1)初始化1个二维数组10*array.length;表示10个桶,标号为:0-9,每个桶(用一个数组表示)最多容纳array.length个元素。
(2)获得待排序数组中最大元素的最高位,依次按个位、十位、…最高位进行排序。
(3)第一轮,按个位进行选取元素,将个位放进对应的桶编号中,当所有的元素均放进桶中后,按顺序读出每个桶中的元素,读取到的序列作为该轮排序的结果。再以该轮排序的结果作为输入条件,依照十位进行排序;最后直到所有的位数都完成。
编码过程中的几个关键点
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;
}
}
}
}
堆排序的算法步骤:
1.根据排序的顺序和逆序来构建大顶堆或者小顶堆
2.交换堆顶元素和最后一个叶子结点,即:arr[0]和arr[arr.length-1];
3.交换后,调整堆直至符合定义;
代码参考如下
public class HeapSort {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
String[] str=sc.nextLine().trim().split(" ");
int[] arr=new int[str.length];
for(int i=0;i<str.length;i++){
arr[i]=Integer.parseInt(str[i]);
}
heapSort(arr);
System.out.print(Arrays.toString(arr));
}
//堆排序的过程
//1.构建大顶堆
//2.堆顶的元素与最后一个元素交换,同时,最后一个元素将排除在堆之外,继续循环调整堆。
public static void heapSort(int[] arr){
makeHeap(arr);//构建大顶堆
for(int i=arr.length-1;i>=0;i--){
swap(arr,0,i);//交换堆顶元素和当前堆的最后一个元素
adjustHeap(arr,0,i);
}
}
//交换两个元素的顺序
public static void swap(int[] arr,int a,int b){
if(a>arr.length-1 || b>arr.length-1){
return ;
}
int tmp=arr[a];
arr[a]=arr[b];
arr[b]=tmp;
}
/**
* 调整一颗二叉树
* @param arr BST
* @param i 以i为根节点开始调整
* @param len 二叉树中参与调整的结点数
*/
public static void adjustHeap(int[] arr,int i,int len){
int k=2*i+1;//以i为结点的左子结点
while(k<len){
while(k+1<len && arr[k]<arr[k+1]){//找左、右两个子结点的大小,找到大的结点
k++;//右结点大,指向右节点
}
//否则左结点要大,仍然指向左结点
if(arr[i]>arr[k]){
break;//此时,以i为根结点的树为大顶堆,直接跳出循环即可
}
swap(arr,i,k);//否则,交换两个位置的元素,使之成为大顶堆
//由于i和k元素发生的了交换,此时,还需要保证以k为根节点的树仍然是一颗二叉树,仍然使用这个循环
i=k;//以k为根
k=2*k+1;//当前根结点的左子节点;
}
}
//构造一棵二叉堆
//构造过程:从最后一颗结点开始,逐个调整所有的子树,使得他们均符合大顶堆的特点
public static void makeHeap(int[] arr){
for(int i=arr.length/2-1;i>=0;i--){//要从最后一个父节点开始,arr.length/2-1;
adjustHeap(arr, i,arr.length );
}
}
}