时间复杂度与简单算法

本文深入探讨了算法的时间复杂度和空间复杂度概念,详细分析了选择排序、冒泡排序和插入排序的实现及复杂度。同时介绍了二分法在解决特定问题中的应用,并通过异或操作解决数组中出现奇数次的数的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

常数时间的操作

如果一个操作与数据量的规模无关,其操作的时间是一定的,称其为常数时间的操作。

时间复杂度

时间复杂度是衡量一个算法中发生了多少常数时间操作的指标,通常用O来表示。若一个算法中的常数项操作的数量为kn^m+pn^2+L,则该算法的时间复杂度为O(n^m),删去其常数项、系数与指数次数不是最高的项。

如果两个算法的时间复杂度相同,还想继续比较两个算法的性能,可以生成大量实验数据直接测验实际运行时间,也就是常数项运行时间。

额外的空间复杂度

除了必要的内存空间(存储输入数据的空间与存储输出数据的空间)外,为了实现算法所花费的空间。

选择排序、冒泡排序与复杂度分析

选择排序

public static void selectionSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		for (int i = 0; i < arr.length - 1; i++) {
			int minIndex = i;
			for (int j = i + 1; j < arr.length; j++) {
				minIndex = arr[j] < arr[minIndex] ? j : minIndex;
			}
			swap(arr, i, minIndex);
		}
	}

	public static void swap(int[] arr, int i, int j) {
		int tmp = arr[i];
		arr[i] = arr[j];
		arr[j] = tmp;
	}

冒泡排序

public static void bubbleSort(int[] arr) {
		if (arr == null || arr.length < 2) {
			return;
		}
		for (int e = arr.length - 1; e > 0; e--) {
			for (int i = 0; i < e; i++) {
				if (arr[i] > arr[i + 1]) {
					swap(arr, i, i + 1);
				}
			}
		}
	}

	public static void swap(int[] arr, int i, int j) {
		arr[i] = arr[i] ^ arr[j];
		arr[j] = arr[i] ^ arr[j];
		arr[i] = arr[i] ^ arr[j];
}

选择排序的时间复杂度的计算过程如下

 n - 1 + n - 2 + n - 3 + ...... + 1 = ((1 + n - 1 ) * ( n - 1)) / 2 , 时间复杂度表示为O(n ^2) ,额外的空间复杂度是常数个,表示为O(1)。

冒泡排序的时间复杂度的计算过程如下

n - 1 + n - 2 + n - 3 + ...... + 1 = (( n - 1 + 1) * ( n - 1)) / 2,时间复杂度表示为O(n ^2) ,额外的空间复杂度是常数个,表示为O(1)。

插入排序与复杂度分析

实现

class Sort{
public:
   static int* insertSort(int* arr,int length){
        int i = 1;
        int j = 0;
        for( ; i < length; i++){
            for(j = i - 1; j >= 0; j--){
                if(arr[j + 1] > arr[j]){
                    swap(arr[j + 1],arr[j]);//swap()函数是c++自带的函数,它的工作原理是交换两个指针的值。
                }
            }
        }
        return (int*)arr;
    }

};

只要数据的数量一定,冒泡排序与选择排序的执行次数就是一定的,而插入排序却不是这样。除了与数据的数量有关外,插入排序的执行次数还与数据的质量有关。如果要求我们返回的是一个按照从小到大的顺序排列的数组,而所给的数据也是从小到大排列的,那么常数时间的操作的数量就是n次;若所给的数据是从大到小给出的,那么常数时间的操作的数量就是((1 + n - 1 ) * ( n - 1)) / 2,与冒泡和选择排序的常数时间的操作的数量相同。

在这种情况下,即常数时间的执行次数与数据的质量有关的情况下,一般按照最坏的情况计算时间复杂度。所以插入排序的时间复杂度为O(n^2).

二分法

现在有3道例题:

1. 在一个有序数组中,查找某数是否存在;

2. 在一个有序数组中,找 >=某个数最左侧的位置;

3. 局部最小值问题(整个数组无序,任意相邻的两个数不相等)。

以上三个问题都可以用二分法来求解。

