算法复习第一阶段

1.算法复杂度分析

时间复杂度
大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是代表代码时间随着数据规模增长的变化趋势,所以也叫渐进时间复杂度,简称时间复杂度
在这里插入图片描述

复杂度量级 **O(1) ** : 只要代码的运行时间不随n的增大而增长,这样代码的时间复杂度都是O(1)

最好时间复杂度
最坏时间复杂度
平均时间复杂度
均摊时间复杂度

均摊时间复杂度和摊还分析应用场景比较特殊,所以我们并不会经常用到

尽管很多数据结构和算法书籍都花了很大力气来区分平均时间复杂度和均摊时间复杂度,但其实我个人认为,均摊时间复杂度就是一种特殊的平均时间复杂度,我们没必要花太多精力去区分它们。你最应该掌握的是它的分析方法,摊还分析。至于分析出来的结果是叫平均还是叫均摊,这只是个说法,并不重要。

在这里插入图片描述
空间复杂度
类似时间复杂度,全称为渐进空间复杂度,表示算法的存储空间和数据规模之间的增长关系 常见的空间复杂度就是O(1),O(n),O(n^2)(像O(logn),O(nlogn)平时都用不到),空间复杂度相对简单,基本了解这些就够用了

复杂度图示

理解与认识 (渐进)时间和空间复杂度分析只是一个粗略的估计,O(logn)并不一定优于O(n)
在这里插入图片描述

在这里插入图片描述

2.数组

2.1总结

1.数组和链表的对比,根本原因在于数组在空间上是连续的,而链表是不连续的
在这里插入图片描述
2.由于数组是在连续空间存储相同类型的数据,所以数组支持随机访问
在这里插入图片描述

2.2问题

1. 为什么数组要从 0 开始编号,而不是从 1 开始呢? 从 1 开始不是更符合人类的思维习惯吗?
a.历史原因,c语言当初就是这这么设计的;
b.下标代表地址的偏移量,从0开始,第k个元素地址的计算公式为:
a[k]_address = base_address + k * type_size,
若从1开始,则: a[k]_address = base_address + (k-1)*type_size , 显然下标从0开始更简洁些
2.下面这段代码发生越界后为什么会无限循环?

int main(int argc, char* argv[]){
    int i = 0;
    int arr[3] = {0};
    for(; i<=3; i++){
        arr[i] = 0;
        printf("hello world\n");
    }
    return 0;
}

这段代码无限循环原因有2,以及一个附加条件:
1.栈空间从高往低依次分配,i占4字节,接着arr占12字节,内存从高往低是这样:存i的4字节|arr[2]|arr[1]|arr[0],数组访问是通过“baseAddr+index乘typeSize”得到,算下来当index=3时,刚好是i的地址
2.这里刚好满足字节对齐,系统为64位系统,字长64,那么字节对齐必须是8字节的倍数,刚好i变量和arr变量占了16字节,对齐了
如果这里将arr[3]改为arr[4],为了对齐,内存从高往低是这样:存i的4字节|空4字节|arr[3]|arr[2]|arr[1]|arr[0],那么arr[4]刚好是空的4字节,无法影响到i的值,则并不会无限循环
附加条件:编译时gcc默认会自动添加越界保护,此处要达到无限循环效果,编译时需加上-fno-stack-protector去除该保护

2.3典型算法题

算法题-数组

3.链表

3.1总结

在这里插入图片描述
注意单链表单纯删除操作的时间复杂度为O(1),但删除给定值时还需要先遍历,所以删除给定值的时间复杂度为O(n)
在这里插入图片描述在这里插入图片描述

3.2问题

链表的头结点和尾结点
头结点:链表的第一个结点
尾结点:链表的最后一个结点
链表本身不需要在内存中连续存储,所以插入,删除特别快(不需要移动元素)
头结点记录了链表的基地址,可借此遍历整个链表
尾结点指向一个空地址 NULL

在这里插入图片描述

优秀评论及答疑:
“数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。” 这里的CPU缓存机制指的是什么?为什么就数组更好了?

