算法概念
查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文简单概括性的介绍了常见的七种查找算法,说是七种,其实二分查找、插值查找以及斐波那契查找都可以归为一类——插值查找。插值查找和斐波那契查找是在二分查找的基础上的优化查找算法
查找算法分类:
静态查找和动态查找;
注:静态或者动态都是针对查找表而言的。动态表指查找表中有删除和插入操作的表。无序查找和有序查找。
无序查找:被查找数列有序无序均可;
有序查找:被查找数列必须为有序数列。平均查找长度(Average Search Length,ASL):需和指定key进行比较的关键字的个数的期望值,称为查找算法在查找成功时的平均查找长度
对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL = Pi*Ci的和。
Pi:查找表中第i个数据元素的概率。
Ci:找到第i个数据元素时已经比较过的次数。
排序方法 条件 时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 顺序查找 无序 分块查找 分块顺序 二分查找 有序 插值查找 有序 斐波那契查找 有序 二叉树查找 有序 哈希表法(散列表) 无序
七种算法
一、顺序查找
线性搜索或顺序搜索是一种寻找某一特定值的搜索算法,指按一定的顺序检查数组中每一个元素,直到找到所要寻找的特定值为止。是最简单的一种搜索算法。顺序查找也称为线形查找,属于无序查找算法。
算法描述
从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;
若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
代码实现
/// 顺序查找 /// @param array 查找数组 /// @param word 要查找关键字 - (NSInteger)sequentialSearch:(NSArray *)array searchWord:(id)word { for (int i = 0; i < array.count; i ++) { if ([array[i] isEqual:word]) { return I; break; } } return -1; } //调用 NSMutableArray * array = [NSMutableArray arrayWithObjects:@"15",@"6",@"106",@"236",@"2",@"34",@"13",@"58",@"37",@"121",@"33", nil]; NSLog(@"顺序查找:%ld", [self sequentialSearch:array searchWord:@"58"]);
算法分析
假设一个数组中有n个元素,最好的情况就是要寻找的特定值就是数组里的第一个元素,这样仅需要1次比较就可以。而最坏的情况是要寻找的特定值不在这个数组或者是数组里的最后一个元素,这就需要进行n次比较。
查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2
时间复杂度为O(n),最好的情况是第一个就查找到了,为O(1),最坏是没有找到,为O(n)。
二、分块查找
分块查找又称索引顺序查找,它是顺序查找的一种改进方法。
将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";即第1块中任一元素的关键字都必须小于第2块中任一元素的关键字;而第2块中任一元素又都必须小于第3块中的任一元素,……
算法描述
- 先选取各块中的最大关键字构成一个索引表;
- 先对索引表进行二分查找或顺序查找,以确定待查记录在哪一块中;
- 然后,在已确定的块中用顺序法进行查找。
代码实现
/// 分块查找 /// @param array 查找数组 /// @param indexItemList 索引表 /// @param number 要查找数字 - (NSInteger)blockSearch:(NSArray *)array indexItemList:(NSArray
*)indexItemList searchNumber:(id)number { BlockItem *indexItem = nil; //遍历索引表 for(int i = 0;i < indexItemList.count; i++) { //找到索引项 if(indexItemList[i].max >= number) { indexItem = indexItemList[I]; break; } } //索引表中不存在该索引项 if(indexItem == nil) { return -1; } //根据索引项,在主表中查找 for(int i = indexItem.start; i < indexItem.start + indexItem.length; i++) { if(array[i] == number){ return I; break; } } return -1; } //索引 @interface BlockItem : NSObject @property (assign, nonatomic) int start; @property (assign, nonatomic) int length; @property (assign, nonatomic) NSNumber *max; - (instancetype)initWithStart:(int)start length:(int)length max:(NSNumber *)max; @end @implementation BlockItem - (instancetype)initWithStart:(int)start length:(int)length max:(NSNumber *)max { if (self = [super init]) { self.start = start; self.length = length; self.max = max; } return self; } @end //调用 NSArray *blockArray = @[@3, @4, @6, @2, @5, @7, @14, @12, @16, @13, @19, @17, @25, @21, @36, @23, @22, @29]; BlockItem *model1 = [[BlockItem alloc] initWithStart:0 length:6 max:@7]; BlockItem *model2 = [[BlockItem alloc] initWithStart:6 length:6 max:@19]; BlockItem *model3 = [[BlockItem alloc] initWithStart:12 length:6 max:@36]; NSArray *indicesArray = @[model1, model2, model3]; NSLog(@"分块查找:%ld", [self blockSearch:blockArray indexItemList:indicesArray searchNumber: @17]);
算法分析
分块查找由于只要求索引表是有序的,对块内节点没有排序要求,因此特别适合于节点动态变化的情况。
索引查找的比较次数等于算法中查找索引表的比较次数和查找相应子表的比较次数之和,假定索引表的长度为m,子表长度为s,
则索引查找的平均查找长度为:
ASL= (1+m)/2 + (1+s)/2 = 1 + (m+s)/2
假定每个子表具有相同的长度,即s=n/m, 则 ASL = 1 + (m + n/m)/2 ,当m = n/m ,(即m = ,此时s也等于), ASL = 1 + 最小 ,时间复杂度为
可见,索引查找的速度快于顺序查找,但低于二分查找。
在索引存储中,不仅便于查找单个元素,而且更方便查找一个子表中的全部元素,若在主表中的每个子表后都预留有空闲位置,则索引存储也便于进行插入和删除运算。
三、二分查找
二分查找算法也叫折半法查找法,要求待查找的列表必须是按关键字大小有序排列的顺序表。
二分查找基于分治思想,在实现上可以使用递归或迭代,首先用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
算法描述
- 将表中间位置记录的关键字与查找关键字比较,如果两者相等则表示查找成功;否则利用中间位置记录将表分成前、后两个子表。
- 如果中间位置记录的关键字大于查找关键字,则进一步查找前一个子表,否则查找后一个子表。
- 重复以上过程,一直到找到满足条件的记录为止时表明查找成功。
- 如果最终子表不存在,则表明查找不成功。
代码实现
/// 二分法查找 /// @param array 查找数组 /// @param number 查找的数字 - (NSInteger)binarySearch:(NSArray *)array searchNumber:(id)number { NSUInteger mid; NSUInteger min = 0; NSUInteger max = array.count - 1; while (min <= max) { mid = (min + max)/2; if ([number intValue] == [array[mid] intValue]) { NSLog(@"We found the number! It is at index %lu", mid); return mid; break; } else if ([number intValue] < [array[mid] intValue]) { max = mid - 1; } else if ([number intValue] > [array[mid] intValue]) { min = mid + 1; } } return -1; } //调用 NSArray *binaryArray = @[@1,@20,@30,@45,@50,@55,@60,@66,@70]; NSLog(@"二分法查找:%ld", [self sequentialSearch:binaryArray searchWord:@30]);
算法分析
二分查找方法具有比较次数少、查找速度快及平均性能好的优点。缺点是要求待查表为有序表,且插入、删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。
最坏情况下,关键词比较次数为,且期望时间复杂度为;
四、插值查找
插值查找是对二分查找的一种改进,适用于均匀分布的有序表。思想基本上和二分查找一样,有修改的地方就是mid的获取。
在二分查找中,mid=(start+end)/2, 即mid=start+(end-start)/2;也就是说我们的mid每次都是折中的取,但是对于一些均匀分布的有序表,这样做感觉有些费时,比如找字典的时候,找a这个字母,我们肯定不会从中间开始,而是偏向于字典前面一些开始。
插值查找就是基于这样的思想对mid取值进行改进。通过类比,我们可以将查找的点改进为如下:
mid = start + (searchValue-array[start]) / (array[end]-array[start]) * (end-start),
也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字searchValue,这样也就间接地减少了比较次数。
算法描述
- 将表mid位置记录的关键字与查找关键字比较,如果两者相等则表示查找成功;否则利用中间位置记录将表分成前、后两个子表。
- 如果mid位置记录的关键字大于查找关键字,则进一步查找前一个子表,否则查找后一个子表。
- 重复以上过程,一直到找到满足条件的记录为止时表明查找成功。
- 如果最终子表不存在,则表明查找不成功。
代码实现
/// 插值查找 /// @param array 查找数组 /// @param number 要查找数字 /// @param startIndex 查找起始位置 /// @param endIndex 查找结束位置 - (NSInteger)insertValueSearch:(NSArray *)array searchNumber:(id)number startIndex:(int)startIndex endIndex:(int)endIndex { if (endIndex >= startIndex) { int mid = startIndex + ([number intValue] - [array[startIndex] intValue]) / ([array[endIndex] intValue] - [array[startIndex] intValue]) * (endIndex - startIndex); if (array[mid] == number) { return mid; } else if (array[mid] > number) { return [self insertValueSearch:array searchNumber:number startIndex:startIndex endIndex:mid-1]; } else if (array[mid] < number) { return [self insertValueSearch:array searchNumber:number startIndex:mid+1 endIndex:endIndex]; } } return -1; } //调用 NSArray *insetValueArray = @[@1,@20,@30,@45,@50,@55,@60,@66,@70]; NSLog(@"插值查找:%ld", [self insertValueSearch:insetValueArray searchNumber:@70 startIndex:0 endIndex:(insetValueArray.count-1)]);
算法分析
查找成功或者失败的时间复杂度均为 。
对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀(如[@1,@2,@3,@201,@202,@203,@1000,@1001,@1003]
),那么插值查找未必是很合适的选择。
五、斐波那契查找
上面我们讲了插值查找,它是基于折半查找改进,然后除了插值方式的切割外,还有基于斐波那契黄金分割点切割方式的改进。
所以斐波那契查找也是改变了二分查找中原有的中值 mid 的求解方式,其 mid 不再代表中值,而是代表了黄金分割点:
mid = left + F_{block - 1} - 1
算法描述
- 将表mid位置记录的关键字与查找关键字比较,如果两者相等则表示查找成功;否则利用中间位置记录将表分成前、后两个子表。
- 如果mid位置记录的关键字大于查找关键字,则进一步查找前一个子表,否则查找后一个子表。
- 重复以上过程,一直到找到满足条件的记录为止时表明查找成功。
- 如果最终子表不存在,则表明查找不成功。
代码实现
#pragma mark 斐波那契查找 /// 斐波那契查找 /// @param array 查找数组 /// @param searchNumber 查找的数字 - (NSInteger)fibonacciSearch:(NSArray *)array searchValue:(id)searchNumber { NSMutableArray *mArray = [NSMutableArray arrayWithArray:array]; NSArray *fibArray = [self fibonacciArray:30]; int startIndex = 0; int endIndex = mArray.count - 1; int k = 0; while (endIndex > ([fibArray[k] intValue] - 1)) { k ++; } for (int i = mArray.count; i <= [fibArray[k] intValue]; i ++) { mArray[i] = mArray[endIndex]; } while (startIndex <= endIndex) { int mid = startIndex + [fibArray[k - 1] intValue] - 1; // 根据斐波那契数列进行黄金分割 if ([mArray[mid] intValue] == [searchNumber intValue]) { if (mid <= array.count - 1) { return mid; } else { // 说明查找得到的数据元素是补全值 return array.count - 1; } } else if ([mArray[mid] intValue] > [searchNumber intValue]) { endIndex = mid - 1; k = k - 1; } else if ([mArray[mid] intValue] < [searchNumber intValue]) { startIndex = mid + 1; k = k - 2; } } return -1; } /// 获得一个斐波那契数组 /// @param size 数组大小 - (NSArray *)fibonacciArray:(NSInteger)size { NSMutableArray *array = [NSMutableArray arrayWithCapacity:size]; array[0] = @1; array[1] = @1; for (int i=2; i < size; i++) { array[i] = @([array[i-1] longLongValue] + [array[i-2] longLongValue]); } return array; } //调用 NSArray *fibonacciArray = @[@1,@3,@5,@7,@8,@10,@13,@15,@17,@19,@21]; NSLog(@"斐波那契查找:%ld", [self fibonacciSearch:fibonacciArray searchValue:@19]);
算法分析
在最坏情况下,斐波那契查找的时间复杂度还是,且其期望复杂度也为,但是与折半查找相比,斐波那契查找的优点是它只涉及加法和减法运算,而不用除法,而除法比加减法要占用更多的时间,因此,斐波那契查找的运行时间理论上比折半查找小,但是还是得视具体情况而定。
六、二叉树查找
二叉查找树(Binary Search Tree,BST)是一种特殊的二叉树,一棵二叉搜索树(BST)是一棵二叉树,其中,每个节点的值都要大于其左子树中任意节点的值而小于右子树中任意节点的值。
算法描述
- 如果二叉查找树为空,则返回空操作,否则,执行一下操作;
- 先取根节点,如果节点 X 等于根节点,则返回;
- 如果节点小于根节点,则递归查找左子树;如果节点大于根节点,则递归查找右子树。
代码实现
@interface Tree : NSObject @property (strong, nonatomic) NSNumber *value;//值 @property (assign, nonatomic) int index;//下标 @property (strong, nonatomic) Tree *left;//左树 @property (strong, nonatomic) Tree *right;//右树 - (instancetype)initWithValue:(NSNumber *)value index:(int)index left:(Tree *)left right:(Tree *)right; @end @implementation Tree - (instancetype)initWithValue:(NSNumber *)value index:(int)index left:(Tree *)left >right:(Tree *)right { if (self = [super init]) { self.value = value; self.index = index; self.left = left; self.right = right; } return self; } @end #pragma mark 二叉树查找 /// 二分法查找 /// @param binaryTree 查找的二叉树 /// @param number 查找的数字 - (NSInteger)binaryTreeSearch:(Tree *)binaryTree searchNumber:(id)number { Tree *tree = binaryTree; NSInteger index = -1; while (tree != nil) { if (number < tree.value) { tree = tree.left; } else if (number > tree.value) { tree = tree.right; } else if (number == tree.value) { index = tree.index; } } return index; }
算法分析
最优情况下,二叉搜索树为完全二叉树,其时间复杂度为:O()
最差情况下,二叉搜索树为单支树, ,其时间复杂度为:O(n)
一般的,二叉排序树的查找性能在O()到O(n)之间。因此,为了获得较好的查找性能,就要构造一棵平衡的二叉排序树,由此出现了2-3树、红黑树、B树、B+树等。
七、哈希表法(散列表)
哈希查找是是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值。
哈希表查找又叫散列表查找,通过查找关键字不需要比较就可以获得需要记录的存储位置,它是通过在记录的存储位置和它的关键字之间建立一个确定的对应关系hashAddress,使得每个 key 对应一个存储位置 hashAddress(key)。若查找集合中存在这个记录,则必定在 hashAddress(key) 的位置上。
算法描述
- 用给定的哈希函数构造哈希表。
- 根据选择的冲突处理方法解决地址冲突,常见的解决冲突的方法:拉链法和线性探测法
- 在哈希表的基础上执行哈希查找
代码实现
#pragma mark 哈希查找 /// 哈希查找(未考虑冲突) /// @param array 查找数组 /// @param number 要查找元素 - (NSInteger)hashSearch:(NSArray *)array searchNumber:(id)number { NSDictionary *hashTable = [self makeHashTable:array]; if ([hashTable valueForKey:[NSString stringWithFormat:@"%@", number]]) { NSInteger index = [[hashTable valueForKey:[NSString stringWithFormat:@"%@", number]] integerValue]; return index; } return -1; } - (NSDictionary *)makeHashTable:(NSArray *)array { NSMutableDictionary *hash = [NSMutableDictionary dictionary]; for (int i = 0; i < array.count; i++) { if (![hash valueForKey:[NSString stringWithFormat:@"%d", i]]) { [hash setValue:[NSString stringWithFormat:@"%d", i] forKey:[NSString stringWithFormat:@"%@", array[i]]]; } } return hash; } NSArray *hashArray = @[@1,@3,@5,@7,@8,@10,@13,@15,@17,@19,@21]; NSLog(@"哈希查找:%ld", [self hashSearch:hashArray searchNumber:@13]);
算法分析
单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)。
哈希表可以以极快的速度来查找、添加或删除元素(只需要数次的比较操作。)它比红黑树、二叉搜索树都要快得多。但是哈希表没有排序功能,类似的,如寻找最大值、最小值、中值这些行为都不能在哈希表中实现。
哈希表的查找过程基本上和造表过程相同。一些关键码可通过哈希函数转换的地址直接找到,另一些关键码在哈希函数得到的地址上产生了冲突,需要按处理冲突的方法进行查找。在介绍的三种处理冲突的方法中,产生冲突后的查找仍然是给定值与关键码进行比较的过程。所以,对哈希表查找效率的量度,依然用平均查找长度来衡量。
查找过程中,关键码的比较次数取决于产生冲突的多少。如果产生的冲突少,查找效率就高,如果产生的冲突多,查找效率就低。因此,影响产生冲突多少的因素,也就是影响查找效率的因素。影响产生冲突多少有以下三个因素:
① 哈希函数是否均匀;
② 处理冲突的方法;
③ 哈希表的装填因子。
分析这三个因素,尽管哈希函数的“好坏”直接影响冲突产生的频度,但一般情况下,我们总认为所选的哈希函数是“均匀的”。因此,可不考虑哈希函数对平均查找长度的影响。
哈希表的装填因子定义为 α =
是哈希表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,填入表中的元素较多,产生冲突的可能性就越大;α越小,填入表中的元素较少,产生冲突的可能性就越小。
实际上,哈希表的平均查找长度是装填因子α的函数,只是不同处理冲突的方法有不同的函数。下表为几种不同处理冲突方法的平均查找长度:
处理冲突的方法 2+ 查找成功时 查找不成功时 线性探测法 Hnl = (1 + ) Unl = (1 + ) 二次探测法与再哈希法 Hnl = - Unl = 链地址法 Hnl = 1 + Unl = α + 哈希方法存取速度快、节省空间,静态查找、动态查找均适用,但由于存取是随机的,因此,不便于顺序查找。