查找算法总结
Posted yangzhixue
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了查找算法总结相关的知识,希望对你有一定的参考价值。
1.顺序查找(线性查找)
顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术。它的查找过程为:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,如果某个记录的关键字和给定值相等,则查找成功,找到所查的记录。如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。
public class SeqSearch { public static void main(String[] args) { int arr[] = { 1, 9, 11, -1, 34, 89 };// 没有顺序的数组 int index = seqSearch(arr, -11); if(index == -1) { System.out.println("没有找到到"); } else { System.out.println("找到,下标为=" + index); } } /** * 这里我们实现的线性查找是找到一个满足条件的值,就返回 * @param arr * @param value * @return */ public static int seqSearch(int[] arr, int value) { // 线性查找是逐一比对,发现有相同值,就返回下标 for (int i = 0; i < arr.length; i++) { if(arr[i] == value) { return i; } } return -1; } }
3、分析
显然可知该算法的时间复杂度为O(n)。当n很大时,查找效率极为低下。用于小型数据的查找可以体现出算法简单的优点。
2.有序表查找
2.1、折半查找
折半查找(Binary Search) 又称为 二分查找。
基本思想:
- 首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;
- 否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。
- 重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
//注意:使用二分查找的前提是 该数组是有序的. public class BinarySearch { public static void main(String[] args) { //int arr[] = { 1, 8, 10, 89,1000,1000, 1234 }; int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 , 11, 12, 13,14,15,16,17,18,19,20 }; // // int resIndex = binarySearch(arr, 0, arr.length - 1, 1000); // System.out.println("resIndex=" + resIndex); List<Integer> resIndexList = binarySearch2(arr, 0, arr.length - 1, 1); System.out.println("resIndexList=" + resIndexList); } // 二分查找算法 /** * * @param arr * 数组 * @param left * 左边的索引 * @param right * 右边的索引 * @param findVal * 要查找的值 * @return 如果找到就返回下标,如果没有找到,就返回 -1 */ public static int binarySearch(int[] arr, int left, int right, int findVal) { // 当 left > right 时,说明递归整个数组,但是没有找到 if (left > right) { return -1; } int mid = (left + right) / 2; int midVal = arr[mid]; if (findVal > midVal) { // 向 右递归 return binarySearch(arr, mid + 1, right, findVal); } else if (findVal < midVal) { // 向左递归 return binarySearch(arr, left, mid - 1, findVal); } else { return mid; } } //完成一个课后思考题: /* * 课后思考题: {1,8, 10, 89, 1000, 1000,1234} 当一个有序数组中, * 有多个相同的数值时,如何将所有的数值都查找到,比如这里的 1000 * * 思路分析 * 1. 在找到mid 索引值,不要马上返回 * 2. 向mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList * 3. 向mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList * 4. 将Arraylist返回 */ public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal) { System.out.println("hello~"); // 当 left > right 时,说明递归整个数组,但是没有找到 if (left > right) { return new ArrayList<Integer>(); } int mid = (left + right) / 2; int midVal = arr[mid]; if (findVal > midVal) { // 向 右递归 return binarySearch2(arr, mid + 1, right, findVal); } else if (findVal < midVal) { // 向左递归 return binarySearch2(arr, left, mid - 1, findVal); } else { // * 思路分析 // * 1. 在找到mid 索引值,不要马上返回 // * 2. 向mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList // * 3. 向mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList // * 4. 将Arraylist返回 List<Integer> resIndexlist = new ArrayList<Integer>(); //向mid 索引值的左边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList int temp = mid - 1; while(true) { if (temp < 0 || arr[temp] != findVal) {//退出 break; } //否则,就temp 放入到 resIndexlist resIndexlist.add(temp); temp -= 1; //temp左移 } resIndexlist.add(mid); // //向mid 索引值的右边扫描,将所有满足 1000, 的元素的下标,加入到集合ArrayList temp = mid + 1; while(true) { if (temp > arr.length - 1 || arr[temp] != findVal) {//退出 break; } //否则,就temp 放入到 resIndexlist resIndexlist.add(temp); temp += 1; //temp右移 } return resIndexlist; } } }
2、插值查找
public class InsertValueSearch { public static void main(String[] args) { // int [] arr = new int[100]; // for(int i = 0; i < 100; i++) { // arr[i] = i + 1; // } int arr[] = { 1, 8, 10, 89,1000,1000, 1234 }; int index = insertValueSearch(arr, 0, arr.length - 1, 1234); //int index = binarySearch(arr, 0, arr.length, 1); System.out.println("index = " + index); //System.out.println(Arrays.toString(arr)); } public static int binarySearch(int[] arr, int left, int right, int findVal) { System.out.println("二分查找被调用~"); // 当 left > right 时,说明递归整个数组,但是没有找到 if (left > right) { return -1; } int mid = (left + right) / 2; int midVal = arr[mid]; if (findVal > midVal) { // 向 右递归 return binarySearch(arr, mid + 1, right, findVal); } else if (findVal < midVal) { // 向左递归 return binarySearch(arr, left, mid - 1, findVal); } else { return mid; } } //编写插值查找算法 //说明:插值查找算法,也要求数组是有序的 /** * * @param arr 数组 * @param left 左边索引 * @param right 右边索引 * @param findVal 查找值 * @return 如果找到,就返回对应的下标,如果没有找到,返回-1 */ public static int insertValueSearch(int[] arr, int left, int right, int findVal) { System.out.println("插值查找次数~~"); //注意:findVal < arr[0] 和 findVal > arr[arr.length - 1] 必须需要 //否则我们得到的 mid 可能越界 if (left > right || findVal < arr[0] || findVal > arr[arr.length - 1]) { return -1; } // 求出mid, 自适应 int mid = left + (right - left) * (findVal - arr[left]) / (arr[right] - arr[left]); int midVal = arr[mid]; if (findVal > midVal) { // 说明应该向右边递归 return insertValueSearch(arr, mid + 1, right, findVal); } else if (findVal < midVal) { // 说明向左递归查找 return insertValueSearch(arr, left, mid - 1, findVal); } else { return mid; } } }
3、斐波那契查找
斐波那契查找是利用黄金分隔原理来实现的。它的本质和二分查找、插值查找没有区别,都是通过设置分隔,不断将区间缩小,最后查找到关键字的。与之前两个查找方法相似,斐波那契的不同也是分隔设置的不同,它是通过斐波那契数列来设置的。
黄金分割点十八一条线段分割为两部分,使其中与全长之比等于一部分之比。取其前三位数字的近似值为0.618
public class FibonacciSearch { public static int maxSize = 20; public static void main(String[] args) { int [] arr = {1,8, 10, 89, 1000, 1234}; System.out.println("index=" + fibSearch(arr, 189));// 0 } //因为后面我们mid=low+F(k-1)-1,需要使用到斐波那契数列,因此我们需要先获取到一个斐波那契数列 //非递归方法得到一个斐波那契数列 public static int[] fib() { int[] f = new int[maxSize]; f[0] = 1; f[1] = 1; for (int i = 2; i < maxSize; i++) { f[i] = f[i - 1] + f[i - 2]; } return f; } //编写斐波那契查找算法 //使用非递归的方式编写算法 /** * * @param a 数组 * @param key 我们需要查找的关键码(值) * @return 返回对应的下标,如果没有-1 */ public static int fibSearch(int[] a, int key) { int low = 0; int high = a.length - 1; int k = 0; //表示斐波那契分割数值的下标 int mid = 0; //存放mid值 int f[] = fib(); //获取到斐波那契数列 //获取到斐波那契分割数值的下标 while(high > f[k] - 1) { k++; } //因为 f[k] 值 可能大于 a 的 长度,因此我们需要使用Arrays类,构造一个新的数组,并指向temp[] //不足的部分会使用0填充 int[] temp = Arrays.copyOf(a, f[k]); //实际上需求使用a数组最后的数填充 temp //举例: //temp = {1,8, 10, 89, 1000, 1234, 0, 0} => {1,8, 10, 89, 1000, 1234, 1234, 1234,} for(int i = high + 1; i < temp.length; i++) { temp[i] = a[high]; } // 使用while来循环处理,找到我们的数 key while (low <= high) { // 只要这个条件满足,就可以找 mid = low + f[k - 1] - 1; if(key < temp[mid]) { //我们应该继续向数组的前面查找(左边) high = mid - 1; //为甚是 k-- //说明 //1. 全部元素 = 前面的元素 + 后边元素 //2. f[k] = f[k-1] + f[k-2] //因为 前面有 f[k-1]个元素,所以可以继续拆分 f[k-1] = f[k-2] + f[k-3] //即 在 f[k-1] 的前面继续查找 k-- //即下次循环 mid = f[k-1-1]-1 k--; } else if ( key > temp[mid]) { // 我们应该继续向数组的后面查找(右边) low = mid + 1; //为什么是k -=2 //说明 //1. 全部元素 = 前面的元素 + 后边元素 //2. f[k] = f[k-1] + f[k-2] //3. 因为后面我们有f[k-2] 所以可以继续拆分 f[k-1] = f[k-3] + f[k-4] //4. 即在f[k-2] 的前面进行查找 k -=2 //5. 即下次循环 mid = f[k - 1 - 2] - 1 k -= 2; } else { //找到 //需要确定,返回的是哪个下标 if(mid <= high) { return mid; } else { return high; } } } return -1; } }
4、总结
- 时间复杂度:二分查找、插值查找和斐波那契查找的时间复杂度都是O(logn)。
- 二分查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,这样的算法已经比较好了。但是对于需要频繁执行插入或者删除操作的数据集来说,维护有序表的排序会带来不小的工作量,并不适合使用。
- 插值查找,对于表长较大,而关键字分布比较均匀的查找表来说,插值查找算法的平均性能要比折半查找好得多。反之,如果数据中分布类似{0,1,9999,999999}这样极端不均匀的数据,用插值查找未必是最合适的选择。
- 斐波那契查找,就平均性能而言,要优于二分查找,但是如果是最坏的情况,比如key=0,那么始终在左侧长半区在查找,查找的效率要低于折半查找。
- 比较关键的一点是,插值、折半都需要进行比较复杂的乘除法运算,而斐波那契只需要进行简单的加减运算,在海量数据的查找过程中,这种细微的差别可能会影响最终的查找效率。
线性索引查找
索引:就是把一个关键字与它对应的i记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。
索引按照结构可以分为:线性索引、树形索引和多级索引。
线性索引是将索引项集合组织为线性结构,也称为索引表。包括稠密索引、分块索引、倒排索引。
1、稠密索引
稠密索引是指在线性索引表中,将数据集中的每个记录对应一个索引项。并且索引项一定是按照关键码有序的排列。
优点:
索引项有序也就意味着,在查找关键字时,可以用到折半、插值、斐波那契等有序的查找算法。
缺点:
如果数据集非常大,比如说上亿,那也就意味着索引同样的数据规模,可能就需要反复查询内存和硬盘,性能可能反而下降了。
补代码
2、分块索引
分块有序需要满足两个条件:块内无序(有序更好,就是代价比较大)、块间有序
对于分块有序的数据集,将每块对应一个索引项,这种索引方法叫做分块索引。索引项结构分为三个数据项:最大关键码、块长和块首指针。
补代码
3、倒排索引
例如我们看如下两句话:
- Books and friends should be few but good
- A good book is a good friend
如图所示,我们将单词做了排序,也就是表格显示了每个不同的单词分别出现在哪篇文章中。
有了这张表,我们在查每个单词时都能很快的查找到这个单词在哪篇文章。
在这里这张单词表就是索引表,索引项的通用结构是:
- 次关键码,例如上表中的“英文单词”
- 记录号表,例如上表中的“文章编号”
其中记录号表存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。这样的索引方法就是倒排索引。
补代码
二叉排序树
二叉排序树,又叫二叉查找树,它或者是一棵空树;或者是具有以下性质的二叉树:
- 1. 若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
- 2. 若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
- 3. 它的左右子树也分别为二叉排序树。
如下图所示:
其中序遍历得到序列是一个有序序列,所以称为二叉排序树。
1、实现查找(递归实现)
BSTNode* BSTree::search(BSTNode* node, int key) { if (node == NULL || node->key == key) return node; if (node->key < key) search(node->right, key); else search(node->left, key); }
2、插入(递归实现)
template<typename T> void BinaryTree<T>::Insert(const T &data, Node<T>* &b) //递归法在二叉排序树中插入一个节点 { if (b == NULL) //递归终止条件 { b = new Node<T>(data); if (!b) { cout << "out of space!" << endl; } } else if (data < b->info) Insert(data, b->lchild); else Insert(data, b->rchild);
}
3、删除
前两种都好说,第三种的思想是,用该节点左孩子子树的最大值,或者该节点右孩子的最小值(这两个值和这个几点最近),代替该节点位置,删除原树中该节点和被用来替换的节点。
补代码
4、总结
给定值的比较次数等于给定值节点在二叉排序树中的层数。如果二叉排序树是平衡的,则n个节点的二叉排序树的高度为Log2n+1,其查找效率为O(Log2n),近似于折半查找。如果二叉排序树完全不平衡,则其深度可达到n,查找效率为O(n),退化为顺序查找。一般的,二叉排序树的查找性能在O(Log2n)到O(n)之间。因此,为了获得较好的查找性能,就要构造一棵平衡的二叉排序树。
平衡二叉树(AVL树)
1、概念
平衡二叉树(Balanced Binary Tree)又被称为AVL树(有别于AVL算法),且具有以下性质:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。
平衡二叉树大部分操作和二叉查找树类似,主要不同在于插入删除的时候平衡二叉树的平衡可能被改变,并且只有从那些插入点到根结点的路径上的结点的平衡性可能被改变,因为只有这些结点的子树可能变化。
平衡二叉树的递归定义:平衡二叉树是一棵二叉树,其可以为空,或满足如下2个性质:①左右子树深度之差的绝对值不大于1。②左右子树都是平衡二叉树。
平衡因子的概念:结点的平衡因子 = 结点的左子树深度 — 结点的右子树深度。若平衡因子的取值为-1、0或1时,该节点是平衡的,否则是不平衡的。
最低不平衡结点的概念:用A表示最低不平衡结点,则A的祖先结点可能有不平衡的,但其所有后代结点都是平衡的。
2.平衡化的实现
整个实现过程是通过在一棵平衡二叉树中依次插入元素(按照二叉排序树的方式),若出现不平衡,则要根据新插入的结点与最低不平衡结点的位置关系进行相应的调整。分为LL型、RR型、LR型和RL型4种类型,各调整方法如下(下面用A表示最低不平衡结点):
(1)LL型调整:
由于在A的左孩子(L)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1增至2。下面图1是LL型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B顺时针旋转一样。
LL型调整的一般形式如下图2所示,表示在A的左孩子B的左子树BL(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将A的左孩子B提升为新的根结点;②将原来的根结点A降为B的右孩子;③各子树按大小关系连接(BL和AR不变,BR调整为A的左子树)。
(2)RR型调整:
由于在A的右孩子(R)的右子树(R)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。图3是RR型的最简单形式。显然,按照大小关系,结点B应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡,A结点就好像是绕结点B逆时针旋转一样。
RR型调整的一般形式如下图4所示,表示在A的右孩子B的右子树BR(不一定为空)中插入结点(图中阴影部分所示)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将A的右孩子B提升为新的根结点;②将原来的根结点A降为B的左孩子;③各子树按大小关系连接(AL和BR不变,BL调整为A的右子树)。
(3)LR型调整:
由于在A的左孩子(L)的右子树(R)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由1变为2。图5是LR型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
LR型调整的一般形式如下图6所示,表示在A的左孩子B的右子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将C的右孩子B提升为新的根结点;②将原来的根结点A降为C的右孩子;③各子树按大小关系连接(BL和AR不变,CL和CR分别调整为B的右子树和A的左子树)。
(4)RL型调整:
由于在A的右孩子(R)的左子树(L)上插入新结点,使原来平衡二叉树变得不平衡,此时A的平衡因子由-1变为-2。图7是RL型的最简单形式。显然,按照大小关系,结点C应作为新的根结点,其余两个节点分别作为左右孩子节点才能平衡。
RL型调整的一般形式如下图8所示,表示在A的右孩子B的左子树(根结点为C,不一定为空)中插入结点(图中两个阴影部分之一)而导致不平衡( h 表示子树的深度)。这种情况调整如下:①将C的右孩子B提升为新的根结点;②将原来的根结点A降为C的左孩子;③各子树按大小关系连接(AL和BR不变,CL和CR分别调整为A的右子树和B的左子树)。
3、分析
4、代码
#include <iostream> #include <algorithm> using namespace std; #pragma once //平衡二叉树结点 template <typename T> struct AvlNode { T data; int height; //结点所在高度 AvlNode<T> *left; AvlNode<T> *right; AvlNode<T>(const T theData) : data(theData), left(NULL), right(NULL), height(0){} }; //AvlTree template <typename T> class AvlTree { public: AvlTree<T>(){} ~AvlTree<T>(){} AvlNode<T> *root; //插入结点 void Insert(AvlNode<T> *&t, T x); //删除结点 bool Delete(AvlNode<T> *&t, T x); //查找是否存在给定值的结点 bool Contains(AvlNode<T> *t, const T x) const; //中序遍历 void InorderTraversal(AvlNode<T> *t); //前序遍历 void PreorderTraversal(AvlNode<T> *t); //最小值结点 AvlNode<T> *FindMin(AvlNode<T> *t) const; //最大值结点 AvlNode<T> *FindMax(AvlNode<T> *t) const; private: //求树的高度 int GetHeight(AvlNode<T> *t); //单旋转 左 AvlNode<T> *LL(AvlNode<T> *t); //单旋转 右 AvlNode<T> *RR(AvlNode<T> *t); //双旋转 右左 AvlNode<T> *LR(AvlNode<T> *t); //双旋转 左右 AvlNode<T> *RL(AvlNode<T> *t); }; template <typename T> AvlNode<T> * AvlTree<T>::FindMax(AvlNode<T> *t) const { if (t == NULL) return NULL; if (t->right == NULL) return t; return FindMax(t->right); } template <typename T> AvlNode<T> * AvlTree<T>::FindMin(AvlNode<T> *t) const { if (t == NULL) return NULL; if (t->left == NULL) return t; return FindMin(t->left); } template <typename T> int AvlTree<T>::GetHeight(AvlNode<T> *t) { if (t == NULL) return -1; else return t->height; } //单旋转 //左左插入导致的不平衡 template <typename T> AvlNode<T> * AvlTree<T>::LL(AvlNode<T> *t) { AvlNode<T> *q = t->left; t->left = q->right; q->right = t; t = q; t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; q->height = max(GetHeight(q->left), GetHeight(q->right)) + 1; return q; } //单旋转 //右右插入导致的不平衡 template <typename T> AvlNode<T> * AvlTree<T>::RR(AvlNode<T> *t) { AvlNode<T> *q = t->right; t->right = q->left; q->left = t; t = q; t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; q->height = max(GetHeight(q->left), GetHeight(q->right)) + 1; return q; } //双旋转 //插入点位于t的左儿子的右子树 template <typename T> AvlNode<T> * AvlTree<T>::LR(AvlNode<T> *t) { //双旋转可以通过两次单旋转实现 //对t的左结点进行RR旋转,再对根节点进行LL旋转 RR(t->left); return LL(t); } //双旋转 //插入点位于t的右儿子的左子树 template <typename T> AvlNode<T> * AvlTree<T>::RL(AvlNode<T> *t) { LL(t->right); return RR(t); } template <typename T> void AvlTree<T>::Insert(AvlNode<T> *&t, T x) { if (t == NULL) t = new AvlNode<T>(x); else if (x < t->data) { Insert(t->left, x); //判断平衡情况 if (GetHeight(t->left) - GetHeight(t->right) > 1) { //分两种情况 左左或左右 if (x < t->left->data)//左左 t = LL(t); else //左右 t = LR(t); } } else if (x > t->data) { Insert(t->right, x); if (GetHeight(t->right) - GetHeight(t->left) > 1) { if (x > t->right->data) t = RR(t); else t = RL(t); } } else ;//数据重复 t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; } template <typename T> bool AvlTree<T>::Delete(AvlNode<T> *&t, T x) { //t为空 未找到要删除的结点 if (t == NULL) return false; //找到了要删除的结点 else if (t->data == x) { //左右子树都非空 if (t->left != NULL && t->right != NULL) {//在高度更大的那个子树上进行删除操作 //左子树高度大,删除左子树中值最大的结点,将其赋给根结点 if (GetHeight(t->left) > GetHeight(t->right)) { t->data = FindMax(t->left)->data; Delete(t->left, t->data); } else//右子树高度更大,删除右子树中值最小的结点,将其赋给根结点 { t->data = FindMin(t->right)->data; Delete(t->right, t->data); } } else {//左右子树有一个不为空,直接用需要删除的结点的子结点替换即可 AvlNode<T> *old = t; t = t->left ? t->left: t->right;//t赋值为不空的子结点 delete old; } } else if (x < t->data)//要删除的结点在左子树上 { //递归删除左子树上的结点 Delete(t->left, x); //判断是否仍然满足平衡条件 if (GetHeight(t->right) - GetHeight(t->left) > 1) { if (GetHeight(t->right->left) > GetHeight(t->right->right)) { //RL双旋转 t = RL(t); } else {//RR单旋转 t = RR(t); } } else//满足平衡条件 调整高度信息 { t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; } } else//要删除的结点在右子树上 { //递归删除右子树结点 Delete(t->right, x); //判断平衡情况 if (GetHeight(t->left) - GetHeight(t->right) > 1) { if (GetHeight(t->left->right) > GetHeight(t->left->left)) { //LR双旋转 t = LR(t); } else { //LL单旋转 t = LL(t); } } else//满足平衡性 调整高度 { t->height = max(GetHeight(t->left), GetHeight(t->right)) + 1; } } return true; } //查找结点 template <typename T> bool AvlTree<T>::Contains(AvlNode<T> *t, const T x) const { if (t == NULL) return false; if (x < t->data) return Contains(t->left, x); else if (x > t->data) return Contains(t->right, x); else return true; } //中序遍历 template <typename T> void AvlTree<T>::InorderTraversal(AvlNode<T> *t) { if (t) { InorderTraversal(t->left); cout << t->data << ‘ ‘; InorderTraversal(t->right); } } //前序遍历 template <typename T> void AvlTree<T>::PreorderTraversal(AvlNode<T> *t) { if (t) { cout << t->data << ‘ ‘; PreorderTraversal(t->left); PreorderTraversal(t->right); } }
红黑树
https://www.cnblogs.com/yyxt/p/4983967.html
https://www.cnblogs.com/yyxt/p/4983967.html
多路查找树(B树)
等下再补
散列表(哈希表)查找
一、散列表相关概念
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。建立了关键字与存储位置的映射关系,公式如下:
存储位置 = f(关键字)
采用散列技术将记录存在在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。那么,关键字对应的记录存储位置称为散列地址。
散列技术既是一种存储方法也是一种查找方法。散列技术的记录之间不存在什么逻辑关系,它只与关键字有关,因此,散列主要是面向查找的存储结构。
二、散列函数的构造方法
2.1 直接定址法
所谓直接定址法就是说,取关键字的某个线性函数值为散列地址,即
优点:简单、均匀,也不会产生冲突。
缺点:需要事先知道关键字的分布情况,适合查找表较小且连续的情况。
由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。
2.2 数字分析法
如果关键字时位数较多的数字,比如11位的手机号"130****1234",其中前三位是接入号;中间四位是HLR识别号,表示用户号的归属地;后四为才是真正的用户号。如下图所示。
如果现在要存储某家公司的登记表,若用手机号作为关键字,极有可能前7位都是相同的,选择后四位成为散列地址就是不错的选择。若容易出现冲突,对抽取出来 的数字再进行反转、右环位移等。总的目的就是为了提供一个散列函数,能够合理地将关键字分配到散列表的各个位置。
数字分析法通过适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布比较均匀,就可以考虑用这个方法。
2.3 平方取中法
这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。
平方取中法比较适合不知道关键字的分布,而位数又不是很大的情况。
2.4 折叠法
折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
比如关键字是9876543210,散列表表长为三位,将它分为四组,987|654|321|0,然后将它们叠加求和987 + 654 + 321 + 0 = 1962,再求后3位得到散列地址962。
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
2.5 除留余数法
此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:
mod是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可以再折叠、平方取中后再取模。
很显然,本方法的关键在于选择合适的p,p如果选不好,就可能会容易产生冲突。
根据前辈们的经验,若散列表的表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。
2.6 随机数法
选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key) = random(key)。这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。
总之,现实中,应该视不同的情况采用不同的散列函数,这里只能给出一些考虑的因素来提供参考:
(1)计算散列地址所需的时间
(2)关键字的长度;
(3)散列表的长度;
(4)关键字的分布情况;
(5)记录查找的频率。
综合以上等因素,才能决策选择哪种散列函数更合适。
三、处理散列冲突的方法
在理想的情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,可现实中,这只是一个理想。市场会碰到两个关键字key1 != key2,但是却有f(key1) = f(key2),这种现象称为冲突。出现冲突将会造成查找错误,因此可以通过精心设计散列函数让冲突尽可能的少,但是不能完全避免。
3.1 开放定址法
所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
它的公式为:
比如说,关键字集合为{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},表长为12。散列函数f(key) = key mod 12。
当计算前5个数{12, 67, 56, 16, 25}时,都是没有冲突的散列地址,直接存入,如下表所示。
计算key = 37时,发现f(37) = 1,此时就与25所在的位置冲突。于是应用上面的公式f(37) = (f(37) + 1) mod 12 =2,。于是将37存入下标为2的位置。如下表所示。
接下来22,29,15,47都没有冲突,正常的存入,如下标所示。
到了48,计算得到f(48) = 0,与12所在的0位置冲突了,不要紧,我们f(48) = (f(48) + 1) mod 12 = 1,此时又与25所在的位置冲突。于是f(48) = (f(48) + 2) mod 12 = 2,还是冲突......一直到f(48) = (f(48) + 6) mod 12 = 6时,才有空位,如下表所示。
把这种解决冲突的开放定址法称为线性探测法。
考虑深一步,如果发生这样的情况,当最后一个key = 34,f(key) = 10,与22所在的位置冲突,可是22后面没有空位置了,反而它的前面有一个空位置,尽管可以不断地求余后得到结果,但效率很差。因此可以改进di=12, -12, 22, -22.........q2, -q2(q<= m/2),这样就等于是可以双向寻找到可能的空位置。对于34来说,取di = -1即可找到空位置了。另外,增加平方运算的目的是为了不让关键字都聚集在某一块区域。称这种方法为二次探测法。
还有一种方法,在冲突时,对于位移量di采用随机函数计算得到,称之为随机探测法。
既然是随机,那么查找的时候不也随机生成di 吗?如何取得相同的地址呢?这里的随机其实是伪随机数。伪随机数就是说,如果设置随机种子相同,则不断调用随机函数可以生成不会重复的数列,在查找时,用同样的随机种子,它每次得到的数列是想通的,相同的di 当然可以得到相同的散列地址。
总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是常用的解决冲突的方法。
3.2 再散列函数法
对于散列表来说,可以事先准备多个散列函数。
这里RHi 就是不同的散列函数,可以把前面说的除留余数、折叠、平方取中全部用上。每当发生散列地址冲突时,就换一个散列函数计算。
这种方法能够使得关键字不产生聚集,但相应地也增加了计算的时间。
3.3 链地址法
将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表前面的指针。对于关键字集合{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},用前面同样的12为余数,进行除留余数法,可以得到下图结构。
此时,已经不存在什么冲突换地址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。
链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保证。当然,这也就带来了查找时需要遍历单链表的性能损耗。
3.4 公共溢出区法
这个方法其实更好理解,你冲突是吧?那重新给你找个地址。为所有冲突的关键字建立一个公共的溢出区来存放。
就前面的例子而言,共有三个关键字37、48、34与之前的关键字位置有冲突,那就将它们存储到溢出表中。如下图所示。
在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表中进行顺序查找。如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。
四、代码
#include <stdio.h> #include <stdlib.h> #define OK 1 #define ERROR 0 #define SUCCESS 1 #define UNSUCCESS 0 #define HASHSIZE 12 //定义散列表表未数组的长度 #define NULLKEY -32768 typedef struct { int *elem; //数据元素存储基地址,动态分配数组 int count; //当前数据元素个数 }HashTable; int m = 0; //散列表长,全局变量 //初始化散列表 int InitHashTable(HashTable *h) { int i; m = HASHSIZE; h->elem = (int *)malloc(sizeof(int) * m ); if(h->elem == NULL) { fprintf(stderr, "malloc() error. "); return ERROR; } for(i = 0; i < m; i++) { h->elem[i] = NULLKEY; } return OK; } //散列函数 int Hash(int key) { return key % m; //除留余数法 } //插入关键字进散列表 void InsertHash(HashTable *h, int key) { int addr = Hash(key); //求散列地址 while(h->elem[addr] != NULLKEY) //如果不为空,则冲突 { addr = (addr + 1) % m; //开放地址法的线性探测 } h->elem[addr] = key; //直到有空位后插入关键字 } //散列表查找关键字 int SearchHash(HashTable h, int key) { int addr = Hash(key); //求散列地址 while(h.elem[addr] != key) //如果不为空,则冲突 { addr = (addr + 1) % m; //开放地址法的线性探测 if(h.elem[addr] == NULLKEY || addr == Hash(key)) { //如果循环回原点 printf("查找失败, %d 不在Hash表中. ", key); return UNSUCCESS; } } printf("查找成功,%d 在Hash表第 %d 个位置. ", key, addr); return SUCCESS; } int main(int argc, char **argv) { int i = 0; int num = 0; HashTable h; //初始化Hash表 InitHashTable(&h); //未插入数据之前,打印Hash表 printf("未插入数据之前,Hash表中内容为: "); for(i = 0; i < HASHSIZE; i++) { printf("%d ", h.elem[i]); } printf("\\n"); //插入数据 printf("现在插入数据,请输入(A代表结束哦). "); while(scanf("%d", &i) == 1 && num < HASHSIZE) { if(i == ‘a‘) { break; } num++; InsertHash(&h,i); if(num > HASHSIZE) { printf("插入数据超过Hash表大小 "); return ERROR; } } //打印插入数据后Hash表的内容 printf("插入数据后Hash表的内容为: "); for(i = 0; i < HASHSIZE; i++) { printf("%d ", h.elem[i]); } printf(" "); printf("现在进行查询. "); SearchHash(h, 12); SearchHash(h, 100); return 0; }
五、散列表的性能分析
如果没有冲突,散列查找是所介绍过的查找中效率最高的。因为它的时间复杂度为O(1)。但是,没有冲突的散列只是一种理想,在实际应用中,冲突是不可避免的。
那散列查找的平均查找长度取决于哪些因素呢?
(1)散列函数是否均匀
散列函数的好坏直接影响着出现冲突的频繁程度,但是,不同的散列函数对同一组随机的关键字,产生冲突的可能性是相同的(为什么??),因此,可以不考虑它对平均查找长度的影响。
(2)处理冲突的方法
相同的关键字、相同的散列函数,但处理冲突的方法不同,会使得平均查找长度不同。如线性探测处理冲突可能会产生堆积,显然就没有二次探测好,而链地址法处理冲突不会产生任何堆积,因而具有更好的平均查找性能。
(3)散列表的装填因子
所谓的装填因子a = 填入表中的记录个数/散列表长度。a标志着散列表的装满的程度。当填入的记录越多,a就越大,产生冲突的可能性就越大。也就说,散列表的平均查找长度取决于装填因子,而不是取决于查找集合中的记录个数。
不管记录个数n有多大,总可以选择一个合适的装填因子以便将平均查找长度限定在一个范围之内,此时散列表的查找时间复杂度就是O(1)了。为了这个目标,通常将散列表的空间设置的比查找表集合大。
六、散列表的适应范围
散列技术最适合的求解问题是查找与给定值相等的记录。对于查找来说,简化了比较过程,效率会大大提高。
但是,散列技术不具备很多常规数据结构的能力,比如
- 同样的关键字,对应很多记录的情况,不适合用散列技术;
- 散列表也不适合范围查找等等。
参考资料:
https://blog.csdn.net/qq_28267025/article/details/78486192(顺序表查找)
https://www.cnblogs.com/cuglkb/p/9324050.html(有序表查找)
https://blog.csdn.net/championlee_cs/article/details/46443415(二叉排序树的C++实现)
https://blog.csdn.net/zxzxzx0119/article/details/80012374(二叉树,程序)
https://www.cnblogs.com/zhangbaochong/p/5164994.html(AVL树)
https://blog.csdn.net/qq_24336773/article/details/81712866(AVL树)
https://www.cnblogs.com/changyaohua/p/4657205.html(散列表查找)
以上是关于查找算法总结的主要内容,如果未能解决你的问题,请参考以下文章