我没有百度也没有Google。之前开发时遇到过,我斗胆说下。
CPU在从内存读取数据的时候,会先把读取到的数据加载到CPU的缓存中。而CPU每次从内存读取数据并不是只读取那个特定要访问的地址,而是读取一个数据块(这个大小我不太确定。。)并保存到CPU缓存中,然后下次访问内存数据的时候就会先从CPU缓存开始查找,如果找到就不需要再从内存中取。这样就实现了比内存访问速度更快的机制,也就是CPU缓存存在的意义:为了弥补内存访问速度过慢与CPU执行速度快之间的差异而引入。

对于数组来说,存储空间是连续的,所以在加载某个下标的时候可以把以后的几个下标元素也加载到CPU缓存这样执行速度会快于存储空间不连续的链表存储。

如果字符串(正序反序都一样)是通过单链表来存储的,那该如何来判断是一个回文串呢?

使用快慢两个指针找到链表中点,慢指针每次前进一步,快指针每次前进两步。在慢指针前进的过程中,同时修改其 next 指针,使得链表前半部分反序。最后比较中点两侧的链表是否相等。

时间复杂度:O(n)
空间复杂度:O(1)

https://github.com/andavid/leetcode-java/blob/master/note/234/README.md

作者回复: 思路正确,不过空间复杂度计算的不对,应该是O(1),不是O(n)。空间复杂度要看额外的内存消耗,不是看链表本身存储需要多少空间。

3.3 典型算法题

LeetCode对应编号:206,141,21,19,876
链表

4.栈/队列/递归

4.1总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.2问题

4.3典型算法题

5.排序

5.1总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.2问题

1.特定算法是依赖特定的数据结构的。我们今天讲的几种排序算法,都是基于数组实现的。如果数据存储在链表中,这三种排序算法(冒泡/插入/选择排序)还能工作吗?如果能,那相应的时间、空间复杂度又是多少呢?

对于老师所提课后题,觉得应该有个前提,是否允许修改链表的节点value值,还是只能改变节点的位置。一般而言,考虑只能改变节点位置,冒泡排序相比于数组实现,比较次数一致,但交换时操作更复杂;插入排序,比较次数一致,不需要再有后移操作,找到位置后可以直接插入,但排序完毕后可能需要倒置链表;选择排序比较次数一致,交换操作同样比较麻烦。综上,时间复杂度和空间复杂度并无明显变化,若追求极致性能,冒泡排序的时间复杂度系数会变大,插入排序系数会减小,选择排序无明显变化。

2.插入排序和冒泡排序的时间复杂度相同,都是 O(n2),在实际的软件开发里,为什么我们更倾向于使用插入排序算法而不是冒泡排序算法呢?
在这里插入图片描述
3.快排和归并用的都是分治思想,递推公式和递归代码也非常相似,那它们的区别在哪里呢?
在这里插入图片描述
4.现在你有 10 个接口访问日志文件,每个日志文件大小约 300MB,每个文件里的日志都是按照时间戳从小到大排序的。你希望将这 10 个较小的日志文件,合并为 1 个日志文件,合并之后的日志仍然按照时间戳从小到大排列。如果处理上述排序任务的机器内存只有 1GB,你有什么好的解决思路,能“快速”地将这 10 个日志文件合并吗?

先构建十条io流,分别指向十个文件,每条io流读取对应文件的第一条数据,然后比较时间戳,选择出时间戳最小的那条数据,将其写入一个新的文件,然后指向该时间戳的io流读取下一行数据,然后继续刚才的操作,比较选出最小的时间戳数据,写入新文件,io流读取下一行数据,以此类推,完成文件的合并,
这种处理方式,日志文件有n个数据就要比较n次,每次比较选出一条数据来写入,时间复杂度是O(n),空间复杂度是O(1),几乎不占用内存,这是我想出的认为最好的操作了,希望老师指出最佳的做法!!!

5.3典型算法题

算法训练-排序

6.查找

6.1总结

在这里插入图片描述
在这里插入图片描述

6.2问题

6.3典型算法题

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值