数据结构与算法
-
时间复杂度
常数操作包括加减乘除,以及从数组中取出一个值(因为直接计算偏移量,是一块连续的区域)
注意:从list中取出一个值不是常数操作,因为需要遍历去找
时间复杂度就是计算存在多少个常数操作
且忽略低阶项,只要高阶项,且忽略高阶项的系数
-
通过亦或完成交换算法
def swap(): a = a ^ b b = a ^ b a = a ^ b
为什么呢?
假设 a = 甲 b = 乙
a = a ^ b a = 甲 ^ 乙
b = a ^b b = 甲乙乙 =甲 (两个相同的数亦或 = 0)(任何一个数亦或0都为它自己)
a = a ^b a = 甲 ^ 乙^甲 = 乙
前提是:a和b内存中的位置不可以相等(数组中相同位置)
不建议使用
-
位运算取出最后一个1
a & (~a + 1)
一个数与上他的取反加一,就提取除了最后一位为1的数
-
排序
- 冒泡排序:相邻位置的数进行比较,大的放到后面,比如,位置1的数与位置2的数比较,位置2的数与位置3的数比较,一轮下来找到最大的数,也就是最右的数,然后开始1-n-2的下一轮比较
- 选择排序:遍历i到n-1找到最小值放入i位置上,i依次++
- 插入排序:拿到一个数,位置为n,与前一个数(n-1)比较,比(n-1)位置上的数大则不交换,因为0-(n-1)一定有序,比(n-1)位置上的数小则交换,直至前面的数大于自己,或前方没有数
-
二分
- 有序数组,是否存在某个数num
- 有序数组,找到大于等于某个数num的最左位置:二分法,这个数的左侧一定是小于num的,右侧一定是等于或大于num的,以此为条件二分
- 无序数组,找到其局部最小:局部最小一定是左侧是向下趋势的,即左侧的数比右侧的大;右侧也是向下趋势的,即右侧的数比左侧的大。先看两边,如果满足条件,再二分
-
归并排序
二分,让左侧的数组与右侧的数组分别有序,再merge左侧数组与右侧数组
merge的逻辑是:开辟一块新的空间存放排好序的help数组。左侧数组的i(i=0;i++)位置的数与右侧数组j(j=0;j++)位置的数比较,谁小就添加到下方help数组中,再i++或j++,再进行比较,直到一方越界,再将剩下的数全部拷贝到help中来。
时间复杂度:O(NlogN)
时间复杂度之所以优化了,是因为没有浪费排序的结果。比较选择排序而言,遍历一次只找到一个最小(或最大)的数,没有充分利用其的比较。
-
归并变形:求小和
求一个数组中,比i小且位置在i左侧的数的和。遍历数组中所有的数的加和,即为小和
也可转换需求,即求右侧比自己大的数的个数,然后乘自己。遍历数组中所有的数再加和,也为小和。
找到右侧比自己大的数的个数,利用归并排序中的merge方法,如果左侧的数比右侧的数小,则小和sum+左侧的数*右侧排好序的个数,右侧排好序的个数通过下标很容易计算出来,然后将左侧的数放入help数组中。如果左侧的数与右侧的数相等,将右侧的数先copy到help数组中。这样才能知道有多少数比左侧的数大。如果左侧的数大,则将右侧数copy到help中,指针右移。
-
荷兰国旗问题
-
问题一:给定一个num,使小于它的数放在左边,大于它的数放到右边,不要求有序。时间复杂度O(n),空间复杂度O(1)
def dutch_flag_reduce(_arr, _num): index = 0 jx = -1 while index < len(_arr): #[index]<=num时,[index]与jx的下一个数交换,jx+1,index继续遍历 if _arr[index] <= _num: swap(_arr, jx + 1, index) jx += 1 #[index]>num时,index继续遍历 index += 1
-
问题二:在问题一的基础上,使小于它的数放在左边,大于它的数放到右边,等于num的数放在中间。
def dutch_flag(_arr, _num): index = 0 left = -1 right = len(_arr) while index < right: #[index]<num时,[index]与left的下一个数交换,left+1,index继续遍历 if _arr[index] < _num: swap(_arr, left + 1, index) left += 1 index += 1 #[index]=num时,index继续遍历 elif _arr[index] == _num: index += 1 #[index]>num时,[index]与right的上一个数交换,right-1,index不变。因为需要继续比较当前index的交换来的新数 elif _arr[index] > _num: swap(_arr, right - 1, index) right -= 1
-
-
快速排序
-
方案一:以数组中最后一个数为num,对前面的数采用荷兰国旗问题一的解法,最后将num与大于区域的第一个交换。然后递归其左侧和右侧。
-
方案二:采用荷兰国旗问题二的解法,再对大于区域和小于区域递归,等于区域不动。对比方案一,少了等于区域那部分的递归,稍微快一些。
-
时间复杂度:如果每次num都正好在数组的中间时,符合master公式,时间复杂度O(nlogN)。
而 如果每次num正好在一侧,相当于一侧的递归范围为0时,符合等差数列,时间复杂度(On^2)
-
-
堆
堆中存在heapsize维护堆的范围
是一个近似完全二叉树的二叉树,每个父节点都大于等于他的子节点,叫做大根堆
当已知一个位置i,他的父节点为(i-1)/2,左子节点为2i+1,右子节点2i+2
大根堆操作:
-
heapInsert:
当用户给我数字时,如何维持大根堆。
与父节点比较,若大于父节点,则交换,继续与当前父节点比较,若大于父节点,则交换,直至数组越界,或不大于了
-
heapify:
去除当前大根堆最大节点,将数组最后一个数放到该父节点上,如何维护大根堆
左孩子与右孩子比较,最大的一个与父节点比较,若大于父节点则与父节点交换。
交换后该节点继续与其左孩子与右孩子比较
-
-
堆排序
-
当那个heapsize为1时,将0、1位置的数heappInsert,heapsize++,接着heapInsert构建出大根堆结构
将最后位置的数与0位置的数交换,然后进行heapify,heapsize--,直至heapsize = 0,排序成功
时间复杂度 O(NlogN) 空间复杂度O(1)
-
反正第一步也是维护大根堆结构,可以用heapify。从数组最后的节点做heapify,依次向前。O(NlogN)
-
-
堆扩展算法
一个几乎有序的数组,几乎有序是指,如果把数组排好序的话,每个元素的移动距离不超过k,且k相对与数组来说比较小
堆排序,对i与i+k之间的数维护成小根堆,放在i位置上,接着i++,对i+1与i+k+1位置上的数维护成小根堆,最后将剩余的小根堆中的数依次停好
-
堆结构
每个编程语言中都有堆结构,其内部结构为数组,堆长度通过heapsize控制。当数组长度不够时,会2n扩容。
编程语言中的堆结构不建议手写修改一个数,更改堆结构。需要更改堆结构时,手写堆。
系统中的堆结构建议是你给它一个数,他给你一个数。
java中只有小根堆,使用比较器改成大根堆
-
计数排序
准备一个辅助数组,他的下标位置上的数代表当前排序数组中存在多少该下标的数,最后将其依次展开。
适合有范围的数据状况,比如年龄排序,辅助数组的范围可以是(0,200)
-
基数排序
排序一组十进制的数,准备10个桶,从个位数依次进桶,比如11进入1号桶,56进入6号桶,36进入6号桶,接着依次倒出,先进先出。接着排序十位,然后百位,倒出来后就已经排好序了。
原理是先排个位数,然后排十位。因为位数越高优先级越高。
-
基数排序代码层面优化
准备一个help数组与原数组长度一致,准备一个原数组位数长度的count数组。
遍历原数组,将其最后一位的词频记录到count数组中,将count数组每一index上的数与前面的数累加
例:原count数组为:0,1,1,4,6。转换为:0,1,2,6,12
含义为原数组个位数<=当前index的数有几个
接着从后遍历原数组,如果数字为22,则看count数组上3位置的数有4个,说明它前面存在3个<=它的数,所以它应该在第4个位置上(help数组),接着4--。继续向前遍历。当前数组遍历完成。相当于对个位数进行了一次入桶。将help数组转移到原数组中,相当于出桶。
然后继续遍历十位上的数。