总述 在给定的集合、多重集(允许多个元素具有相同的值)中找给定值
(查找键,search key )
顺序搜索
折半查找:效率高但应用受限
将原集合用另一种形式表示以方便查找
评价 没有任何一种查找算法在任何情况下都是最优的
有些算法速度快,但是需要较多存储空间
有些算法速度快,但是只适合有序数组
查找算法没有稳定性问题,但会发生其他问题
无序线性表查找 顺序查找 算法
将给定列表中连续元素和给定元素查找键进行比较
直到遇到匹配元素:成功查找
匹配之前遍历完整个列表:失败查找
1 2 3 4 5 6 7 8 9 10 11 12 SequentialSearch(A[0. .n-1 ], K) A[n] = K i = 0 while A[i] != K do i = i + 1 if i < n return i else return -1
改进
将查找键添加找列表末尾,查找一定会成功,循环时将不必每次
检查是否到列表末尾
如果给定数组有序:遇到等于(查找成功)、大于(查找失败)
查找键元素,算法即可停止
二叉查找树 算法
特点
预排序查找 对线性表预排序,有序表中查找速度快得多
算法 1 2 3 4 5 6 7 PreorderSearch(A[0. .n-1 ]) 对B[(数组元素, 索引)]进行预排序 使用折半查找寻找二元组 返回二元组中索引
特点
预排序检验唯一性 1 2 3 4 5 6 7 8 9 PresortElementUniqueness(A[0. .n-1 ]) 对数组排序 for i=0 to n-2 do if A[i] = A[i+1 ] return false return true
预排序求众数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PresortMode(A[0. .n-1 ]) 对数组A排序 i = 0 modefrequency = 0 while i <= n-1 do runlength = 1 runvalue = A[i] while i + runlength <= n-1 and A[i+runlength] == runvalue runlength = runlength+1 if runlength > modefrequency modefrequency = runlength modevalue = runvalue i = i+runlength return modevalue
有序顺序表查找
折半查找/二分查找 二分查找:利用数组的有序性,通过对查找区间中间元素进行判断,
缩小查找区间至一般大小
基本版 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 BinarySearchEndReturnV1(nums[0. .n-1 ], target): // 非递归折半查找基本版,不直接返回,方便嵌入 // 输入:升序数组nums[0. .n-1 ]、查找键target // 输出:存在返回数组元素下标m,否则返回-1 left, right = 0 , n-1 while left <= right: mid = (left + right) // 2 if nums[mid] == target: break elif nums[mid] < target: left = mid+1 else : right = mid-1 else : assert (mid == right == left-1 ) if nums[mid] == target: return mid else : return -1
输入判断:无需判断输入列表是否为空
初始条件:left, right = 0, n-1
左、右均未检查、需检查
即代码中查找区间为闭区间 $[left, right]$,此时
搜索区间非空
中点选择:mid = (left + right) // 2
向下取整
对此版本无影响,left
、right
总是移动,不会死循环
循环条件:left <= right
搜索区间为闭区间,则相应检查条件为left <= right
,
否则有元素未被检查
在循环内left
、right
可能重合
更新方式:left=mid+1, right=mid-1
终止情况:right=left-1
、mid=left/right
循环条件、循环内部+1
保证:上轮循环left==right
越界终止情况:左、右指针均剔除mid
,两侧均可能越界
mid=right=n-1, left=n
mid=left=0, right=-1
target
位置
若存在target
:必然在mid
处,循环结束只需判断mid
若不存在target
target
位于[right, left]
间
left=mid+1=right+1
表示小于target
元素数量
需要和左右邻居(查找结束后) 、left
、right
比较
情况下,容易考虑失误,不适合扩展使用
高级版1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 BinarySearchV2(nums[0. .n-1 ], target): left, right = 0 , n while left < right: mid = (left + right) if nums[mid] == target: break elif nums[mid] < target: left = mid+1 else : right = mid else : assert(mid-1 == left == right) if nums[mid] == target: return mid else : return -1
输入判断:无需判断输入列表是否为空
初始条件:left, right = 0, n
左未检查、需检查,右无需检查
即代码中查找区间为左闭右开区间 $[left, right)$
中点选择:mid = (left + right) // 2
向下取整
此处必须选择向下取整,否则left=right-1
时进入死循环
即:循环内检查所有元素,取整一侧指针必须移动
循环条件:left < right
搜索区间为左闭右开区间,则检查条件为left < right
,
此时搜索区间非空
在循环内left
、right
不会重合
更新方式:left=mid+1, right=mid
为保证搜索区间始终为左闭右开区间
移动左指针时需剔除mid
移动右指针时无需剔除mid
终止情况:right=left
、mid=left/left-1
循环条件、循环内部+1
保证:上轮循环
left+1=mid=right-1
left=mid=right-1
越界终止情况:仅左指针剔除mid
,仅可能右侧越界
target
位置
若存在target
:必然在mid
处,循环结束只需判断mid
若不存在target
left=mid=right
:循环过程中target
可能已不在
搜索区间中,最终位于(mid-1, mid)
mid+1=left=right
:(mid, left)
left
表示小于target
元素数量
高级版2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 BainarySearchEndReturnV3(nums[0. .n-1 ], target): left, right = -1 , n while left < right: mid = (left + right + 1 ) if nums[mid] == target: break elif nums[mid] < target: right = mid-1 else : left = mid if nums[mid] == target: return mid else : return -1
输入判断:无需判断输入列表是否为空
初始条件:left, right = -1, n-1
左无需检查,右未检查、需检查
即代码中查找区间为左开右闭区间 $(left, right]$
中点选择:mid = (left + right + 1) // 2
向上取整
此处必须选择向上取整,否则left=right-1
时进入死循环
(或放宽循环条件,添加尾判断)
即:循环内检查所有元素,取整一侧指针必须移动
循环条件:left < right
搜索区间为左开右闭区间,则检查条件为left < right
,
此时搜索区间非空
在循环内left
、right
不会重合
更新方式:left=mid, right=mid+1
为保证搜索区间始终为左闭右开区间
移动右指针时需剔除mid
移动左指针时无需剔除mid
终止情况:left=right
、mid=left/left+1
循环条件、循环内部+1
保证:上轮循环
left+1=mid=right-1
left+1=mid=right
越界终止情况:仅右指针剔除mid
,仅可能左侧越界
target
位置
若存在target
:必然在mid
处,循环结束只需判断mid
若不存在target
left=mid=right
:循环过程中target
可能已不在
搜索区间中,最终位于(right, right+1)
mid+1=left=right
:(left, right)
left+1
表示小于target
元素数量
高级版3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 BinarySearchEndReturnV2(nums[0. .n-1 ], target): // 折半查找,保持左右指针顺序、不重合 // 输入:升序数组nums、查找键target // 输出:存在返回数组元素下标m,否则返回-1 left, right = -1 , n while left < right-1 : mid = (left + right) // 2 if nums[mid] == target: break elif nums[mid] < target: left = mid else : right = mid if nums[mid] == target: return mid else : return -1
高级版1-原 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 BinarySearchKeep1(nums[0. .n-1 ], target): if n == 0 : return -1 left, right = 0 , n-1 while left < right - 1 : mid = floor ((left + right) / 2 ) if nums[mid] == target: return mid elif nums[mid] < target: right = mid else : left = mid if nums[left] == target: return left if nums[right] == target: return right return -1
初始条件:left = 0, right = n-1
终止情况 :left = right - 1
指针移动:left = mid, right= mid
关键特点
n>1
时left
、mid
、right
循环内不会重合
mid
可以left
、right
任意比较,无需顾忌重合
需要和左右邻居 、left
、right
比较时适合使用
扩展使用需要进行真后处理,对left
、right
进行判断
终止情况:left = right - 1
循环过程中会保证查找空间至少有3个元素 ,剩下两个
元素时,循环终止
循环终止后,left
、right
不会越界,可以直接检查
target
位置
仅仅二分查找的话 ,后处理其实不能算是真正的后处理
高级版2-原 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 BinarySearchKeep0(nums[0. .n-1 ], target): if n == 0 : return -1 left, right = 0 , n while left < right: mid = floor ((left + right) / 2 ) if nums[mid] == target: return mid elif nums[mid] < target: left = mid + 1 else : right = mid if left != len(nums) and nums[left] == target: return left return -1
初始条件:left = 0, right = n
,保证n-1
可以被正确处理
终止情况 :left = right
指针移动:left = mid + 1, right = mid
中点选择对此有影响,指针移动行为非对称,取ceiling
则
终止情况还有left = right + 1
关键特点
left
、right
循环内不会重合
由于floor
的特性,mid
可以right
任意比较,无需
顾忌和right
重合
需要和右邻居 、right
比较时适合使用
终止情况:left = right
循环过程中会保证查找空间至少有2个元素 ,否则循环
终止
循环终止条件导致退出循环时,可能left=right=n
越界
target
位置
若存在target
,可能位于mid
、left=right
若不存在target
,则target
位于(left-1, left)
适合访问目标在数组中索引、及其直接右邻居
仅二分查找 ,甚至无需后处理
判断nums
长度在某些语言中也可以省略
高级版3-原 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 BinarySearchNoKeep(nums[0. .n-1 ], target): if n == 0 : return -1 left, right = -1 , n-1 while left < right - 1 : mid = floor ((left + right) / 2 ) if nums[mid] == target: return mid elif nums[mid] < target: left = mid else : right = mid - 1 if right >= 0 and nums[right] == target: return right return -1
初始条件:left = -1, right = n-1
终止情况 :left = right
、left = right + 1
指针移动:left = mid, right = mid - 1
中点选择对此有影响,指针移动行为非对称,取ceiling
则
同高级版本2
特点
插值查找 Interpolation Search:查找有序数组,在折半查找的基础上考虑
查找键的值
算法
特点
子串匹配 蛮力字符串匹配 算法
将pattern对齐文本前m个字符,从左向右匹配相应字符
m个字符全部匹配,匹配成功,算法停止
遇到不匹配字符则
模式右移1位,然后从模式首个字符开始重复以上匹配
在n-m位置无法匹配成功,则无足够匹配字符,算法停止
1 2 3 4 5 6 7 8 9 10 11 12 BruteForceStringMatch(T[0. .n-1 ], P[0. .m-1 ]) for i = 0 to m-m do j = 0 while j < m and P[j] = T[i+j] do j = j + 1 if j = m return i return -1
特点
最坏情况下,算法比较次数属于$O(nm)$
即在移动模式之前,算法需要做足m次比较
但是一般在自然语言中,算法平均效率比最差好得多
在随机文本中,有线性效率
Horspool算法 算法从右往左 匹配,在任意位置 匹配不成功时只考虑
同模式最后字符匹配的文本字符c
,确定安全移动距离,在
不会错过匹配子串的情况下移动最长距离
如果c
在模式中出现,则模式移动到其最后c
至同文本中c
匹配
否则移动模式长度m
特别的,如果c
只在模式最后字符出现,则也应该移动m
算法
对给定长度m的模式及文本中用到的字母表,构造移动表t
将模式同文本开始对齐
重复以下过程直至发了了匹配子串或模式达到了文本字符外
从模式最后字符开始 ,比较模式、文本相应字符
若m个字符匹配,停止
遇到不匹配字符c
,若为当前文本中和模式最后匹配字符
对齐的字符,将模式移动t(c)
个字符
1 2 3 4 5 6 7 8 9 10 11 ShiftTable(P[0. .m-1 ]) for i=0 to size-1 do Table[i] = m for j=0 to m-2 do Table[P[j]] = m - 1 - j return Table
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 HorspoolMatching(P[0. .m-1 ], T[0. .n-1 ]) Table = ShiftTable(P[0. .m-1 ]) i = m - 1 while i <= n-1 do k = 0 while k <= m-1 and P[m-1 -k]=T[i-k] do k = k+1 if k == m return i-m+1 else i = i+Table[T[i]] return -1
特点
算法效率
最差情况下模式为相同字符,效率$\in O(nm)$
对随机文本效率$\in O(n)$
输入增强
Boyer-Moore算法
坏符号移动:模式、文本中相应不匹配字符确定 的移动
(不是Horspool中简单根据最后字符确定移动)
好后缀移动:模式、文本匹配的后缀确定 的移动
算法
对给定长度m的模式及文本用到的字母表,构造坏符号移动表t
对给定长度m的模式构造后缀移动表
将模式与文本开始处对齐
重复以下直到找到匹配子串或模式达到文本字符以外
从模式最后字符开始比较模式、文本相应字符
所有m个字符匹配,则停止
若c
是不匹配字符,移动坏符号表、后缀移动表决定的
距离较大者
1 2 3 4 5 BadSymbolShift(P[0. .m-1 ])
特点
KMP算法 算法从左往右 匹配,失败时不回溯指针,利用已经得到的
部分匹配 结果尽可能将模式滑动一段距离,从模式中间next
字符开始比较
算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 KMPShift(P[0. .m-1 ]) i = -1 j = 0 next[0 ] = -1 while j < m if i == -1 or P[j] == P[i] i += 1 j += 1 next[j] = i else i = next[i] return next
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 KMPShiftVal(P[0. .m-1 ]) i = -1 j = 0 next_val[0 ] = -1 while j < m do if i == -1 or P[j] == P[i] i += 1 j += 1 if T[j] != T[i] next_val[j] = i else next_val[j] = next_val[i] else i = next_val[i] return next_val
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 KMPMatching(P[0. .m-1 ], T[0. .n-1 ]) i = 0 j = 0 while i < m and j < n do if i == -1 or P[i] == T[j] i += 1 j += 1 else i = next[i] if i >= m return j - m + 1 else return -1
特点