数据结构(C\C++)——算法复杂度

发布于:2025-03-16 ⋅ 阅读:(15) ⋅ 点赞:(0)

前言

本文介绍算法复杂度的知识,以及相关练习

1. 数据结构前言

1.1 数据结构

数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。没有一种单一的数据结构对所有用途都有用,所以我们要学习各式各样的数据结构,如:线性表、树、图、哈希等。

1.2 算法

算法(Algorithm):就是定义良好的计算过程,它取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。

1.3 如何学好数据结构和算法

理解透彻每种常用算法后多刷题。

2. 算法效率

2.1 复杂度的概念

算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

时间复杂度主要衡量一个算法的运行快慢(不是运行时间),空间复杂度主要衡量一个算法运行所需要的额外空间,在当今计算机存储空间迅速发展的情况下,空间复杂度已经不是考虑重点。

2.2 复杂度的重要性

复杂度在校招中的考察已经很常见,并且在刷题时,对复杂度的限制也是十分明显。

3. 时间复杂度

3.1 定义

在计算机科学中,算法的时间复杂度是一个函数式T(N),它定量描述了该算法的运行时间。时间复杂度是衡量程序的时间效率,但不能直接计算程序的运行时间,原因如下:

  • 程序运行时间和编译环境和运行机器的配置都有关系,比如同一个算法程序,用不同编译器编译,在同样机器下运行时间不同。
  • 同一个算法程序,在不同配置机器上运行时间也不同。
  • 并且时间只能程序写好后测试,不能写程序前通过理论思想计算评估。

T(N)函数式计算了程序的执行次数。通过计算程序的执行次数,可以代表程序时间效率的优劣。

// 请计算⼀下Func1中++count语句总共执⾏了多少次? 
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; 
 } 
}

分析

实际中我们计算时间复杂度时,通常使用大O的渐进表示法,只需要计算程序能代表增长量级的大概执行次数

3.2 大O的渐进表示法

大O符号(Big O notation):是用于描述函数渐进行为的数学符号 。

推导大O阶规则:

  1. 时间复杂度函数式T(N)中,只保留最高阶项,其余低阶项对结果的影响很小,忽略不计。
  2. 如果最高阶项存在且不是一,通常忽略此项的常熟系数,N越大这个系数的影响就越小,N为无穷大时可以忽略不计。
  3. T(N)如果没有N相关的项,用常数1代替所有加法常数。
    现在看一下3.1的Fun1,算法复杂度为O(N^2)

3.3 时间复杂度计算示例

3.3.1 示例1
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); 
} 

Func2执行的基本操作次数: T ( N ) = 2 N + 10 T(N)=2N + 10 T(N)=2N+10,根据推导规则,Func2的时间复杂度为: O ( N ) O(N) O(N)

3.3.2 示例2
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); 
}

Func3执行的基本操作次数: T ( N ) = M + N T(N)=M + N T(N)=M+N,通常情况下时间复杂度为 O ( N + M ) O(N+M) O(N+M)

3.3.3 示例3
void Func4(int N) 
{ 
    int count = 0; 
    for (int k = 0; k < 100; ++ k) 
    {
        ++count; 
    }
    printf("%d\n", count); 
}

Func4执行的基本操作次数: T ( N ) = 100 T(N)=100 T(N)=100,根据推导规则1,Func4的时间复杂度为: O ( 1 ) O(1) O(1)

3.3.4 示例4
const char * strchr ( const char * str, int character)
{
    const char* p_begin = s;
    while (*p_begin != character)
    {
        if (*p_begin == '\0')
            return NULL;
        p_begin++;
    }
    return p_begin;
}

strchr执行的基本操作次数:

  • 若要查找的字符在字符串第一个位置,则 T ( N ) = 1 T(N)=1 T(N)=1 ,最好情况时间复杂度为 O ( 1 ) O(1) O(1)
  • 若要查找的字符在字符串最后的一个位置,则 T ( N ) = N T(N)=N T(N)=N ,最坏情况时间复杂度为 O ( N ) O(N) O(N)
  • 若要查找的字符在字符串中间位置,则 T ( N ) = N 2 T(N)=\frac{N}{2} T(N)=2N ,平均情况时间复杂度为 O ( N ) O(N) O(N)
    总结:
    大O的渐进表示法在实际中一般关注的是算法的上界,也就是最坏运行情况。
3.3.5 示例5 冒泡排序时间复杂度
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;
    }
}

BubbleSort执行的基本操作次数:

  • 若数组有序,则 T ( N ) = N T(N)=N T(N)=N
  • 若数组无序,分析

T ( N ) = N ∗ ( N + 1 ) 2 T(N)=\frac{N*(N + 1)}{2} T(N)=2N(N+1)

BubbleSort的时间复杂度取最差情况为: O ( N 2 ) O(N^{2}) O(N2)

