以下内容均来自本人学习专栏时的个人笔记、总结,侵权即删
专栏地址:https://time.geekbang.org/column/126
希望看到本文章的,可以去支持一下老师,讲的很好!!
目录
O(n)的排序(非基于比较,对数据要求苛刻,复杂度n-》线性排序)
跳表(区间查询)[链表中的二分查找]{Redis-->散列表+跳表}
时间复杂度为O(n)=logn的代码
i = 1;
while(n<i){
i = i*2;
}
变量 i 的取值就是一个等比数列。如果我把它一个一个列出来,就应该是这个样子的:
所以,只需要知道x的值,就可以知道这段代码执行的次数了,也就是log2n
而对于 i = i × x 的情况(x是一个常数,可以想成3),也可以得知时间复杂度是logxn
而所有对数阶的时间复杂度一般都表示为logn,因为可以通过换底公式,logxn = logx2 × log2n(x是一个常数)
平均时间复杂度=单一情况发生的概率 × 这种情况的时间复杂度
--->>>均摊时间复杂度(思维角度):O(1)->O(1)->O(1)->O(1)->...n次...->O(n) :执行n次O(1)后会有一次O(n)的操作
可以将O(n)分成n次均摊到每个O(1)的操作,这样算下来整个代码的平均时间复杂度也就是O(1)了
没有头结点要多判断什么?-->哨兵结点作用
/*
* 没有头结点的插入、删除
*/
//一般插入结点
new_node->next = p->next;
p->next = new_node;
//空链表第一个结点
if (head == null) {
head = new_node;
}
//删除结点
p->next = p->next->next;
//空链表最后一个结点删除
if (head->next == null) {
head = null;
}
如果有头结点(不存数据的结点),不管有没有结点都可以使用同一个逻辑了,不用再根据特殊情况来判断
数组和链表的区别
这里我要特别纠正一个“错误”。我在面试的时候,常常会问数组和链表的区别,很多人都回答说,“链表适合插入、删除,时间复杂度 O(1);数组适合查找,查找时间复杂度为 O(1)”。实际上,这种表述是不准确的。数组是适合查找操作,但是查找的时间复杂度并不为 O(1)。即便是排好序的数组,你用二分查找,时间复杂度也是 O(logn)。所以,正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为 O(1)。
容器(ArrayList)和数组的选择
我个人觉得,ArrayList 最大的优势就是可以将很多数组操作的细节封装起来。比如前面提到的数组插入、删除数据时需要移其他数据等。另外,它还有一个优势,就是支持动态扩容。
- Java ArrayList 无法存储基本类型,比如 int、long,需要封装为 Integer、Long 类,而 Autoboxing、Unboxing 则有一定的性能消耗,所以如果特别关注性能,或者希望使用基本类型,就可以选用数组。
- 如果数据大小事先已知,并且对数据的操作非常简单,用不到 ArrayList 提供的大部分方法,也可以直接使用数组。
- 还有一个是我个人的喜好,当要表示多维数组时,用数组往往会更加直观。比如 Object[][] array;而用容器的话则需要这样定义:ArrayList<ArrayList> array
队列
阻塞队列
其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
你应该已经发现了,上述的定义就是一个“生产者 - 消费者模型”!是的,我们可以使用阻塞队列,轻松实现一个“生产者 - 消费者模型”!
并发队列
前面我们讲了阻塞队列,在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,那如何实现一个线程安全的队列呢?
最简单直接的实现方式是直接在 enqueue()、dequeue() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。实际上,基于数组的循环队列,利用 CAS 原子操作,可以实现非常高效的并发队列。这也是循环队列比链式队列应用更加广泛的原因。在实战篇讲 Disruptor 的时候,我会再详细讲并发队列的应用。
队列的应用场景和实现方式选择
队列的知识就讲完了,我们现在回过来看下开篇的问题。线程池没有空闲线程时,新的任务请求线程资源时,线程池该如何处理?各种处理策略又是如何实现的呢?我们一般有两种处理策略。
第一种是非阻塞的处理方式,直接拒绝任务请求;另一种是阻塞的处理方式,将请求排队,等到有空闲线程时,取出排队的请求继续处理。那如何存储排队的请求呢?我们希望公平地处理每个排队的请求,先进者先服务,所以队列这种数据结构很适合来存储排队请求。我们前面说过,队列有基于链表和基于数组这两种实现方式。
这两种实现方式对于排队请求又有什么区别呢?基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue),但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的。而基于数组实现的有界队列(bounded queue),队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说,就相对更加合理。不过,设置一个合理的队列大小,也是非常有讲究的。队列太大导致等待的请求太多,队列太小会导致无法充分利用系统资源、发挥最大性能。实际上,对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。
递归
递归需要满足的三个条件
- 一个问题的解可以分解为几个子问题的解
- 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
- 存在递归终止条件
如何编写递归
写出递归公式,找到终止条件,将两者转换成代码
递归注意事项
警惕堆栈溢出(空间复杂度高)
递归调用一次就会在内存栈中保存一次现场数据,所以在分析递归复杂度的时候要考虑这个部分,同时也要考虑递归层数过深导致的堆栈溢出。
解决方法:
- 方法①:在比如说递归深度超过1000层的时候就抛出异常,不再进行递归(规模小的时候适用,因为实时计算栈的剩余空间过于复杂)
- 方法②:自己模拟一个栈,用非递归代码实现
警惕重复计算
排序
O(n^2)的排序(基于比较)
冒泡排序
/**
* 冒泡排序
* @param a
*/
private static void bubbleSortLineryArray(int[] a) {
for(int i=0;i<a.length;i++) {
boolean flag = false; //标记一轮冒泡中是否有交换数据,没有就直接break
for(int j=0;j<a.length-i-1;j++) {
if(a[j] > a[j+1]) {
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
}
if(!flag)break;
}
}