【Algorithm&DataStructure】极客时间-数据结构与算法之美专栏笔记I

以下内容均来自本人学习专栏时的个人笔记、总结,侵权即删

专栏地址:https://time.geekbang.org/column/126

希望看到本文章的,可以去支持一下老师,讲的很好!!


 

目录

时间复杂度为O(n)=logn的代码

没有头结点要多判断什么?-->哨兵结点作用

数组和链表的区别

容器(ArrayList)和数组的选择

 

队列

阻塞队列 

并发队列

队列的应用场景和实现方式选择

 

递归

递归需要满足的三个条件

如何编写递归

递归注意事项

警惕堆栈溢出(空间复杂度高)

警惕重复计算

 

排序

O(n^2)的排序(基于比较)

冒泡排序

插入排序

选择排序

O(nlogn)的排序(基于比较)

归并排序(分治思想=大问题化成小问题)(递归编程技巧)

快排

O(n)的排序(非基于比较,对数据要求苛刻,复杂度n-》线性排序)

桶排序

计数排序(桶排序的一种特殊情况)

基数排序(排序低位-->排序高位)

 

二分查找(O(logn))

二分查找变体

思考题1(LeetCode33)

思考题2(求一个数平方根,小数点精确到后六位)

 

跳表(区间查询)[链表中的二分查找]{Redis-->散列表+跳表}

|S:O(logn) K:O(n)|

跳表索引的动态更新:

为什么Redis用跳表不用红黑树

 

散列表(高效的CRUD)

核心

散列表哈希冲突的解决方法

开放寻址法(不允许在同一个结点)

链表法(允许在同一个结点)

如何设计一个散列表

为什么散列表和链表经常一块使用(顺序遍历)

 

哈希算法的分布式应用

负载均衡

数据分片

分布式存储

 


 

 

 

 

 

时间复杂度为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;
		}
	}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值