最近在学习算法中,看的视频是Liuyubobobo视频课,感觉看了视频之后还是得自己敲一下代码才行。部分笔记,图片都是源于视频课,部分笔记与代码源于leetcode cookbook。
排序算法
O ( n 2 ) O(n^2) O(n2)的算法在某些情况下会更适用
1.选择排序
O ( n 2 ) O(n^2) O(n2)
for i=0:n{
寻找[i,n)区间里的最小值
}
2.插入排序
O ( n 2 ) O(n^2) O(n2)
类似于打扑克牌时的排序,跟左边的所有牌挨个比较
改进:在遍历之后不是马上进行插入,将当前元素copy一个副本,找到其合适的插入位置,而不是一直与前一个元素进行交换操作(调用swap函数)。——在近乎有序的情况下性能很好。
对小数组进行插入排序
3.冒泡排序
双循环,每轮确定一个数的位置
4.希尔排序
5.归并排序
Mergesort
自顶向下
Merge-sort()
- 分解:将序列分成 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor ⌊n/2⌋长度的2个子序列 Merge-sort()
- 解决:用归并排序递归地排序子序列 Merge-sort()
- 合并:合并两个已排序的子序列得到答案 Merge()
Merge函数(考虑数组越界):加上两个哨兵牌∞ / 加左右两个哨兵left与right
改进:
- 在merge中,如果左边序列已经大于右边(即左边最后一位与右边第一位):加入判断
- 如果不断分割序列后的长度到定值时,用插入排序进行排序。
自底向上
双循环 0~sz-1, 1~2sz-1, 2~3sz-1
sz = 1 :merge([0] [1]) ; merge([2] [3] ) #先是每两个元素进行排序
sz = 2 :merge([0,1] [2,3 ])
sz = 4 :merge([0,1,2,3])
考虑到数组出界的问题:
代码
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
func merge(arr []int, l int, mid int, r int) {
// 经测试,传递aux数组的性能效果并不好
copy := make([]int, r+1)
for i := l; i <= r; i++ {
copy[i] = arr[i]
}
i, j := l, mid+1
// 判断边界条件
for k := l; k <= r; k++ {
if i > mid {
arr[k] = copy[j]
j++
} else if j > r {
arr[k] = copy[i]
i++
} else if copy[i] < copy[j] {
arr[k] = copy[i]
i++
} else {
arr[k] = copy[j]
j++
}
}
}
func mergesort(arr []int, l int, r int) {
if l >= r {
return
}
mid := (l + r) / 2
mergesort(arr, l, mid)
mergesort(arr, mid+1, r)
merge(arr, l, mid, r)
}
6.快速排序
快速排序是在原地排序,不需要合并操作,这点可以与归并排序进行比较一下
Quicksort
分治思想
关键是Partition过程,主元可以选择首个元素或末尾元素
分成两部分<v , >=v
[tip: 一定长度的子数组,用插入排序]
改进:随机快速排序 Quicksort1
Quicksort 对近乎有序的数组快速排序性能很差——(选择首个元素时)只有右子树,退化为
O
(
n
2
)
O(n^2)
O(n2)
**改进:**随机选择一个元素作为主元
改进:双路快速排序 Quicksort2
当=v的元素较多时,序列会被分成很不平衡的两部分——退化为
O
(
n
2
)
O(n^2)
O(n2)
改进:两个扫描方向,分别从两端往中间,直到满足交换条件(左边指针i的值>v,右边<v)
改进:三路快速排序 Quicksort3
三路快速排序法,增加了一路 ==v
代码
// 参数传递应该是地址传递?
// slice本身就是引用类型,相当于引用传递了
//快速排序,一定长度的子数组,用插入排序
func partition(nums []int, l int, r int) int {
v := nums[l]
// <=v: nums[l+1,j], >v: nums[j+1,i-1]
j := l
for i := l + 1; i <= r; i++ {
if nums[i] < v {
j++
nums[i], nums[j] = nums[j], nums[i]
}
}
nums[j], nums[l] = nums[l], nums[j]
return j
}
func quicksort(nums []int, l int, r int) {
if l >= r {
return
}
// nums[l,p-1],nums[p+1,r]
p := partition(nums, l, r)
quicksort(nums, l, p-1)
quicksort(nums, p+1, r) //为什么是p+1?,p位置元素就是主元
}
-----------------------------------------------------------
func partition1(nums []int, l int, r int) int {
//不初始化Seed的话,生成的是伪随机数,每次运行程序都是一样的(主函数中加入初始化)
n := rand.Intn(len(nums))
fmt.Println(n)
nums[l], nums[n] = nums[n], nums[l]
v := nums[l]
// <=v: nums[l+1,j], >v: nums[j+1,i-1]
j := l
for i := l + 1; i <= r; i++ {
if nums[i] < v {
j++
nums[i], nums[j] = nums[j], nums[i]
}
}
nums[j], nums[l] = nums[l], nums[j]
return j
}
func quicksort1(nums []int, l int, r int) {
if l >= r {
return
}
// nums[l,p-1],nums[p+1,r]
p := partition1(nums, l, r)
quicksort1(nums, l, p-1)
quicksort1(nums, p+1, r) //为什么是p+1?,p位置元素就是主元
}
func main() {
// 我们一般使用系统时间的不确定性来进行初始化
rand.Seed(time.Now().Unix())
nums := []int{1, 6, 5, 8, 6, 8, 9, 4, 4, 5, 10}
quicksort1(nums, 0, len(nums)-1)
fmt.Println(nums)
}
-----------------------------------------------------------
// 改进2:从两端开始扫描
func partition2(nums []int, l int, r int) int {
v := nums[l]
i, j := l+1, len(nums)-1
// nums[l+1,i-1] <v , nums[j,r] >v
for true {
for i <= r && nums[i] < v {
i++
}
for j >= l+1 && nums[j] > v {
j--
}
if i > j {
break
}
nums[i], nums[j] = nums[j], nums[i]
i++
j--
}
nums[l], nums[j] = nums[j], nums[l]
return j
}
func quicksort2(nums []int, l int, r int) {
if l >= r {
return
}
// i从左边开始扫描,j从右边
p := partition2(nums, l, r)
quicksort2(nums, l, p-1)
quicksort2(nums, p+1, r)
}
-----------------------------------------------------------
//改进3:三路快排法
func partition3(nums []int, l int, r int) (int, int) {
v := nums[l]
lt, gt := l, r+1
i := l + 1
// nums[l+1,lt] <v , nums[lt+1,i)==v, nums[gt,r] >v
for i < gt {
if nums[i] < v {
nums[i], nums[lt+1] = nums[lt+1], nums[i]
i++
lt++
} else if nums[i] > v {
nums[i], nums[gt-1] = nums[gt-1], nums[i]
// i++
//gt-1未知,所以i不往后挪
gt--
} else {
i++
}
}
nums[l], nums[lt] = nums[lt], nums[l]
return lt, gt
}
func quicksort3(nums []int, l int, r int) {
if l >= r {
return
}
// i从左边开始扫描,j从右边
p1, p2 := partition3(nums, l, r)
quicksort3(nums, l, p1-1)
quicksort3(nums, p2, r)
}
归并与快速排序衍生问题
分治算法:
堆排序
堆
堆的实际应用不是排序,常用于实现优先队列。
优先队列的实现:普通数组 / 顺序数组 / 堆
二叉堆的性质:
- 是完全二叉树:父节点,左节点,右节点的下标有一定关系
- 大顶堆 / 小顶堆
用数组存储二叉堆(0处是否设为空):
Heap sort
堆的基本操作:添加元素 / 取元素
- 向堆添加元素(siftup函数):(不断上浮操作:与父节点比较大小)
- 从堆中取元素,调整堆(siftdown函数): 堆顶和堆的末尾元素进行交换,交换后删除尾节点,然后再重新对堆进行调整,(不断下沉操作,与子节点进行比较)
堆上的2种操作:heapify / replace
- replace:取出最大元素后,放入一个新元素
实现1:先extractMax 再add,两次 O ( l o g n ) O(logn) O(logn)
**实现2:**直接将堆顶元素替换为新元素,再用siftdown,一次 O ( l o g n ) O(logn) O(logn)
- heapify:把数组整理成堆的样子
如何找到最后一个非叶子节点:先找到最后一个叶子节点(n-1),再找其父节点
** 实现:**从倒数第一个非叶子节点往上siftup操作,再往下siftdown操作,算法复杂度是 O ( n ) O(n) O(n)
Init实现:如果是将n个元素逐个插入到一个空堆中,算法复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)
改进1:原地堆排序
在原地将数组整理成有序数组,而不用一直pop操作来获得有序操作。
思路:
如何在原地进行排序:把原始数组直接看成一个堆
-
通过heapify操作,将数组构建成最大堆(此时,数组首元素v为最大值,应该去数组的末端)
-
将当前最大值v与末尾元素w进行调换
-
调换后,此时橙色部分不为最大堆,需要将w进行siftdown操作,从而使当前部分成为最大堆。
-
重复以上操作,将当前最大堆的首元素与堆的末尾元素进行调换,v去到了数组倒数第二位。
-
repeat…
代码
数组排序的拓展应用
归并思想求逆序对
归并过程中进行计数
代码:
// 归并算法的思想来求逆序对
func revers(arr []int, l int, r int) {
if l >= r {
return
}
mid := (l + r) / 2
revers(arr, l, mid)
revers(arr, mid+1, r)
merge(arr, l, mid, r) //归并过程中进行计数
}
// 将arr[l...mid]和arr[mid+1...r]两部分进行归并
func merge(arr []int, l int, mid int, r int) {
// 经测试,传递aux数组的性能效果并不好
copy := make([]int, r+1)
for i := l; i <= r; i++ {
copy[i] = arr[i]
}
i, j := l, mid+1
// 判断边界条件
for k := l; k <= r; k++ {
if i > mid {
arr[k] = copy[j]
j++
} else if j > r {
arr[k] = copy[i]
i++
} else if copy[i] < copy[j] {
arr[k] = copy[i]
i++
} else {
arr[k] = copy[j]
j++
count += (mid - i + 1)
}
// 注意!
// 此时, 因为右半部分j所指的元素小
// 这个元素和左半部分的所有未处理的元素都构成了逆序数对
// 左半部分此时未处理的元素个数为 mid - j + 1
}
}
Top K问题
快排解决Top K问题
思路:首先要清楚快速排序中的partition操作
一次partition操作后,通过主元v的位置下标m可以得到:
1. m=k:取前m个元素
2. m<k:对右侧数组递归地进行partition,找前n-k个元素
3. m>k:对左侧数组递归地进行partiion,找前k个元素
堆排序解决Top K 问题
如果要求取的是最小的k个数,则用最大堆来维护这k个数(pop出最大的前n-k个数后,留下的就是答案)。
Top K问题的方法对比
Top K问题有两种不同的解法,一种解法使用堆(优先队列),另一种解法使用类似快速排序的分治法。
在面试中,另一个常常问的问题就是这两种方法有何优劣。看起来分治法的快速选择算法的时间、空间复杂度都优于使用堆的方法,但是要注意到快速选择算法的几点局限性:
第一,算法需要修改原数组,如果原数组不能修改的话,还需要拷贝一份数组,空间复杂度就上去了。
第二,算法需要保存所有的数据。如果把数据看成输入流的话,使用堆的方法是来一个处理一个,不需要保存数据,只需要保存 k 个元素的最大堆。而快速选择的方法需要先保存下来所有的数据,再运行算法。当数据量非常大的时候,甚至内存都放不下的时候,就麻烦了。所以当数据量大的时候还是用基于堆的方法比较好。
代码
剑指offer40题
解法一(最大堆):
type Intheap []int
func (h Intheap) Len() int {
return len(h)
}
func (h Intheap) Less(i, j int) bool {
return h[i] > h[j]
}
func (h Intheap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
}
func (h *Intheap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *Intheap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[:n-1]
return x
}
func getLeastNumbers(arr []int, k int) []int {
h := &Intheap{}
for _, v := range arr {
heap.Push(h, v)
}
for i := 0; i < len(arr)-k; i++ {
heap.Pop(h)
}
return *h
}
解法二(快排思想):
func getLeastNumbers(arr []int, k int) []int {
if k==0{
return nil
}
partionarr(arr, 0, len(arr)-1, k)
return arr[0 : k]
}
//递归思想
func partionarr(arr []int, l, r int, k int) {
p := partition(arr, l, r)
if p == k-1 { //需要考虑k=0时
return
} else if p > k-1 {
partionarr(arr, l, p-1, k)
} else {
partionarr(arr, p+1, r, k)
}
}
// 快排忘记了
//[l+1,j] <=v, [j+1,r]>v
func partition(arr []int, l, r int) int {
v := arr[l]
j := l
for i := l + 1; i <= r; i++ {
if arr[i] < v {
arr[i], arr[j+1] = arr[j+1], arr[i]
j++
}
}
arr[j], arr[l] = arr[l], arr[j]
return j
}
参考:https://leetcode.cn/problems/zui-xiao-de-kge-shu-lcof/solution/tu-jie-top-k-wen-ti-de-liang-chong-jie-fa-you-lie-/