数据结构第一章复杂度的认识

发布于:2025-07-12 ⋅ 阅读:(42) ⋅ 点赞:(0)

一.复杂度的概念

1.复杂度的分类

在数据结构这门课程中,复杂度分为时间复杂度和空间复杂度。两者对应着不同的计算方法,下面我将一一讲解。

2.复杂度的理解

时间复杂度:该复杂度用来衡量算法的时间效率,并不是代码的具体运行时间。

空间复杂度:该复杂度用来衡量算法的空间效率,并不是计算代码占用多少字节的空间。

目前比较看重时间复杂度,因为目前市面上对节省空间的需求越来越少,因此时间效率便成为了看重的目标。

二.时间复杂度

1.计算方法

时间复杂度的计算方法是计算该算法的大概执行次数(估算)。

2.引例

请计算下列代码的时间复杂度

void Func1 (int N)
{
    int count=0;
    for(int i=0 ; i<N ; i++)//外循环执行N次
    {
        for(int j=0; j<N; j++)//内循环执行N次
        {
            count++;
        }
    }
    for(int k=0;k<2*N;k++)//此循环执行2*N次
    {
        count++;
    }
    int M=0;
    while(M--)//这个while循环执行M次,也就是10次。
    {
        count++;
    }
        printf("%d",count);
}

上述代码的具体执行次数为N*N+2*N+10,并且随着N的增大该代码执行的次数也随之增大。且表达式中的N^2对执行次数影响最大。

因为时间复杂度的计算是估算,应该选择表达式中影响最大的那一项,所以该代码的时间复杂度为O(N^{}^2)。(括号内是N平方的意思)

上面时间复杂度的表达手法被叫做大O的渐进表示法,主要是由一个大写的字母O和估算出来的时间复杂度构成,下面来说一下这个大O的渐进表示法。

3.大O的渐进表示法

(1)常用数字1取代运行时间复杂度表达式中的所有常数项。

(2)在修改后的运行次数函数中,只保留最高阶的那一项。

(3)如果最高项的系数不为1,应该把它的系数换成1。

这样得出的最终结果便是算法的复杂度。

4.练习

(1)写出函数Func2的时间复杂度

    void Func2 (int N)
    {
        int count =0;
        for(int k=0; k<2*N; k++)//循环了2*N次
            count++;
        int M=10;
        while(M--)//while循环了M次,也就是10次。
            count++;
        printf("%d",count);
    }

上述代码的具体执行次数为2*N+10,留住最高阶项,同时把最高项的系数换为1。得到了该算法的时间复杂度为O(N)。

(2)写出函数Func3的时间复杂度

void Func3 (int N)
{
    int count=0;
    for(int k=0; k<M;k++)//该循环了M次
        count++;
    for(int j=0; j<N; j++)//该循环了N次
        count++:
    printf("%d",count);
}

上述代码的具体执行次数为M+N,所以上述代码的时间复杂度为O(M+N)。

        注意:时间复杂度最后得出的结果不一定只有一个未知量,如上算法的时间复杂度便有两个未知量:M和N。

(3)写出函数Func4的时间复杂度

void Func4 (int N)
{
    int count =0;
    for(int k=0;k<100;k++)//该循环了100次
        count++;
    printf("%d",count);
}

上述代码的具体执行次数为100。根据大0渐进法的定义第一条:用常数1替换掉所有的时间表达式中的常数项。所以该算法的时间复杂度为O(1)。

        注意:如果时间复杂度为O(1),则说明该算法随着输入的N变大,时间效率是固定不变的。这也暗示O(1)是时间效率最高的代码!

(4)写出函数strchr的时间复杂度

const char* strchr (const char* str, char character)
{
    while(*str !='\0')//如果*str为'\0',也就是循环到*str字符串结尾的时候,终止循环。
    {
        if(*str == character)//如果此时的*str和目标数据character相同
            return str;//就返回这个*str的地址
            str++;//如果不相等,就继续循环,一直到循环终止为止
    }
    return NULL;//如果遍历了整个字符串,依然没有找到,就返回NULL。
}

上述代码的作用是:找到一个字符串中需要的字符并返回,如若没找到则返回NULL。

该代码的具体执行次数应该分情况讨论,(先假设字符串的长度为N)

