查找
查找的基本概念
查找是指,在数据结构中寻找满足给定条件的数据元素,也成为检索或搜索
1.查找条件、查找操作和查找结果
查找条件:数据元素(包含关键字key)。
查找操作:比较元素相等,T类的equals(Object)
查找结果:查找成功,查找不成功。
表现形式:
- 如果判断数据结构是否包含某个特定元素,则查找结果为是/否两种状态
- 如果根据关键字查找以期望获得特定元素的其他属性,则查找结果为特定元素
- 如果数据结构中包含多个关键字相同的数据元素,那么,还需要约定是否返回首次出现的元素或者是返回元素集合等。
- 查找不成功,也是查找操作实行完成的一种结果,与没有找含义不同
2.查找是删除、替换等操作的基础
3.查找算法效率
平均查找长度(ASL,Average Search Length) 指的是查找过程中,关键字的平均比较次数,pi是元素的查找概率,ci是查找相应元素需要的比较次数
A
S
L
=
∑
i
=
1
n
(
p
i
∗
c
i
)
ASL=\sum^n_{i=1}(p_i*c_i)
ASL=i=1∑n(pi∗ci)
顺序查找算法
顺序查找算法是最简单的一种查找算法,基于遍历算法,在遍历一个数据结构的过程中,将与key与每个元素进行比较是否相等,确定查找成功或者不成功
int search(T key) //顺序表SeqList<T>
Node<T> search(T key) //SinglyList<T>
DoubleNode<T> search(T key) //CirDoublyList<T>
BinaryNode<T> search(T key) //BinaryTree<T>
TreeNode<T> search(T key) //Tree<T>
算法效率
排序线性表
//排序线性表,覆盖,采用T类的compareTo(T)方法(实现java.lang.Comparable<T>接口)比较对象相等和大小。
int search(T key) //SortedSeqList<T>
Node<T> search(T key) //SortedSinglyList<T>
DoubleNode<T> search(T key) //SortedCirDoublyList<T>
算法效率
提高算法效率的措施
二分法查找算法(Binary Search)
二分法查找适用于数据量较大时,但是数据需要先排好顺序。
主要思想是:(设查找的数组区间为array[low, high])
(1)确定该区间的中间位置K
(2)将查找的值T与array[k]比较。若相等,查找成功返回此位置;否则确定新的查找区域,继续二分查找。区域确定如下:a.array[k]>T 由数组的有序性可知array[k,k+1,……,high]>T;故新的区间为array[low,……,K-1]b.array[k]<T 类似上面查找区间为array[k+1,……,high]。
每一次查找与中间值比较,可以确定是否查找成功,不成功当前查找区间将缩小一半,递归查找即可。时间复杂度为:O(log2n)。
public class SortedArray
{
//已知value数组元素按升序排序,在begin~end范围内,二分法查找关键字为key元素,若查找成功返回下标,否则返回-1;若begin、end越界,返回-1
public static<T extends Comparable<? super T>>
int binarySearch(T[] value, int begin, int end, T key)
public static <T extends Comparable<? super T>>
int binarySearch(T[] value, T key)
}
//二分查找的递归实现
public static int binarySearch(int[] value, int key, int begin, int end)
{
if (begin<=end)
{ int mid = (begin+end)/2;
if (value[mid]==key)
return mid;
if (key < value[mid])
return binarySearch(value, key, begin, mid-1);
return binarySearch(value, key, mid+1, end);
}
return -1;
}
二叉判定树
用二叉判定树来表示查找过程中关键字的比较次序
顺序查找
基于索引顺序表的分块查找
索引与分块查找
索引项
完全索引表
不完全索引
分块查找
查找索引表
在一块中 查找key
散列(Hash)
散列指一种按照关键字编址的存储和查找技术。
Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。
这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
散列表
散列表根据元素的关键字确定元素的存储位置,其查找/插入和删除操作效率接近O(1),至目前查找效率最高的一种数据结构
关键问题是:设计散列函数和处理冲突
散列函数的构造方法
要求1:n个数据仅占用n个地址,虽然散列表是以空间换时间,但是仍然希望散列的地址空间尽量小
要求2:无论用什么方法存储,目的都是尽量均匀地存放元素,以避免冲突
直接定址法
Hash(Key) = a*key+b(a,b为常数)
优点:以关键码key的某个线性函数值为散列地址,不会产生冲突
缺点:要占用连续地址空间,空间效率低,比较浪费空间
除留余数法
Hash(Key) = key mod p(p为整数)
除留余数法
int hash(int key) //散列函数
{ return key % prime; //除留余数法
}
关键在于如何选取合适的p?
技巧:设表长为m,取p≤m且为质数
处理冲突方法
(1)开放定址法,散列表内
-
探测序列是i+1,i+2,…,顺序查找
-
不能删除元素;非同义词冲突,堆积
这个方法的基本思想是:当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。这个过程可用下式描述:
Hash ( key ) = ( Hash ( key )+ d i ) mod m ( i = 1,2,…… , k ( k ≤ m – 1))
其中: H ( key ) 为关键字 key 的直接哈希地址, m 为哈希表的长度, di 为每次再探测时的地址增量。
采用这种方法时,首先计算出元素的直接哈希地址 H ( key ) ,如果该存储单元已被其他元素占用,则继续查看地址为 H ( key ) + d 2 的存储单元,如此重复直至找到某个存储单元为空时,将关键字为 key 的数据元素存放到该单元。增量 d 可以有不同的取法,并根据其取法有不同的称呼:
( 1 ) d i = 1 , 2 , 3 , …… 线性探测再散列;
( 2 ) d i = 1^2 ,- 1^2 , 2^2 ,- 2^2 , k^2, -k^2…… 二次探测再散列;
( 3 ) d i = 伪随机序列 伪随机再散列;例1设有哈希函数 H ( key ) = key mod 7 ,哈希表的地址空间为 0 ~ 6 ,对关键字序列( 32 , 13 , 49 , 55 , 22 , 38 , 21 )按线性探测再散列和二次探测再散列的方法分别构造哈希表。
解:
( 1 )线性探测再散列:
32 % 7 = 4 ; 13 % 7 = 6 ; 49 % 7 = 0 ;
55 % 7 = 6 发生冲突,下一个存储地址( 6 + 1 )% 7 = 0 ,仍然发生冲突,再下一个存储地址:( 6 + 2 )% 7 = 1 未发生冲突,可以存入。
22 % 7 = 1 发生冲突,下一个存储地址是:( 1 + 1 )% 7 = 2 未发生冲突;
38 % 7 = 3 ;
21 % 7 = 0 发生冲突,按照上面方法继续探测直至空间 5 ,不发生冲突,所得到的哈希表对应存储位置:
下标: 0 1 2 3 4 5 6
49 55 22 38 32 21 13
( 2 )二次探测再散列:
下标: 0 1 2 3 4 5 6
49 22 21 38 32 55 13
注意:对于利用开放地址法处理冲突所产生的哈希表中删除一个元素时需要谨慎,不能直接地删除,因为这样将会截断其他具有相同哈希地址的元素的查找地址,所以,通常采用设定一个特殊的标志以示该元素已被删除。
(2)链地址法
-
散列数组
-
同义词单链表
-
链地址法解决冲突的做法是:如果哈希表空间为 0 ~ m - 1 ,设置一个由 m 个指针分量组成的一维数组 ST[ m ], 凡哈希地址为 i 的数据元素都插入到头指针为 ST[ i ] 的链表中。这种方法有点近似于邻接表的基本思想,且这种方法适合于冲突比较严重的情况。
例 2 设有 8 个元素 { a,b,c,d,e,f,g,h } ,采用某种哈希函数得到的地址分别为: {0 , 2 , 4 , 1 , 0 , 8 , 7 , 2} ,当哈希表长度为 10 时,采用链地址法解决冲突的哈希表如下图所示。
(a)散列表满,元素个数=散列表容量×装填因子
(b)散列表扩充容量
二叉排序树
定义
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。
一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
(4)没有键值相等的结点。
查找
步骤:若根结点的关键字值等于查找的关键字,成功。
若key==p.data,则查找成功返回;
若key<p.data,则查找p的左子树;
否则查找p的右子树。
重复执行②,直到p为空,查找不成功。
插入
二叉排序树是一种动态树表。
其特点是:树的结构通常不是一次生成的,而是在查找过程中,当树中不存在关键字等于给定值的结点时再进行插入。新插入的结点一定是一个新添加的叶子结点,并且是查找不成功时查找路径上访问的最后一个结点的左孩子或右孩子结点。
插入40:
删除(三种情况)
在二叉排序树删去一个结点,分三种情况讨论:
- 若p结点为叶子结点,即PL(左子树)和PR(右子树)均为空树。由于删去叶子结点不破坏整棵树的结构,则可以直接删除此子结点。
- 若p结点只有左子树PL或右子树PR,此时只要令PL或PR直接成为其双亲结点f的左子树(当p是左子树)或右子树(当p是右子树)即可,作此修改也不破坏二叉排序树的特性。
-
若p结点的左子树和右子树均不空。在删去p之后,为保持其它元素之间的相对位置不变,可按中序遍历保持有序进行调整,
可以有两种做法:
其一是令p的左子树为f的左/右(依p是f的左子树还是右子树而定)子树,s为p左子树的最右下的结点,而p的右子树为s的右子树;
其二是令p的直接前驱(或直接后继)替代p,然后再从二叉排序树中删去它的直接前驱(或直接后继)-即让f的左子树(如果有的话)成为p左子树的最左下结点(如果有的话),再让f成为*p的左右结点的父结点。
二叉排序树的查找性能分析
排序
插入排序算法
有一个已经有序的数据序列,要求在这个已经排好的数据序列中插入一个数,但要求插入后此数据序列仍然有序,这个时候就要用到一种新的排序方法——插入排序法,插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得到一个新的、个数加一的有序数据。
直接插入排序算法 (最简单的排序法)
直接插入排序的排序思路是:每次将一个待排序的元素与已排序的元素进行逐一比较,直到找到合适的位置按大小插入。
i=1 开始时,有序序列列只有一个元素32,
i=2 26与32比较,26小,26拿出来放在在temp,则32往后移动一位,26放到前面。
i=3 87与32比较,87大,不移动
i=4 72与87比较,72小,72先拿出来放在temp,87往后移动一位,72再与32比较,72大,不移动,
i=5 26与87比较,26小,26放在temp,87往后移,再与72比较,26小,72往后移…知道比前一个元素大或者相等
直接插入排序算法分析
最好情况,已排序{1,2,3,4,5,6},
O
(
n
)
O(n)
O(n)。
最坏情况,反序排列{6,5,4,3,2,1},
O
(
n
2
)
O(n^2)
O(n2) 。
随机排列,
O
(
n
2
)
O(n^2)
O(n2) 。
二分法插入排序(折半)
基本思想
折半查找法的基本思路是:用待插元素的值与当前查找序列的中间元素的值进行比較,以当前查找序列的中间元素为分界,确定待插元素是在当前查找序列的左边还是右边,假设是在其左边。则以该左边序列为当前查找序列。右边也相似。依照上述方法,递归地处理新序列。直到当前查找序列的长度小于1时查找过程结束。
算法分析
-
时间复杂度
折半插入排序适合记录数较多的场景,与直接插入排序相比。折半插入排序在寻找插入位置上面所花的时间大大降低,可是折半插入排序在记录移动次数方面和直接插入排序是一样的,所以其时间复杂度为 O ( n 2 ) O(n^2) O(n2)
其次,折半插入排序的记录比較次数与初始序列无关。由于每趟排序折半寻找插入位置时,折半次数是一定的。折半一次就要比较一次,所以比较次数也是一定的。
-
空间复杂度
同直接插入排序一样,为 O ( 1 ) O(1) O(1)。 -
稳定性
折半插入排序是一种稳定的排序算法。
希尔排序
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,同时该算法是冲破 O ( n 2 ) O(n^2) O(n2)的第一批算法之一。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
分组的直接插入排序。算法描述如下:
①分组,每组元素相隔增量,组内直接插入排序。
②增量,初值为数据序列长度的一半,每趟增量减半,最后值为1。
希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
算法分析
希尔排序算法的时间复杂度和步长的选取有关,
平均时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),
最坏为 O ( n 2 ) O(n^2) O(n2),
最好为 O ( n ) O(n) O(n)).
冒泡排序
冒泡排序是一种简单的排序算法,它也是一种稳定排序算法。其实现原理是重复扫描待排序序列,并比较每一对相邻的元素,当该对元素顺序不正确时进行交换。一直重复这个过程,直到没有任何两个相邻元素可以交换,就表明完成了排序。冒泡排序比较的相邻位置的元素
一般情况下,称某个排序算法稳定,指的是当待排序序列中有相同的元素时,它们的相对位置在排序前后不会发生改变。
冒泡排序算法分析
时间复杂度
最好情况,排序,一趟,比较n次,没有移动,
O
(
n
)
O(n)
O(n)。
最坏情况,随机排列和反序排列,n-1趟,
O
(
n
2
)
O(n^2)
O(n2)
空间复杂度为
O
(
1
)
O(1)
O(1),交换两个元素。
冒泡排序算法稳定。快速排序
快速排序
快速排序(Quicksort)是对冒泡排序算法的一种改进。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
快速排序算法通过多次比较和交换来实现排序,其排序流程如下:
(1)首先设定一个分界值,通过该分界值将数组分成左右两部分。
(2)将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
(3)然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
(4)重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
排序演示
假设一开始序列{xi}是:5,3,7,6,4,1,0,2,9,10,8。(总共11个数)
此时,ref=5(分界值),i=1(low),j=11(high),从后往前找,第一个比5小的数是x8=2,因此序列为:2,3,7,6,4,1,0,5,9,10,8。
此时i=1,j=8,从前往后找,第一个比5大的数是x3=7,因此序列为:2,3,5,6,4,1,0,7,9,10,8。
此时,i=3,j=8,从第8位往前找,第一个比5小的数是x7=0,因此:2,3,0,6,4,1,5,7,9,10,8。
此时,i=3,j=7,从第3位往后找,第一个比5大的数是x4=6,因此:2,3,0,5,4,1,6,7,9,10,8。
此时,i=4,j=7,从第7位往前找,第一个比5小的数是x6=1,因此:2,3,0,1,4,5,6,7,9,10,8。
此时,i=4,j=6,从第4位往后找,直到第6位才有比5大的数,这时,i=j=6,ref成为一条分界线,它之前的数都比它小,之后的数都比它大,对于前后两部分数,可以采用同样的方法来排序。
https://blog.csdn.net/elma_tww/article/details/86164674
算法分析
最好情况,分成长度相近的两个子序列,
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)
最坏情况,分成长度差异很大的两个子序列,
O
(
n
2
)
O(n^2)
O(n2)
快速排序算法不稳定。
快速排序的一次划分算法从两头交替搜索,直到low和hight重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
样的方法来排序。
[外链图片转存中…(img-eEqkMs1G-1619590293209)]
https://blog.csdn.net/elma_tww/article/details/86164674
算法分析
最好情况,分成长度相近的两个子序列,
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n)
最坏情况,分成长度差异很大的两个子序列,
O
(
n
2
)
O(n^2)
O(n2)
快速排序算法不稳定。
快速排序的一次划分算法从两头交替搜索,直到low和hight重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)