初阶数据结构学习记录——하나 时间和空间复杂度

目录

一、时间复杂度

二分查找

递归

二、空间复杂度

冒泡排序

斐波那契数列

递归

三、练习

数组nums包含从0-n的每个整数,但缺少了一个。请在O(N)时间内找到它。

3、异或


一、时间复杂度

程序的优化不仅仅是在说简化代码,减少代码行这样,而是要尽量减少时间复杂度和空间复杂度。时间复杂度用来判断一个程序运行所耗费的时间。然而它并不是所想象的计算运行时间,而是计算基本操作被执行的次数。这里的操作不是那些定义变量的活,而是要执行的语句。有没有作用都可。即使要排序某个东西,在排序之前,操作者又把所有数字遍历输出了一遍,这也会进入到计算中。一个形象的例子:

void Func1(int N)
{
    int count = 0;
    for (int i = 0; i < N; ++i)
    {
        for (int j = 0; j < N; ++j)
        {
            ++count;
        }
    }
    for (int k = 0; k < 2 * N; ++k)
    {
        ++count;
    }
    int M = 10;
    while (M--)
    {
        ++count;
    }
    printf("%d\n", count);
}

一个简单的代码块。这个程序的时间复杂度怎么算?现在里头有三个循环,次数分别是N*N, 2 * N, 10。虽然程序执行了这么多次语句,但实际上,时间复杂度并不就是这个数。我们可以带入一个数字,比如N = 10,,N = 20,,N = 100,,N = 10000等等越来越大,这时候会发现N * N,也就是N的2次方这个数占比很大,而2N+10这个数占比则越来越小。当N趋近于无穷大时,2N+10可以忽略。所以我们只需要取N的2次方即可。

关于时间复杂度的表现形式如下:

O()

括号里就是我们得到的数值。比如上述程序时间复杂度就是O(N^2)。时间复杂度的表现形式就是大O符号,是用来描述函数渐进行为的数学符号。由上面的例子我们可引出大O阶的一个规则:在修改后的运行次数函数中,只保留最高阶项。现在继续例子。

void Func1(int N)
{
    int count = 0;
    for (int k = 0; k < 2 * N; ++k)
    {
        ++count;
    }
    int M = 10;
    while (M--)
    {
        ++count;
    }
    printf("%d\n", count);
}

现在去掉上面的ij循环,只剩下2*N+10,10一样可以忽略,这对于计算机来讲不算什么。所以最后得到的数值就是2N吗?这涉及到另一个规则:如果最高阶项存在且不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶。啥意思?意思就是这个程序的时间复杂度就是O(N)。去掉了2,最终数值就是N。回头看一下,或许还有一个疑问,为什么强调不是1?最后一个规则:用常数1取代运行时间中的所有加法常数。看例子

void Func1(int N)
{
    int count = 0;
    int M = 10;
    while (M--)
    {
        ++count;
    }
    printf("%d\n", count);
}

还是这个。现在只有一个10了。大O阶就是O(1).其实大O阶是个估算的东西,估算的就是大概次数所属量级。到了现在,你一定有疑惑,为什么就可以去除掉一些东西,真的没影响吗?其实这对于计算机来说确实没影响。请看代码

int main()
{
    size_t begin = clock();
    size_t n = 0;
    for (size_t i = 0; i < 100000000000; ++i)
    {
        ++n;
    }
    size_t end = clock();
    printf("%d毫秒\n", end - begin);
    return 0;
}

这里用到了clock这个库函数,这得需要<time.h>头文件。此程序计算了这个循环的用时。在debug下,随着数字越来越大,毫秒数也会变大。但是即使现在这个11个0的大数字,也就是几百毫秒。如果换成release版本,就会变成0毫秒了。所以这对计算机来说真不算压力。

继续一些实例。

回想一下冒泡排序。i和j,都需要遍历。遍历一次后,就从N个数字变成N-1个数字,然后一点点减小,直到1。这就是一个等差数列,算出来的总次数就是N*(N-1)/2,也就是1/2 * N^2 - 1/2 * N。按照规则,0.5就可以去掉,0.5N也可以去掉。所以这个的大O阶就是O(N^2)。

二分查找

int BinarySearch(int* a, int n, int x)
{
    assert(a);
    int begin = 0;
    int end = n - 1;
    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(log N)。二分查找每次查找,查找区间都会缩减一半。而关于运行语句的次数,最后数字量会变为1, N /2/2/2/2/2.....=1。所以得出N是2的次方,那么x就是logN。所以时间复杂度就是O(log N)。带入具体的数字就会发现,和暴力查找,只是简单的循环遍历而言,二分查找高效得多。

递归

long long Fac(size_t N)
{
    if (0 == N)
        return 1;
    return Fac(N - 1) * N;
}

传入N这个数字,进入函数中,我们也就是判断了1次,是就退出,不是就继续递归。每次进入就执行1次,那么总体的时间复杂度就是O(N)。

变化下

long long Fac(size_t N)
{
    if (0 == N)
        return 1;
    for (size_t i = 0; i < N; ++i)
    {
        //....
    }
    return Fac(N - 1) * N;
}

同之前一样,是个等差数列。进入一次就会遍历N次。当然我们也可以加上 if 语句的判断。实际上下来,时间复杂度就是O(N^2)。

二、空间复杂度

时间复杂度写完,现在看空间复杂度。空间复杂度其实计算的是整个程序临时占用存储空间大小的量度,同样也使用大O阶。函数运行时所需要的栈空间(存储参数,局部变量,一些寄存器信息等等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时显式申请的额外空间来确定。

冒泡排序

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;
    }
}

a,n都是已经安排好的,而这里的变数就是end, exchange, i这三个临时建立,只有这三个常数,所以空间复杂度就是O(1)。

斐波那契数列

long long* Fib(size_t n)
{
    if (0 == n)
        return NULL;
    long long* fib = (long long*)malloc((n + 1) * sizeof(long long));
    fib[0] = 0;
    fib[1] = 1;
    for (int i = 2; i <= n; ++i)
    {
        fib[i] = fib[i - 1] + fib[i - 2];
    }
    return fib;
}

动态开辟了n+1个空间。再加上i,也就是O(N)。实际上,空间复杂度大多都是O(1)   O(N)。

递归

递归在每一次进入后,都会开辟栈帧所需的空间,最终会消耗N个栈帧。其实和时间复杂度一样,时间会去计算每次调用的执行次数,而空间会计算变量个数。所以空间复杂度就是O(N)。

三、练习

数组nums包含从0-n的每个整数,但缺少了一个。请在O(N)时间内找到它。

我们现在有一些办法:

1、求和相减

0-n作为一个等差数列,算出他们的值再减去nums的总和,这样就能找到缺少的值。这个方法时间为O(N),空间为O(1)。

2、qsort排序

这个方法并不推荐。时间为O(logN * N),空间为O(log N)。如果是冒泡排序,时间则为O(N^2),,空间为O(1)。

3、异或

时间为O(N), 空间为O(1)。

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

借助这些例子,更好地理解时空间复杂度。

结束。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值