前言
这个课讲的内容比较简单,不过在简单过一遍的过程中依然发现了一些自己之前没太想过或者说基础不牢固的地方,在这里整理一下。
笔记
数组
数组和链表的区别
数组是连续分配的内存块,因此即使内存剩余很多若是有很多碎片的话数组依然无法很好利用内存。链表则对每个节点的位置没有要求,相对分散。作为代价,链表便于插入删除,而数组则易于随机访问。
同时,因为内存连续,CPU可以更好缓存数组内容(缓存通常一次缓存一个页)。
计算机堆栈结构
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;
}
上述程序发生了越界问题,不过因为C语言没有越界异常检测功能,这个程序会一直循环下去。这与计算机堆栈结构有关。
原因:函数体内的局部变量存在栈上,且是连续压栈。在Linux进程的内存布局中,栈区在高地址空间,从高向低增长。变量i和arr在相邻地址,且i比arr的地址大,所以arr越界正好访问到i。当然,前提是i和arr元素同类型,否则那段代码仍是未决行为。
对于高级语言使用者,有时依然需要用到数组,原因如下:
1.Java ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
2. 如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
3. 还有一个是我个人的喜好,当要表示多维数组时,用数组往往会更加直观。比如 Object[][] array;而用容器的话则需要这样定义:ArrayList<ArrayList<object> > array。
队列
可以使用循环队列+CAS实现并发队列
具体方法:考虑使用CAS实现无锁队列,则在入队前,获取tail位置,入队时比较tail是否发生变化,如果否,则允许入队,反之,本次入队失败。出队则是获取head位置,进行cas。
排序
1、快速排序优化点
例如随机选取pivot节点
或者在中间、开头、最后取三个值,取其中的mid以便进行快速划分避免遇到n^2情况
2、特定情况下存在着时间效率为O(n)的排序方法的:桶排序
如果要排序的数据有 n 个,我们把它们均匀地划分到 m 个桶内,每个桶里就有 k=n/m 个元素。每个桶内部使用快速排序,时间复杂度为 O(k * logk)。m 个桶排序的时间复杂度就是 O(m * k * logk),因为 k=n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是一个非常小的常量,这个时候桶排序的时间复杂度接近 O(n)。
3、递归的设计实际上关乎计算机的组成架构
递归可能出现问题,因为栈空间有限,而每次递归的变量都会保存在栈空间上(例如变量的值、变量的长度等)
如果人工在堆上手动模拟进出栈就不会遇到这个问题,因为这时候定义的内容储存在具有更大内存空间的堆上
4、关于排序算法设计的原理
Qsort会根据情况自动选择使用插入排序、归并排序还是快速排序
归并排序:稳定nlogn,但浪费空间
另外n^2数据在小数据不一定比Onlogn好
因此在数据长度不同的情况下排序算法的实现机理会发生变化
跳表
跳表的原理:对不支持随机访问的链表增加索引以便支持快速访问
时间复杂度:O(logn)
对比红黑树的优势:支持快速访问区间内数据
空间复杂度为n,需要浪费一些空间来构造索引
删除也有一定学问
散列表
散列处理方法
1、开放寻址法(一个冲突了就就往后走)
便于序列化
2、双重散列法(一个哈希函数冲突就用第二个)
3、链表法
更适合大数据,可以用红黑树替代对应的链表
散列表的查询效率并不能笼统地说成是 O(1)。它跟散列函数、装载因子、散列冲突等都有关系
迁移数据都不能直接进行迁移,可以创建一个对应的内存空间,然后每次查找时将查找对应的全部内容复制到对应的新列表里,摊还成本,避免出现连锁问题(与Redis的扩容原理一致,类似写时复制的原理,一步到位)
散列表和跳表通常合并使用
散列表解决无法快速随机访问的问题,链表解决删除更新的问题
工业界通常使用二叉树而非散列表的原因:
1、散列表中的数据都处于无序状态,输出有序数据需要进行排序,而二叉查找树可通过中序遍历直接看到对应的结果
2、散列表扩容耗时久,而且遇到散列冲突时性能不稳定
3、因为哈希冲突,常量不一定比logn小,另外哈希函数本身也需要耗时
4、散列表设计更复杂