class BS{
public:
    //1. 在一个有序数组中,查找某数是否存在;
    static bool isBSExit(int* arr,int length,int num){
        int left = 0;
        int right = length - 1;
        int mid = 0;
        for( ; right > left; ){
                //cout << left << "  " << right << endl;
            mid = left + ((right - left) >> 1);
            if(arr[mid] > num){
                right = mid - 1;
            }
            else if(arr[mid] < num){
                left = mid + 1;
            }
            else{
                return true;
            }
        }
        return arr[right] == num;
    }

    //2. 在一个有序数组中,找 >= 某个数最左侧的位置;
    static int getBSLocation(int* arr,int length,int num){
        int left = 0;
        int right = length - 1;
        int mid = 0;
        int location = -1;
        for( ; right > left; ){
            mid = left + ((right - left) >> 1);
            if(arr[mid] > num){
                right = mid - 1;
                location = mid;
            }
            else if(arr[mid] < num){
                left = mid + 1;
            }
        }
        return location;
    }

    //3. 局部最小值问题。
    static int getMinValue(int* arr, int length){
        if(arr == NULL || length == 0)
            return -1;
         if( arr[0] < arr[1])
            return 0;
        if (arr[length - 1] < arr[length - 2])
            return length - 1;
        int left = 1;
        int right = length - 2;
        int mid = 0;
        for( ; left < right; ){
            mid = left + ((right - left ) >> 1);
            if(arr[mid] < arr[mid + 1] && arr[mid] < arr[mid - 1]){
                return mid;
            }
            else if(arr[mid] > arr[mid - 1]){
                right = mid - 1;

            }
            else if(arr[mid] > arr[mid + 1]){
                left = mid + 1;
            }
        }

    }

};

 

异或

在上面实现冒泡排序的算法中,swap()函数采用了异或的算法来实现位置交换,不用使用额外的变量(但其实此方法并不推荐使用,因为如果传入的两个参数指向的是同一块内存,会导致两个数都变成0)。

​
public static void swap(int[] arr, int i, int j) {
		arr[i] = arr[i] ^ arr[j];
		arr[j] = arr[i] ^ arr[j];
		arr[i] = arr[i] ^ arr[j];
}

异或(^) 可以看作是没有进位的加法运算,异或的性质有:a^a = 0; a^0 = a;异或满足交换律与结合律。

所以swap()函数的实现与证明过程如下:

a = a^b;//a = a^b  b = b 

b = a^b;//a = a^b  b = a^b^b = a

a = a^b;//a = a^b^a = b  b = a

现在有两道例题:

1. 一个数组中有一种数出现了奇数次,其余出现了偶数次,找出这个数

2. 一个数组中有两种数出现了奇数次,其余出现了偶数次,找出这个两种数

class OddTimes{
public:
    //1. 一个数组中有一种数出现了奇数次,其余出现了偶数次,找出这个数
    static int findOddTimesNum(int* arr,int length){
        int i = 1;
        int eor = arr[0];
        for( ; i < length; i++){
            eor ^= arr[i];
        }
        return eor;
    }

   // 2. 一个数组中有两种数出现了奇数次,其余出现了偶数次,找出这个两种数
    static void findOddTimesNum_2(int* arr, int length){
        int i = 1;
        int eor = arr[0];
        int rightOne = 0;
        for( ; i < length; i++){
            eor ^= arr[i];
        }
        cout <<"eor:" << eor <<endl;
        int res_1 = 0,res_2 = 0;
        //两种数出现了奇数次,它们的和一定不为0,即一定有至少一位为1,此处rightOne得到的是eor最右边的1的位置位置是1,其余位置是0
        rightOne = eor & (~eor + 1);
        cout << "rightOne:" << rightOne << endl;
        for(i = 0; i < length; i++){
            if( (rightOne & arr[i]) != 0){//注意这里 != 的优先级要高于  &
                 res_1 ^= arr[i];
                 cout << "i: " << i << "  res: "<< res_1 << endl;
            }

        }
        res_2 = eor ^ res_1;
        cout << res_1 << " " << res_2;
    }
};

对数器

