最近在阅读关于算法书,想把算法在过一遍,毕竟自己不是科班出身,网上有人说我们java攻城狮不懂算法一样可以写代码,我个人的观点是你要想当码农,是可以不需要懂底层算法,要是你想研究底层,看底层源码实现,算法是必须要懂的,没见过那个真正的大佬是不懂算法的,懂算法能加深自己对底层的理解,纯属个人观点.
关于算法的标准:大O表示法
算法复杂度大体分类
O(1) 最优秀的算法 不论处理多少数据都是常数级时间
O(logN)
O(N)
O(N²)
一.简单算法 O(N²)
先从最简单的算法说起,也是我们最熟悉的算法,他们的复杂度都是O(N²),说他们简单是因为好理解,而且你完全可以很快的手敲出一个他们的实现
1.冒泡排序
原理:举个例子来说 在一个有十个数字的数组中,拿到左边第一个数字开始和相邻右边的数字开始比较,如果比右边大则换位,如果右边的比较大,则拿右边数字和下一个做比较,每次都能把最大的一个移动到最右边.
2.顺序排序
原理:从左边开始标记第一个数字,和后面的数字做比较,发现比之前标记小的数字则标记它,继续拿后面的数字和他作比较,知道全部比较一遍,找到最小的数字,并且将最小的数字放到最左边,然后在从第二个数字开始反复循环.
比较:相对于冒泡,顺序排序减少了数字换位的次数,每循环一次只有一个元素被换位置.
3.插入排序
原理:拿出一个数字,找到比这个数字n-1位置上大,比n位置小的位置,将这个数字插入到n-1位置后面,n位置以后的数字都向后移动.
这三种排序插入排序效率都是最高的,但是他们的复杂度都是O(N²).在算法中复杂度是O(N²)就是很差的算法,但是在数据量很少的情况下有时候这三种算法反而更加适合.
二.关于归并排序和递归
1.归并排序
归并排序的复杂度,用大O表示法来表示是:O(N*logN).
一句话总结的话,就是把一个麻烦的问题分解,而且是一层一层的分解,分解若干个小碎片,从小的碎片开始逐层解决,解决完成一层后,在返回到上一层,依次类推,其实这也是算法比较常用的解决问题的思路,举个例子吧,如下面这个数组,使用归并排序实现图:
在归并算法实现中使用了递归来处理,但是实际的过程中递归的实际效率并不是很高.
归并排序能做到复杂度到O(N*logN),也已经算是比较优秀的算法了,但是和后面同样是O(N*logN)算法相比(例如快排),他有一个劣势他在处理时需要附加的内存空间辅助它将这个排序碎片化.
2.关于递归和栈
我这也是想到那写到哪,其实递归的实现是和栈这种数据结构又紧密的联系的,当你调用一个方法的时候,编译器会把这个方法所有的参数及其返回地址(这个方法能够到达的地方)都压入栈里,然后将栈的控制权交给这个方法,当方法返回数据是,这些值开始退栈,参数消失了,并且控制权重新回到地址处.
递归也是利用了栈这种后进先出的原则来实现的.
三.高级排序
1.希尔排序(shell)
希尔排序其实是前面提到的简单排序中插入排序的改进,因为插入排序是从左边开始依次拿数据做比较插入到对应位置的,如果排序已经进行到了最后,前面已经有N个数据了,这时你取到一个最小值,这时你需要把它插入到最左边,这以为着你之前排好的数据都要移动一遍,即:n次,当然这是极端情况,平均下来就是移动 n/2次,有n个元素,所以要移动 n²/2次,用大O表示法,插入排序他的复杂度就是O(N²).
1.1希尔排序的具体实现
希尔排序是通过加大插入排序中元素之间的间隔,并且在有间隔的数据进行插入排序,同时实现数据项能大跨度的移动.
上图是跨度为4时
在这个过程中,可能数组会很大,我们会选择不同的跨度,而跨度的有相对应的计算公式:
最大间隔:h=3*h+1
最小间隔:(h-1)/3
这个h初始值是1.
h最常见的跨度值:1 4 13 40 121 363 1093
当跨度间隔越来越小的时候,排序也就越来越接近最终的结果.
1.2希尔排序的复杂度
刚才说了,希尔排序是插入排序的一种改进,确实在加入跨度值后,希尔排序的复杂度O(N)(3/2)方 -O(N)(7/6)方
这里统一使用O(log²N)
2.快速排序
2.1 关于划分算法
将一个数据根据一个标准值(一般是平均值)划分成两组的操作.
划分是快速排序的基础.
划分算法是两个指针分别冲两端相向而行,直到两个指针相遇为止,划分算法的O(N)
2.2 快速排序
毫无疑问,快速排序是目前最流行的排序算法,大多数情况下快速排序都是最快的,他的时间复杂度是:O(N*logN).
快速排序的本质,是通过划分将数据划分成两个数组,在通过两个数组自己调用递归,对自己的内部元素进行排序.当然这个实现中还要加一些其他的东西,对算法本身进行加工.
2.3 选择按钮
按钮的选择:
1.在数据中选择一个具体的数据做按钮.
2.可以任意选择一个数据项做按钮,一般为了简便,都是选择数组最右边的>元素.
3.要保证按钮左边的元素都小于该元素,元素右边都大于该元素,也就是说按钮放入的位置就是他最终的位置.
这里涉及到一个问题,你选择一个按钮,然后把它插入到数组内某个位置时,需要删除他原来所在的位置的元素,并且将在他插入位置右边的数据都移动一遍.这个是没有必要的,常见做法是:36和处在第四个位置上的63交换一下位置.
快排就是利用了按钮的位置就是其最终位置的特性,当遇到较长数组时,可以将数组多次分割成小的数组,每次分割的过程中都会产生一个按钮.但是这样也是有弊端的,因为理想状态下按钮是正好能够将数组分割成对等的两个更小的数组,但是实际情况并非如此,有些按钮将数组分割的过度不均匀时,极端情况就是每次选的按钮都是将数组分成了,1和N-1这个两个数组,这样就会使快排的算法复杂度从原来的:
O(N*logN) => O(N²)
这样还带来了一个潜在的问题,随着算法复杂度的上升,也增加了递归运算的次数,相对应的栈的压力也会变大,有溢出的风险.
综上所述,可以看出快速排序中按钮的选择尤为重要,比较常用的方式是,从数组两端和数组中间分别拿到三个数字,取他们的中间值做按钮.这样有效的避免了取到最大值,或者最小值,做按钮的可能性.同时当面对较小的数组的时候,可以使用插入排序.
四.关于二叉树
1.关于二叉树我的一点理解
对于二叉树这种结构来说,作为一个程序员一定不陌生,他在特定的场景下有效的结合了,数组和链表的优点,这里稍微说一下数组和链表:
数组:查询,修改快,但是它的插入和删除性能相对比较的底,因为不管是新增还是删除产生的位置变动都需要移动后面的对象,这是多余的性能开销.
链表:插入删除快,查找修改慢,链表当你需要插入一个元素的时候,只需要将插入新的元素指向,插入点前一个元素,并且让插入点后一个元素指向自己就可以,删除同理,但是它想要查找一个元素时,就需要将整个链表遍历才能找到这个元素.
二叉树的作用就是集合这两个数据结构的有点而诞生的;
总体上来说,这种数据结构是将数组和链表这种一维数据结构(都是线性的),提升到了二维,包含层数,左右子节点等参数来定位这个数据的位置,提升一个维度有效的结合二者的优点.这也是数据结构设计的一个解决问题的思路吧,个人的一点感想.
2.二叉树的时间复杂度
二叉树:查找,插入,删除等操作
时间复杂度:O(logN)
3.关于二叉树的缺点
二叉树的理想状态是每一个节点都挂着两个子节点,最好每一层都能如此,这样才能发挥二叉树的最佳性能,但是这也是一厢情愿的,如果拿一个排列好的数列使用二叉树去直接插入的话,就会出现最差情况,二叉树就会出现只有单一路径,一直向左或向右,说白了这就使二叉树变成了链表,这样他们的效率就会变成链表,为了避免这种情况后来又出现了会旋转的红黑树,2,3,4树.他们使用了一些特殊的算法去处理以上所说的树不平衡的问题.
五.关于堆
1.堆的实现
堆是使用二叉树实现的,他是一种完全二叉树,看图
他是一种除了最后一层其他层每个节点都挂了两个子节点的书,当时他不同于一般的树的是,它左右两边并不是像二叉树一样,遵循左小右大的原则,也就是说他是一种’弱’关系的树,
2.堆的插入
当删除树的根节点时,堆是这样处理的,层层比较,将较大的节点移动到上一层.
六.哈希表
1.关于哈希(hash)
哈希(hash)是计算机编程中最常见的,他不但速度快,而且也很简单,他是java编程语言最常见,也是最基础的算法(Object.hashcode()),结构(哈希表),他的时间复杂度是O(1),不管数据量有多大,复杂度都是常数级别的,这也是算法中最优秀的.
在Java中通过hash算法直接将一个对象经过hashCode(),算出一个数字,在java中这是一个16进制的数字,他直接表示的是,这个对象在堆中的地址,也就是说你在不重写toString()方法是输出的地址就是hash算出来的.
2.关于哈希的弊端与解决
使用哈希虽然很快,但是有一个最大的问题,他对空间的利用率可能并不高,而且对象相同时计算的结果相同,这些问题就需要一些其他的机制去解决,如果解决不当也会出现内存利用的浪费.
总结
1.数据结构的横向对比
在最近的开发中我就遇到了使用到数据结构问题
List<User> userList = new arrayList<>();
`for(Long userId:userIdList){
User user = userDao.getById(userId);
userList.put(user);
}`
这里当时并不知道会有多少个user信息放进去,debug的时候发现将近有3000人的信息放到userList中,(当然我这是举个例子,现实中不会一次一次掉数据库JDBC每次获取一个user信息,这里一般都要批量处理),后来看了一下ArrayList的源码实现,它的初始化大小是10,当超过最大容量时,他会扩容成原来的1.5倍, 10 –> 3000 每次扩容1.5倍!!,这要扩容多少次!所以速度慢,这里不应该使用ArrayList,应该使用LinkList,即List的基于链表的实现.数组是查找快,链表是新增删除快.当然这也是我举个例子,想必大家可能不会像我一样这么简单的东西都不会用.
2.算法的横向对比
这里算法并不是都要使用性能最好的,每一个算法都有自己试用的范围,例如
当数据量小于1000条时,完全可以使用插入排序,当数据量上升在5000条以内的时候,可以采用希尔排序,当数据量再上升时,可以采用更高级的排序,并且这些排序也都有自己的缺陷,快速排序在不随机的情况下,复杂度也会提升到O(N²).