最好的情况就是第1次就找到了,具体的执行次数就是1;

最坏的情况就是遍历了整个字符串都没有找到,具体的执行次数就是N。

不好不坏的情况(平均情况)就是遍历字符串到一半的时候找到了,具体的执行次数为N/2。

如上,便是所有情况下,算法的具体执行次数。那么我们应该选择哪一个呢??应该选择第一种:报喜不报忧,还是平均的情况呢?其实都不是。我们在计算时间复杂度时,如果遇到此类型需要分类讨论的题目时,我们应该使用最坏的情况。(只要最坏的情况能让别人接受,那么该算法就算通关了!!)

(5)写出函数BinarySearch的时间复杂度

int BinarySearch (int *a, int n,int x)
{
    assert(a);//判断a地址是否正确,正确则程序继续,错误则打印错误。
    int begin=0;
    int end=n;
    while(begin<end)//当begin大于等于end时循环终止
    {
        int mid= begin +((end-begin)>>1);//>>为右移操作符,a>>1的具体效果是a除以2.
        if(a[mid]<x)
            begin= mid+1;
        else if(a[mid]>x)
            end =mid;
        else 
            return mid;
    }
    return -1;
}

上述代码是一个二分查找的C语言代码,主要作用是在一个数组中找到一个数。

假设数组长度为N。

最好的情况是第一次就找到了,该情况的具体执行次数是1。

平均情况是遍历数组到一半的时候找到了目标数,此时该情况的具体执行次数为N/2。

最坏的情况就是一直在折半查找,折半到最后一步才找到(或者折半到最后一次也没有找到),此时,该情况的具体执行次数为_{}(以2为底N的对数)。解析:每次折半都会消失数组中一半的数据,等到消失到只有一个数据时,就结束了。假设最坏情况下的执行次数为x,那么2^x=N,所以最坏情况下的具体执行次数x为(以2为底N的对数)。

最后得出该函数的时间复杂度为O(logN)。一般情况下会省略底数,原因也很简单,计算机键盘不好输出对数函数的形式,所以省略底数。

(6)写出Factorical函数的时间复杂度

longlong Factorical (int N)
{
     return N<2?N:Factorical(N-1)*N
}

上述代码运行递归的手法,自己调用自己,完成了阶乘的计算。具体的执行次数为N次,所以该算法的时间复杂度为O(1)。

5.算法效率排行

在写代码的时候,不同的代码有着不同的时间复杂度,因为时间复杂度是用来计算代码运行的时间效率的,所以也可以根据时间复杂度来得知该代码的效率。下面给出不同时间复杂度的效率排行。

O(1)>O(logN)>O(N)>O(N*logN)>O(N^2)>O(2*N)>O(N!)

 三.空间复杂度

1.概念

空间复杂度:对一个算法在运行过程中临时占用存储空间大小的量度,并不是计算具体占用多少字节的空间,而是计算变量的个数。(同样满足大O渐进表示法)

2.练习

(1)写出BubbleSort函数的空间复杂度

void BubbleSort (int *a,int n)
{
    assert(a);//判断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;
    }
}

上述代码一共创建了5个变量,所以该函数的空间复杂度为O(1)

需要注意的是,循环创建的变量一直在重复利用他同一块空间,不用重复计算,也就是说:时间是累积的,空间是不累积的。

(2)写出Fibnacci函数的空间复杂度

longlong *Fibnacci (size_t n)//1
{
    if(n==0)
        return NULL;
    longlong *fibarray=(longlong *)malloc ((n+1)*size of(longlong));//fibarray为一个,后面又申请了N+1个
    fibarray[0]=0;//1
    fibarray[1]=1;//1
    for(int i=2; i<=n; i++)//1
    {
        fibarray [i]=fibbarray[i-1] +fibarray[i-2];
    }
    return fibarray;
}

上述代码一共开辟了N+6个变量,根据大O的渐进表示法,该函数的空间复杂度为O(N)。

(3)写出Factorical函数的空间复杂度

longlong Factorical (size_t N)
{
    return N<2?N:Factorical(N-1)*N;
}

        上述代码是一个递归函数 ,自己递归调用自己 ,每次递归调用都会建立一个栈帧,每次栈帧的创建都会使用一个常数空间。上面的算法,每次建立栈帧会创建一个变量,所以具体创建的变量的个数就等于创建的栈桢数(递归调用函数的次数),所以上述代码的空间复杂度为O(N)。