对数器用来验证某一种算法(设要检测的算法为A)是否正确。首先实现一个随机样本产生器,使用另一种实现简单的算法(设该种算法为B)与A运行相同的随机样本,观察得到的结果是否一致。若不一致,打印样本或断点调试进行人工干预,改进算法A或算法B。若随机样本的数量很多时A和B的运行结果仍然相同,可以认为方法A时正确的。下面是验证排序算法时使用的计数器,这里可以使用冒泡或选择排序作为comparator。


    //for test
    //rand()返回一随机数值的范围在0至RAND_MAX 间。RAND_MAX的范围最少是在32767之间(int)。
    //用unsigned int 双字节是65535,四字节是4294967295的整数范围。0~RAND_MAX每个数字被选中的机率是相同的。
    static int getRandlyNum(){
        srand((unsigned int)(time(NULL)));
        return rand();
    }
    static int getRandlyNum(int maxSize){ //返回 0 到 maxSize 之间的随机整数
        srand((unsigned int)(time(NULL)));
        return rand() % maxSize;
    }
    static int getRandlyNum(int a,int b){//返回 a 到 b 之间的随机整数
        srand((unsigned int)(time(NULL)));
        return rand()% (b - a) + a;
    }
    static double getRandlyDoubleNum(){ //返回0到1之间的小数
        srand((unsigned int)(time(NULL)));
        return rand() / double(RAND_MAX);
    }

    static int* generateRandlyArray(int maxSize,int maxValue,int& length){
        length = getRandlyNum(2,maxSize);
        int* arr = new int(length);
        int i = 0;
        srand((unsigned int)(time(NULL)));
        for( ; i < length; i++){
            arr[i] = rand() % maxSize;
        }
        return arr;
    }

    static int* copyArray(int* arr){
        int i = 0;
        int length = sizeof(arr) / sizeof(arr[0]);
        int* res = new int(length);
        for( ; i < length; i++){
            res[i] = arr[i];
        }
        return res;
    }

    static bool isEqual(int* arr_1,int* arr_2){
        if( arr_1 == NULL || arr_2 == NULL || (sizeof(arr_1) / sizeof(arr_1[0])) != (sizeof(arr_2) / sizeof(arr_2[0]) ))
            return false;
        int i = 0;
        for( ; i < (sizeof(arr_1) / sizeof(arr_1[0])); i++){
            if(arr_1[i] != arr_2[i])
                return false;
        }
        return true;
    }

    static void  printArray(int* arr){
        if(arr == NULL)
            return;
        int length = sizeof(arr) / sizeof(arr[0]);
        int i = 0;
        for( ; i < length; i++){
            cout << arr[i] << " ";
        }
        cout << endl;
    }

 

递归

递归的实质是依靠系统栈来实现的。

可以将递归在系统栈中的执行顺序以二叉树的形式来表现。

计算递归算法的时间复杂度可以使用master公式。

master公式:T(N) = aT(N / b) + O(N^d)      其中,a表示实现该算法中有几个子问题,N/b 表示下一步子问题的数据规模,O(N^d)表示子问题外的时间复杂度。

如果 log(b,a) > d, 算法的时间复杂度为   O(N^log(b,a));

如果 log(b,a) = d, 算法的时间复杂度为   O(N^d*logN);

如果 log(b,a) < d, 算法的时间复杂度为   O(N^d);

如使用递归算法查找数组中的最大值。

public class GetMax {

	public static int getMax(int[] arr) {
		return process(arr, 0, arr.length - 1);
	}

	public static int process(int[] arr, int L, int R) {
		if (L == R) {
			return arr[L];
		}
		int mid = L + ((R - L) >> 1);
		int leftMax = process(arr, L, mid);
		int rightMax = process(arr, mid + 1, R);
		return Math.max(leftMax, rightMax);
	}

}

 

该算法中,a = 2, N/ b = N / 2, 子问题外的时间复杂度为O(1)。因为如果 log(b,a) = log(2,2) = 1 > d = 0, 算法的时间复杂度为   O(N));

需要注意的是,使用master公式的前提是子问题的规模必须相等。

子问题不相等的递归算法有BFPRT算法。

补充阅读::www.gocalf.com/blog/algorithm-complexity-and-master- theorem.html

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值