《数据结构与算法图解》第十章《飞快的递归算法》笔记自存

分区

此处的分区指的是从数组随机选取一个值,以其为轴,将比它小的值放到它左边,比它大的 值放到它右边。

(1) 左指针逐个格子向右移动,当遇到大于或等于轴的值时,就停下来。

(2) 右指针逐个格子向左移动,当遇到小于或等于轴的值时,就停下来。

(3) 将两指针所指的值交换位置。

(4) 重复上述步骤,直至两指针重合,或左指针移到右指针的右边。

(5) 将轴与左指针所指的值交换位置。

class SortableArray 
 attr_reader :array 
 def initialize(array) 
 @array = array 
 end

 def partition!(left_pointer, right_pointer) 

 # 总是取最右的值作为轴
 pivot_position = right_pointer 
 pivot = @array[pivot_position] 

 # 将右指针指向轴左边的一格
 right_pointer -= 1 

 while true do 

 while @array[left_pointer] < pivot do
 left_pointer += 1 
 end

 while @array[right_pointer] > pivot do
 right_pointer -= 1 
 end
 
 if left_pointer >= right_pointer 
 break
 else
 swap(left_pointer, right_pointer) 
 end 
 end

 # 最后将左指针的值与轴交换
 swap(left_pointer, pivot_position) 

 # 根据快速排序的需要,返回左指针
 return left_pointer 
 end

 def swap(pointer_1, pointer_2) 
 temp_value = @array[pointer_1] 
 @array[pointer_1] = @array[pointer_2] 
 @array[pointer_2] = temp_value 
 end 
end

快速排序

(1) 把数组分区。使轴到正确的位置上去。

(2) 对轴左右的两个子数组递归地重复第 1、2 步,也就是说,两个子数组都各自分区,并形 成各自的轴以及由轴分隔的更小的子数组。然后也对这些子数组分区,以此类推。

(3) 当分出的子数组长度为 0 或 1 时,即达到基准情形,无须进一步操作

def quicksort!(left_index, right_index) 
 # 基准情形:分出的子数组长度为 0 或 1 
 if right_index - left_index <= 0 
 return
 end
 # 将数组分成两部分,并返回分隔所用的轴的索引
 pivot_position = partition!(left_index, right_index) 
 # 对轴左侧的部分递归调用 quicksort 
 quicksort!(left_index, pivot_position - 1) 
 # 对轴右侧的部分递归调用 quicksort 
 quicksort!(pivot_position + 1, right_index) 
end

为了搞清楚快速排序的效率,我们先从分区开始。分解来看,你会发现它包含两种步骤。

 比较:每个值都要与轴做比较。

 交换:在适当时候将左右指针所指的两个值交换位置

总步数:N×log N

 

快速选择

如之前所述,分区的作用就是把轴排到正确的格子上。快速选择就利用了这一点。

例如要在一个长度为 8 的数组里,找出第 2 小的值。

先对整个数组分区。

轴很可能落到数组中间某个地方。

现在轴已安放在正确位置了,因为那是第 5 个格子,所以我们掌握了数组第 5 小的值是什么。 虽然我们要找的是第 2 小的值,但刚才的操作足以让我们忽略轴右侧的那些元素,将查找范围缩 小到轴左侧的子数组上。这看起来就像是不断地把查找范围缩小一半的二分查找。

然后,继续对轴左侧的子数组分区。

假设子数组的轴最后落到第 3 个格子上。

现在第 3 个格子的值已经确定了,该值就是数组第 3 小的值,第 2 小的值也就是它左侧的某个元素。于是再对它左侧的元素分区。

这次分区过后,最小和第 2 小的元素也就能确定了。

这么一来,我们就可以拿出第 2 个格子的值,告诉别人找到第 2 小的元素了。快速选择的优势就在于它不需要把整个数组都排序就可以找到正确位置的值。

分析快速选择的效率,你会发现它的平均情况是 O(N)。回想每次分区的步数大约等于作用 数组的元素量,你便可算出,对于一个含有 8 个元素的数组,会有 3 次分区:第一次处理整个数 组的 8 个元素,第二次处理子数组的 4 个元素,还有一次处理更小的子数组的 2 个元素。加起来 就是 8 + 4 + 2 = 14 步。于是 8 个元素大概是 14 步。

如果是 64 个元素,就会是 64 + 32 + 16 + 8 + 4 + 2 = 126 步;如果是 128 个元素,就会是 254 步; 如果是 256 个元素,就会是 510 步。

用公式来表达,就是对于 N 个元素,会有 N + (N / 2) + (N / 4) + (N / 8) + … + 2 步。结果大概 是 2N 步。由于大 O 忽略常数,我们最终会说快速选择的效率为 O(N)。

代码实现:

def quickselect!(kth_lowest_value, left_index, right_index)
 # 当子数组只剩一个格子——即达到基准情形时,
 # 那我们就找到所需的值了
 if right_index - left_index <= 0
 return @array[left_index]
 end
 # 将数组分成两部分,并返回分隔所用的轴的索引
 pivot_position = partition!(left_index, right_index)
 if kth_lowest_value < pivot_position
 quickselect!(kth_lowest_value, left_index, pivot_position - 1)
 elsif kth_lowest_value > pivot_position
 quickselect!(kth_lowest_value, pivot_position + 1, right_index)
 else # 至此 kth_lowest_value 只会等于 pivot_position
# 如果分区后返回的轴的索引等于 kth_lowest_value,
 # 那这个轴就是我们要找的值
 return @array[pivot_position]
 end
end
#想要从一个无序数组中找出第 2 小的值,可以运行如下代码。
array = [0, 50, 20, 10, 60, 30]
sortable_array = SortableArray.new(array)
p sortable_array.quickselect!(1, 0, array.length - 1)
#此方法的第一个参数是查找的位置。因为数组索引从 0 开始算起,所以我们传入 1 来查找第2 小的值。

 总结:

由于运用了递归,快速排序和快速选择可以将棘手的问题解决得既巧妙又高效。这也提醒了 我们,有些看上去很普通的算法,可能是经过反复推敲的高性能解法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值