当前位置: 首页 > news >正文

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

一.复杂度的概念

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;//1fibarray[1]=1;//1for(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);
}

http://www.dtcms.com/a/274364.html

相关文章:

  • WebSocket 重连与心跳机制:打造坚如磐石的实时连接
  • 005---Xilinx Viivado FIFO (二)---fifo IP核使用总结
  • python之set详谈
  • 大数据驱动的酒店用品需求预测模型研究 开发——毕业论文,毕业设计——仙盟创梦IDE
  • Linux驱动基本概念(内核态、用户态、模块、加载、卸载、设备注册、字符设备)
  • linux文件系统目录结构以及交互界面
  • 稳定币将成为新时代的重要金融工具
  • Pandas:数据类型转换
  • c99-柔性数组
  • NVME在ubuntu上总是导致死机
  • Android simpleperf生成火焰图
  • 深度解析 DApp 开发:从技术架构到商业落地的全链路解决
  • Linux 进程管理核心机制
  • 掌握Spring声明式事务传播机制:AOP与ThreadLocal的协同工作
  • 破解异构日志清洗五大难题,全面提升运维数据可观测性
  • 用FunctionCall实现文件解析(一):环境准备与基础知识
  • uniapp语音播报天气预报微信小程序
  • 秒杀系统该怎么设计?
  • uniapp-在windows上IOS真机运行(含开发证书申请流程)
  • 在Spring Boot 开发中 Bean 的声明和依赖注入最佳的组合方式是什么?
  • Spring Boot集成Redis:从配置到实战的完整指南
  • Adobe Acrobat DC JavaScript 基础到应用
  • python的卷烟营销数据统计分析系统
  • 重学前端003 --- CSS 颜色
  • 汽车级MCU选型新方向:eVTOL垂桨控制监控芯片的替代选型技术分析
  • 实现在线预览pdf功能,后台下载PDF
  • PDF 转图助手 PDF2JPG 绿色版:免安装直接用,急处理文件的救急小天使
  • 电力分析仪的“双语对话”:CCLinkIE与Modbus TCP的无缝连接
  • 【Unity游戏存档系统】
  • 爬虫练习1