四.OJ练习题

1.题目一

        题目描述:有一个数组nums包括从0到n的所有整数,但是里面缺一个数字,请以时间复杂度为O(1)的代码找出这个数。

        示例:输入:[9,6,4,2,3,5,7,0,1]

                   输出:8

思路一:因为给出的数组是乱序,所以先给数组进行排序。排好顺序之后,遍历数组,找出缺少的那个数字。但是这个思路的时间复杂度不满足题意,舍去!!

下面是思路一的代码实现:

#include <stdio.h>

// 写出swap函数,使用传址调用
void swap(int* a, int* b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main()
{
    int nums[9];

    
    for (int i = 0; i < 9; i++)
    {
        scanf("%d", &nums[i]);
    }

    // 冒泡排序算法
    for (int j = 0; j < 8; j++) {
        for (int k = 0; k < 8 - j; k++) {
            if (nums[k] > nums[k + 1]) {
                swap(&nums[k], &nums[k + 1]);
            }
        }
    }

    // 查找并输出缺失的数字
    int flag = 0;
    for (int k = 0; k <= 9; k++) 
    {  // 检查0-9的数字
        flag = 0;
        for (int i = 0; i < 9; i++)
        {
            if (nums[i] == k) 
            {
                flag = 1;
                break;  // 找到后可以提前退出内层循环
            }
        }
        if (flag == 0) 
        {
            printf("%d ", k);  // 输出缺失的数字
        }
    }

    return 0;
}

思路二:将给出的数组全部加起来记作结果一,然后把数字0-N全部加起来记作为结果二。然后用结果二减去结果一就是缺失的那个数字。

思路三:将给出的数组和0-N的所有数据按位异或,最后得到的答案就是缺失的那个数字。

下面是思路三的代码实践:

int missingNumber(int *nums,int numSize)
{
    int x=0;
    for(int i=0;i<numSize;i++)
    {
        x=x^nums[i];
    }
    for(int j=0;j<numSize;j++)
    {
        x=x^j;
    }
    return x;
}
  

2.题目二

        题目描述:给定一个数组,将数组中的元素向右移动k个位置,其中k>=0。(空间复杂度控制在O(1)。)

        示例:输入:nums=[1,2,3,4,5,6,7]  k=3

                   输出:           [5,6,7,1,2,3,4]

                   解释:向右旋转第一步:[7,1,2,3,4,5,6]

                              向右旋转第二步:[6,7,1,2,3,4,5]

                              向右旋转第三步:[5,6,7,1,2,3,4]

思路一:旋转k次,每次把最后一个元素放到第一位,此算法的缺点是当数组中的数据过于庞大,时间复杂度就会变大,所以次算法不推荐。

下面是思路一的代码实践:

void rotate (int *nums,int numsSize,int k)
{
    while(k--)
    {
        int tmp=nums[numsSize-1];
        for(int end=numsSize-2;end>=0;end--)
        {
            nums[end+1]=nums[end]
        }
        nums[0]=tmp;
    }
}

思路二:创建一个新的数组,让旧数组后k个值赋值到新数组的前k位置上,让旧数组前k个元素赋值到新数组的后k个位置上。最后把新数组整体赋值给旧数组上。

思路三:让后k个元素逆置一次,然后让前n-k个元素逆置一次,最终让整个数组逆置一次,便能神奇地得到结果数组。

以下是思路三的代码实践:

void Reverse (int *nums,int left,int right)//写一个逆置函数
{
    while(left<right)
    {
        int tmp=nums[left];
        nums[left]=nums[right];
        nums[right]=tmp;
        left++;
        right--;
    }
}
void ROtate(int *nums,int numsSize,int k)
{
    if(k>=numsSize)//如果k大于数组的大小,对k进行处理,而后程序继续
        k%=numsSize;
    Reverse(nums,numsSize-k,numsSize-1);//分三次逆置,得到目标数组
    Reverse(nums,0,numsSize-k-1);
    Reverse(nums,0,numsSize-1);
}


网站公告

今日签到

点亮在社区的每一天
去签到