复杂度——时间复杂度

目录

前言

        算法的复杂度

        时间复杂度

        三、时间复杂度习题

                  例1、2、3、4:普通循环的时间复杂度

例5:二分查找法的时间复杂度

例6:冒泡排序的时间复杂度

例7:普通递归的时间复杂度

例8:求斐波那契数的时间复杂度

附加题:逆置数组


前言

        C程序在实现某种特定的功能的时候,会有多种代码去实现,为了提高程序的效率,需要考虑代码执行的时间和代码存储需要的空间。而在当今存储设备的进步,人们不是很在意存储代码所花空间的大小,会更在意代码运行的时间。

算法的复杂度

        我们熟知的斐波那契数列,用递归的方式去实现的时候,代码十分简洁,但是这简洁的代码能说明它的效率高吗?

long long Fib(int N) 
{

     if(N < 3)
     return 1;
     return Fib(N-1) + Fib(N-2);
}

        算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般 是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算 机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计 算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

时间复杂度

        定义:在计算机科学中,算法的时间复杂度是一个函数 ,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有当程序执行起来,从它的执行快慢的情况下主观的判断。如果我们将每个代码都上机测试,那样将会很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法 的时间复杂度。

        即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。

O 的渐进表示法
1.若运行次数为常数,无论大小,时间复杂度均记为O(1)
2.若计算的运行次数为N的表达式,则只保留最高阶项,并将系数视为1
        上述方法可以去掉对结果影响不大的项,简单明了的表示出了执行次数,在某些情况下算法的时间复杂度存在最好、平均、最坏三种。
最坏情况
任意输入规模的最大运行次数 ( 上界 )
平均情况
任意输入规模的期望运行次数
最好情况
任意输入规模的最小运行次数 ( 下界 )

时间复杂度习题

例1

// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{
	int count = 0;
	for (int i = 0; i < N; ++i)        //N
	{
		for (int j = 0; j < N; ++j)    //N
		{
			++count;
		}
	}
                                       //共N*N次
	for (int k = 0; k < 2 * N; ++k)    //2*N
	{
		++count;
	}
	int M = 10;
	while (M--)                        //10
	{
		++count;
	}
    
    printf("%d\n", count);
}

        经过计算count一共计算F(N)=  N^2+2*N+10次

        N=10            F(N)=130

        N=100          F(N)=10210

        N=1000        F(N)=1002010

         上述中随着N的值不断增大,除了最高阶项,其余项对计算次数了影响很小,所以可以忽略不计。那么实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法可知时间复杂度为O(N^2)。

例2

// 计算Func2的时间复杂度?
void Func2(int N) 
{

	int count = 0;
	for (int k = 0; k < 2 * N; ++k)
	{
		++count;
	}

	int M = 10;
	while (M--)
	{
		++count;
	}

	printf("%d\n", count);
}

        经过计算一共计算F(N)=  2*N+10次,则时间复杂度为O(N)

例3

// 计算Func3的时间复杂度?
void Func3(int N, int M) 
{
	int count = 0;
	for (int k = 0; k < M; ++k)
	{
		++count;
	}

	for (int k = 0; k < N; ++k)
	{
		++count;
	}

	printf("%d\n", count);
}

        经过计算一共计算F(N)=  M+N次,则时间复杂度为O(M+N),若M>>N,则时间复杂度为O(M),反之则为O(N)。

例4

void Func4(int N) 
{
	int count = 0;
	for (int k = 0; k < 100; ++k)
	{
		++count;
	}

	printf("%d\n", count);
}

        程序总共计算了100次,次数为常数,所以时间复杂度为O(1)

例5

int BinarySearch(int* a, int n, int x) 
{
	assert(a);
	int begin = 0;
	int end = n - 1;
	// [begin, end]:begin和end是左闭右闭区间,因此有=号
	while (begin <= end)
	{
		int mid = begin + ((end - begin) >> 1);        //求中间值
		if (a[mid] < x)
			begin = mid + 1;
		else if (a[mid] > x)
			end = mid - 1;
		else
			return mid;
	}
	return -1;
}

        二分查找法,最好的情况是第一次就找到了,或者几次就找到了,这样时间复杂度为O(1)

最坏的情况是最终找不到需要找到的值。

例6

        冒泡排序