3.3.6 示例6
void func5(int n)
{
    int cnt = 1;
    while (cnt < n)
    {
        cnt *= 2;
    }
}

假设执行次数为x,则 2 x = n 2^{x}=n 2x=n,因此执行次数 x = l o g n x = log n x=logn,func5的时间复杂度取最差情况为: O ( l o g n ) O(log n) O(logn) .

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

递归算法的时间复杂度 = 单次递归的时间复杂度 * 递归次数
在这里插入图片描述

调用一次Fac函数的时间复杂度为 O ( 1 ) O(1) O(1),而在Fac函数中,存在n次递归调用Fac函数,因此阶乘递归的时间复杂度为: O ( n ) O(n) O(n)

4. 空间复杂度

空间复杂度也是一个数学表达式,是对一个算法在运行过程中因为算法的需要额外临时开辟的空间。空间复杂度不是程序占用了多少bytes的空间,算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法

注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定

4.1 空间复杂度计算示例

4.1.1 示例1
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;
    }
}

BubbleSort额外申请的空间有exchange等有限个局部变量,使用了常数个额外空间,因此空间复杂度为 O ( 1 ) O(1) O(1)

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

Fac递归调用了N次,额外开辟了N个函数栈帧,每个栈帧使用了常数个空间,因此空间复杂度为: O ( N ) O(N) O(N)

5. 常见复杂度对比

n n n l o g 2 n log_{2} n log2n n × l o g 2 n n ×log_{2} n n×log2n n 2 n^{2} n2 n 3 n^{3} n3 2 n 2^{n} 2n n ! n! n!
4 4 4 2 2 2 8 8 8 16 16 16 64 64 64 16 16 16 24 24 24
8 8 8 3 3 3 24 24 24 64 64 64 512 256 80320
10 10 10 3.32 33.2 100 1000 1024 3628800
16 16 16 4 4 4 64 64 64 256 4096 65536 2.1 × 1 0 13 2.1×10^{13} 2.1×1013
32 32 32 5 5 5 160 1024 32768 4.3 × 1 0 9 4.3×10^{9} 4.3×109 2.6 × 1 0 35 2.6×10^{35} 2.6×1035
128 7 7 7 896 16384 2097152 3.4 × 1 0 38 3.4×10^{38} 3.4×1038 ∞ \infty
1024 10 10 10 10240 1048576 1.07 × 1 0 9 1.07×10^{9} 1.07×109 ∞ \infty
10000 13.29 132877 108 1 0 12 10^{12} 1012 ∞ \infty ∞ \infty

从图表可以看出,随着n的增长,不同复杂度的函数值变化差异很大, O ( 1 ) O(1) O(1)复杂度最低, O ( n ! ) O(n!) O(n!)复杂度增长最快。

6. 复杂度算法题

6.1 旋转数组

题目链接:https://leetcode.cn/problems/rotate-array/description/

思路1

循环K次将数组所有元素向后移动一位(但时间复杂度超出限制)。

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

在这里插入图片描述
时间复杂度 O ( n 2 ) O(n^{2}) O(n2).

思路2

空间复杂度 O ( n ) O(n) O(n),申请新数组空间,先将后k个数据放到新数组中,再将剩下的数据挪到新数组中。

void rotate(int* nums, int numsSize, int k) 
{
	//创建新数组
    int newArr[numsSize];
	//向右轮转K次,将结果保存在临时数组中
    for (int i = 0; i < numsSize; ++i) 
    {
        newArr[(i + k) % numsSize] = nums[i];
    }
	//将临时数组中的数据导入到数组中
    for (int i = 0; i < numsSize; ++i) 
    {
        nums[i] = newArr[i];
    }
}
思路3(进阶)

基本思路:三次逆置
思路

空间复杂度 O ( 1 ) O(1) O(1),前n - k个逆置,后k个逆置,整体逆置。

void reverse(int* nums,int begin,int end)
{
    while(begin<end){
        int tmp = nums[begin];
        nums[begin] = nums[end];
        nums[end] = tmp;
        begin++;
        end--;
    }
}
void rotate(int* nums, int numsSize, int k)
{
    k = k%numsSize;
    reverse(nums,0,numsSize-k-1);
    reverse(nums,numsSize-k,numsSize-1);
    reverse(nums,0,numsSize-1);
}

总结

这部分是数据结构的前置知识,但也很重要,如有错误还望指正。
数据结构从这篇开始了,明天更新顺序表,还望多多支持。
想起一段游戏台词(略改)作为结尾吧:
——敬我们的过去、现在、未来和稚子至死不渝的梦。
——敬坚忍的岁月、每个悲伤的夜晚,和终将到来的黎明。
——敬不在沉默的历史、热烈而勇敢的奔赴,和通往群星的旅途。
——敬不完美的明天。