1.1 初识数据结构
首先我们要知道何为数据结构?===>数据结构是一种组织和存储数据的方式,旨在使数据能够高效地被访问和修改。它涉及到在计算机中组织数据的设计和实现方法。
而我们之前所学习的C语言就好比是一种语言,而数据结构就是一种说话的技巧,如何使说话更加简洁,有逻辑,容易让别人听懂,这种表达技巧不管使用中文还是英文都能够实现。C语言是用来讲解数据结构的一种方法,除此之外我们还可以用Java语言来讲解数据结构,这里就不过多赘述了。
1.2 数据结构与算法
在一个好的程序是需要数据结构和算法共同发挥作用的。(数据结构+算法=程序)我们可以打个比方,数据结构就相当于一个果园的结构,它是数据组织和存储的方式,而算法则是果实采摘和加工的技艺。果园的结构(数据结构)决定了如何有效地组织和存储果实(数据),而采摘和加工技艺(算法)则是在特定的结构上进行操作的方法。由此我们可以得出结构:没有果园的结构,采摘和加工技艺将无法有效地进行;同样,没有合适的数据结构,算法也无法有效地处理数据。
一个人工作干的好不好,我们得看他效率高不高;算法同样如此,看一个算法怎么样,得看他算法效率高不高。算法在编写一个程序时,运行需要消耗时间资源和空间(内存)资源。因此,我们衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度
1.3 复杂度
时间复杂度主要来衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在几十年前,计算机的存储空间是很小的,因此十分看重空间复杂度,但随着我们现代科技的迅速发展,计算机的存贮容量已经达到了一个很高的水平了,因此我们也不用再那么看重空间复杂度。至于决定复杂度的另一个因素-----时间,我们就来好好唠嗑唠嗑吧。
1.3.1 时间复杂度
在计算机科学中,算法的时间复杂度是⼀个函数式T(N),它定量描述了该算法的运⾏时间。时
间复杂度是衡量程序的时间效率。
一个算法的运行时间该如何计算呢?想必聪明的你已经想到了:每次运行的时间*运行的次数。但是,我们还得考虑一个问题:咱们每个人使用的装备都不同,咱们在敲代码时的编译环境和运行环境都不尽相同,那么,咱们每个人运行代码的时间应该也是不同的吧,因此咱们要把这个“好动分子”关进大牢,咱们就关注它的好朋友----运行次数就好了。现在咱们来看看几个例子,看看如何写时间复杂度的函数式。
int Fun1(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<n;k++)
{
count++;
}
int m=100;
while(m--)
{
count++;
}
}
T(N)=N^2+N+100 我们可以看出,这个时间复杂度函数式是由好几项组成的,但是我们想想如果我们要比较两个算法的时间复杂度,如果两个都有好几项,难不成咱们要一项一项比较嘛,想想都觉得很麻烦吧。由于我们在计算时间复杂度时,计算的并不是程序的精确的执行次数(因为计算机的水是很深的,咱们一眼望不到低的)通常咱们都是估算,于是乎就发明了大O的渐进表示法。
---------------------------------------------------------------------------------------------------------------------------------
⼤O符号(Big O notation):是⽤于描述函数渐进⾏为的数学符号 O()
💡 推导⼤O阶规则
1. 时间复杂度函数式T(N)中,只保留最⾼阶项,去掉那些低阶项,因为当N不断变⼤时,
低阶项对结果影响越来越⼩,当N⽆穷⼤时,就可以忽略不计了。
2. 如果最⾼阶项存在且不是1,则去除这个项⽬的常数系数,因为当N不断变⼤,这个系数
对结果影响越来越⼩,当N⽆穷⼤时,就可以忽略不计了。
3. T(N)中如果没有N相关的项⽬,只有常数项,⽤常数1取代所有加法常数
---------------------------------------------------------------------------------------------------------------------------------
现在我们Fun1用大O的渐进表示法表示就是O(N^2)
---------------------------------------------------------------------------------------------------------------------------------
void Fun2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 100;
while (M--)
{
count++;
}
printf("%d\n", count);
}
上面Fun2的时间复杂度函数式就是T(N)=2*N+100,用大O的渐进表示法就是O(N)
---------------------------------------------------------------------------------------------------------------------------------
void Fun3(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++ k)
{
count++;
}
for (int k = 0; k < N ;k++)
{
count++;
}
上面Fun3的时间复杂度的函数式是T(N)=M+N,但是用大O的渐进表示法咱们就要推敲推敲了。
当M>>N时,N对M来说可以忽略不计,因此就可以表示为O(M);
当N>>M时,M对N来说可以忽略不计,因此就可以表示为O(N);
当M与N相差不大的时候,M与N对彼此来说都是不可忽略的存在,因此表示为O(M+N).
---------------------------------------------------------------------------------------------------------------------------------
void Fun4(int N)
{
int count = 0;
for (int k = 0; k < 100; k++)
{
count++;
}
}
上面Fun4的时间复杂度函数式是 T(N)=100,用大O表示就是O(1)。注意这里的此1非彼1,由于对于计算机来说,那些成千上万次运行不过是毫秒之间。为了方便,咱们就索性把那些常数都看作1,记作O(1)
---------------------------------------------------------------------------------------------------------------------------------
现在咱们再来介绍一个例子:
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)=N;
当我们所查找的字符恰好在字符串的中间位置:T(N)=N/2.
上面那些情况的时间复杂度分别为O(1),O(N),O(N)
---------------------------------------------------------------------------------------------------------------------------------
💡 总结
通过上⾯我们会发现,有些算法的时间复杂度存在最好、平均和最坏情况。
最坏情况:任意输⼊规模的最⼤运⾏次数(上界)
平均情况:任意输⼊规模的期望运⾏次数
最好情况:任意输⼊规模的最⼩运⾏次数(下界)
⼤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;
}
}
当这个数组是有序的时候,T(N)=N;
当这个数组是无序的时候,T(N)=N*(N+1)/2;
在冒泡排序中外循环进行了n次,在每次外循环进行之后,其对应的内循环也开始运作了:外循环第一次运行,内循环运行了n-1次,外循环第二次运行,内循环运行了n-2次(因为每运行一轮就能少排一个数)按这种规律下去,当外循环进行第n次循环时,内循环进行1次,最后我们将内循环中所有的运行次数相加即为咱们所求的时间复杂度函数式。
因此 ,BubbleSort的时间复杂度取最差的情况为O(N^2)
---------------------------------------------------------------------------------------------------------------------------------
void fun5(int n)
{
int cnt = 1;
while (cnt < n)
{
cnt *= 2;
}
}
当n=2时,执行次数为1; 当n=4时,执行次数为2; 当n=16时,执行次数为4
假设执行次数为 x ,则 2^x=n
因此执行次数: x = log n(log以2为底数,计算机键盘敲不出来)
因此:func5的时间复杂度取最差情况为:O(log n) (log以2为底数)
其实对于上面那个问题,那些搞计算机的早就解决了,由于在计算机中,那些对于我们来说很大的数字,对于计算机来说却不值一提,因此不管底数多少都可以忽略不写,即可以写成log n.
--------------------------------------------------------------------------------------------------------------------------------
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
对于上面递归函数,咱们也可以来求求他的时间复杂度,在递归函数中,每次运行时的时间复杂度都是O(1),则进行了n次就是O(N)(n*O(1))
1.3.2 空间复杂度
空间复杂度其实也是一个数学表达式,一个算法在运行过程中会临时开辟空间。空间复杂度计算的并不是程序占用了多少bytes的空间,因为每种数据的类型的字节大小都是不同的,因此我们为的统一,空间复杂度算的是变量的个数(注意是临时创建的变量,并不是原先就存在的变量)
空间复杂度的计算规则与时间复杂度类似,也使用大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;
}
}
上面的代码中函数栈帧在编译期间就已经确定好了,只需要关注函数在运行时额外申请的空间。
上面那个代码中只申请了常数个额外空间,固空间复杂度为O(1)
---------------------------------------------------------------------------------------------------------------------------------
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
上面的那个代码是个递归函数,由于递归函数在每次进行时都要开辟一个空间,递归了n次,因此其空间复杂度为O(N)。
下面是一些常见复杂度对比图

1.0&spm=1001.2101.3001.5002&articleId=140284414&d=1&t=3&u=00e09c91f8fe4e94ba026d65d18ef53b)
1452

被折叠的 条评论
为什么被折叠?