void BubbleSort(int* a, int n)
{
	assert(a);
	for (size_t end = n; end > 0; --end)
	{
		int exchange = 0;
		for (size_t i = 1; i < end; ++i)
		{
			if (a[i - 1] > a[i])
			{
				Swap(&a[i - 1], &a[i]);
				exchange = 1;
			}
		}
		if (exchange == 0)
			break;
	}
}

        执行次数为:(N-1)+(N-2)+(N-3)+.....+2+1=(N^N-N)/2,故时间复杂度为O(N^N)若数据本来就是有序的,则时间复杂度为O(N)。

 例7

// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N) 
{
    if(0 == N)
        return 1;
    return Fac(N-1)*N; 

}

        该递归调用时,N的值变化情况有:N、N-1、N-2、N-3......3、2、1,总共有N次变化情况,所以该递归的时间复杂度为O(N)。

例8

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
     if(N < 3)
         return 1;
 
     return Fib(N-1) + Fib(N-2);
}

         递归数为:2^0+2^1+2^2+......+2^(N-2)+2^(N-1)=-2^0+2^N=2^N+1

        则斐波那契数的递归的时间复杂度为O(2^N)

附加题:轮转数组——只考虑时间复杂度

方法一:

int main()
{
	int arr[] = { 1,2,3,4,5,6,7 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int k;
	scanf("%d", &k);
	while (k--)
	{
		int n = arr[sz-1];
		for (int i = sz-1; i > 0; --i)
		{
			arr[i] = arr[i - 1];
		}
		arr[0] = n;
	}

	for (int j = 0; j < sz; j++)
		printf("%d ", arr[j]);
	return 0;

}

        打印只是为了观察交换的结果,我们忽略打印的步骤,可以计算出。

k=1,时间复杂度为O(N)

未知k等于多少,时间复杂度为O(kN)

没说明k的值,时间复杂度为O(N^N)

方法二:牺牲空间,提高算法效率

void rotate(int* nums, int numsSize, int k) 
{
    int i = 0;
    int j = 0;
    if (k > numsSize)
        k = k % numsSize;
    if (k == numsSize)
    {
        for (i = 0; i < numsSize; ++i)
        {
            printf("%d", nums[i]);
        }
    }
    else
    {
        int left = numsSize - k;
        int right = k;
        int* arr1 = (int*)malloc(sizeof(int) * left);
        int* arr2 = (int*)malloc(sizeof(int) * right);
        if(arr1==NULL || arr2==NULL)
        {
            printf("malloc fail");
            return;
        }
       
        for (i = 0; i < numsSize; ++i)
        {
            if (i < left)
                arr1[i] = nums[i];
            else
            {
                arr2[j] = nums[i];
                j++;
            }

        }
        for (i = 0, j = 0; i < numsSize; ++i)
        {
            if (i < right)
                nums[i] = arr2[i];
            else
            {
                nums[i] = arr1[j];
                j++;
            }

        }   
        free(arr1);
        free(arr2);
    }
}

        该方法为:将需要倒叙的数组元素分别放在新创建两个数组中,最终再将两个新创建的数组按照需要的顺序放入原数组中,放入新数组执行了N次,再放入元素组执行力N次,一共执行了2N次,所以时间复杂度为O(2N)=O(N),该方法需要浪费一定的空间。

例如:k=3

原数组:int nums[ ]={1,2,3,4,5,6,7}

新数组:int arr1[ ]={1,2,3,4}

新数组:int arr2[ ]={5,6,7}

调整后的原数组:int nums[ ]={5,6,7,1,2,3,4}

方法三:最优解

void reverse(int* p, int left, int right)
{
	while (right > left)
	{
		int tmp = p[left];
		p[left] = p[right];
		p[right] = tmp;
		right--;
		left++;
	}
}
void rotate(int* p ,int sz, int k) 
{
    if (k > sz)
		k %= sz;

    reverse(p, 0, sz - k - 1);
	reverse(p, sz - k , sz - 1);
	reverse(p, 0, sz - 1);
}

        该方法为先将左边部分逆序,再将右边部分逆序,然后再整体逆序。

例如:k=3

原数组:int nums[ ]={1,2,3,4,5,6,7}

前n-k个逆置:int nums[ ]={4,3,2,1,5,6,7}

后k个逆置:int nums[ ]={4,3,2,1,7,6,5}

整体逆置:int nums[ ]={5,6,7,1,2,3,4}

猜你喜欢

转载自blog.csdn.net/m0_64616721/article/details/125975977