数据结构算法之----查找(学习总结)

  1. 基本概念

查找结构(查找表):用于查找的数据集合称为查找结构(查找表),他可以是一个链表,也可以是一个数组或其他数据类型。对查找表进行的操作一般有四种:

1)查询某个特定的数据元素是否在查找表中。

2)检索满足条件的某个特定的数据元素的各种属性。

3)在查找表中插入一个数据元素。

4)在查找表中删除某个数据元素。

如果一个查找表的操作只涉及1,和2的操作,则无须动态的修改查找表,此类查找表为静态查找表,需要动态的修改查找表则称为动态查找表。(散列查找即可用于静态查找表也可用于动态查找表中)

2.平均查找长度ASL:在查找过程中,一次查找的长度是指需要比较的关键码次数,而平均查找长度是所有查找过程中进行的关键码比较次数的平均值。式中,pi为查找第i个元素的概率,一般认为每个元素的查找概率相等。Ci为第i个元素所需的比较次数。平均查找长度是衡量查找算法效率的最主要指标。

3.折半查找(二分查找)

适用于事前已经排好序的顺序表。思路:首先将给定值k与表中中间位置元素的关键字比较,若相等,则查找成功,返回该元素的存储位置,若不等,则所需查找的元素只能在中间数据以外的前半部分和后半部分中。缩小范围继续进行同样的查找,重复直到找到为止。

int Binary_Search(SeqList L, elemtype key)
	{//在有序表L中查找关键字为key的元素,若存在则返回其位置,若不存在则返回-1
		int low = 0, high = L.TableLen - 1, mid;
		while (low<=high)
		{
			mid = (low + high) / 2;  //取中间位置
			if (L.elem[mid] == key)
				return mid;    //查找成功,则返回所在位置
			else if (elem[mid] > key)
				high = mid + 1;  //从前半部分继续查找
			else
				low = mid + 1;  //从后半部分继续查找
		}
		return -1;
	}

折半查找需要方便的定位查找区域,所以适合折半查找的存储结构必须具有随机存取的特性。所以该查找法适合于线性表的顺序存储结构,不适合链式存储结构,且要求元素按关键字有序排列。

1)有一循环有序数组A,如{7,8,9,0,1,2,3,4,5,6}不知道其最小值的位置。如何从这样的数组中寻找一个特定的元素?(使用二分查找,在得到中间元素的时候判断位于哪个子数组)

1)//lower 为数组首元素下标,u为最后元素下标,x为要查找的值
	int search(int a[], int lower, int u, int x)
	{
		while (lower <= u)
		{
			int m = (lower + u) / 2;
			if (a[m] == x)
			{
				return m;
			}
			else
				if (a[m] >= a[lower])
				{
					if (x > a[m])
					{
						lower = m + 1;
					}
					else
						if (x >= a[lower])
						{
							u = m - 1;
						}
						else
							lower = m + 1;
				}
				else
					if (a[m] > x)
					{
						u = m - 1;
					}
					else
						if (x <= a[u])
							lower = m + 1;
						else
							u = m - 1;
		}
		return -1;
	}   //此算法对于数组元素重复的时候不支持。

2)判定树

折半查找的过程可用判定树来描述。树中每个圆形节点表示一个记录,结点中的值为该记录的关键字值。树中叶子结点都是方形的,他表示查找不成功的情况。从判定树可看出,查找成功时的查找长度为从根节点到目的结点的路径上的结点数。查找不成功的查找长度为从根节点到对应失败结点的父节点路径上的结点数。每个结点大于其左子结点,且小于其右子结点。若有序序列有n个元素,则对应的判定树有n个圆形的非叶结点和n+1个方形叶结点。

图中n个圆形结点构成的树的深度与n个结点的完全二叉树的深度相同。均为  折半查找的时间复杂度为:比顺序查找效率高。

在上图所示的判定树中,在等概率条件下,查找成功的ASL=(1*1+2*2+3*4+4*4)/11=3,查找不成功的ASL=(3*4+4*8)/12=11/3.

结论:用折半查找法查找给定值查找成功或失败的比较次数最多不会超过树的高度。

4.键树

键树又称数字查找树,他是一棵度大于等于2的树,树中每个结点中不是包含一个或几个关键字,而是只含有组成关键字的字符。(若关键字为数值,则结点中只包含一个数位,若关键字为单词,则结点中只包含一个字母字符)

{CAI,CAO,LI,LAN,CHA,CHANG,WEN,CHAO,YUN,YANG,LONG,WANG,ZHAO,LIU,WU,CHEN}从根到叶子结点路径中结点的字符组成的字符串表示一个关键字,叶子结点中的特殊符号$表示字符串的结束。键树的存储通常两种方法:

1)用树的孩子兄弟链表表示键树(称为双链树)每个node有三个域:symbol:存储关键字的一个符号,son:存储第一颗子树的根指针,brother:存储右兄弟的指针。(这时的键树又称双链树)

查找过程:顺着son查找,若相等,继续下一个son,否则沿着brother查找,直到空指针为止,若仍未完成key匹配,查找不成功。在双链树中插入或删除一个关键字,相当于在树中的某个结点上插入或删除一棵子树。

2)用多重链表表示(又称trie树,字典树)

以树的多重链表表示键树,则树中每个结点中应包含d个(d为关键字符的基,如字符集由英文大写字母构成时d=26)指针域,此时的键树叫trie树。(p276)

trie树的思想:利用字符串的公共前缀来降低时空开销。

他的典型应用是用于统计和排序大量的字符串(不仅限于字符串),经常被搜索引擎系统用来文本词频统计。

缺点:如果存在大量字符串且这些字符串基本没有公共前缀,则trie树将非常消耗内存。优点:最大限度的减少无谓的字符串比较。

总结:双链树和Trie树是键树的两种不同表示方法,从不同的存储结构特性可见,若键树中结点的度较大,则采用trie树结构较双链树更为合适。

5.后缀树和后缀数组

1)后缀树:键树只适合前缀匹配和全字匹配,并不适合后缀和字串匹配(后缀树适合)。后缀树和键树的最大不同在于,后缀树的单词集合是由指定字符串的后缀子串构成的。字符串“minimize”的后缀子串分别为:minimize,inimize,

Nimize,imize,mize,ize,ze,e.然后对这些子串的集合建立一棵键树,即为minimize的后缀树。

字符串s为BIBS则其建立的后缀树如下(常用于在串s中查询子串p是否存在)

查询效率:匹配成功正好比较了len次(查找单词长度)字符。查询效率为:O(len).

后缀树还可以用来找出字符串s的最长重复子串s1(比如abcdabcefda里abc同da都重复出现,而最长公共子串为abc).找出字符串s1和s2的最长公共子串(比如字符串acdfd和akdfc的最长公共子串为df).找出字符串s的最长回文子串s1(XMADAMYX的最长回文子串是MADAM)

2)后缀数组:后缀树实现较为复杂,通常可以用其变形后缀数组代替。

案例:有串abcdabcd,最长重复子串abcd.最长重复子串也可重叠,例如:abcdabcda,这时最长重复子串为abcda,中间a是被重叠的。思路:首先检查长度为n-1的字符串情况,如果不存在重复则检测n-2,一直递减下去,直到1.这种方法的时间复杂度:

改进的方法:利用后缀数组。后缀数组是一种数据结构。如输入字符串为“banana”该数组将表示这些后缀:a[0]:banana   a[1]:anana   a[2]:nana   a[3]:ana   a[4]:na  a[5]:a

数组a中的指针分别指向字符串中的每个后缀,所以叫后缀数组。然后对后缀数组进行快速排序,将后缀相近的子串集中在一起。排序如下:

a[0]:a  a[1]:ana   a[2]:anana   a[3]:banana  a[4]:na   a[5]:nana  最后使用comlen函数对数组进行扫描比较邻接元素,找出最长从夫的字符串。(p287)

时间复杂度分析:处理过程先对一个字符串生成相应的后缀数组(1),然后再排序(2),排完序一次检测相邻两个字符串的开头公共部分。其中生成后缀数组的时间复杂度O(n),

排序时间复杂度O(nlogn*n)最后边的n是因为字符串比较也是O(n),依次检测相邻的两个字符串的时间复杂度为,故总的时间复杂度为优先于

6.哈希表

1)哈希表的基本概念

哈希表也叫散列表,他是基于快速存取的角度设计的,是一种典型的空间换时间的做法。哈希表是普通数组的一种推广,因为数组可以直接寻址,故可在O(1)时间内访问数组的任意元素。哈希表是根据关键字(key value)而直接进行访问的数据结构。他将关键字通过某种规则映射到数组中某个位置,以加快查找的速度。这种映射规则称为哈希函数(散列函数)

存取记录的数组称为哈希表。哈希表建立了关键字和存储地址之间的一种直接映射关系。

哈希函数冲突:若多个不同的关键字通过哈希函数计算得到相同的数组下标,称其发生了冲突。这种发生冲突的不同关键字称为同义词。(设计好的hash函数应尽量减少这样冲突,由于这样的冲突是不可避免的,所以要设计好的处理冲突方法)

2)哈希函数

哈希函数特性:如果两个散列值是不相同的(根据同一函数),那么这两个散列的原始输入也是不相同的。这个特性使散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。典型的散列函数都有无限定义域,比如任意长度的字节字符串,和有限的值域,比如固定长度的比特串。

常用哈希函数介绍:数据结构课程中几种简单常用的散列函数如直接定址法,数字分析法,平方取中法,折叠法,除留余数法,随机数法,这些算法通常用于散列表中。

以下介绍工业界比较著名的哈希函数(哈希算法),这些算法通常用于信息安全领域。

典型的哈希算法包括MD4,MD5和SHA-1.MD5和SHA-1(安全哈希算法)是目前应用最广泛的的Hash算法,而他们都是以MD4为基础设计的。

MD4

MD(message digest缩写)他是一种测试信息完整性的密码散列函数的实现。其摘要长度为128位,一般128位长的MD4散列被表示为32位的16进制数字。(适用32位字长的处理器上,并用高速软件实现)

MD5

一种符合工业标准的单向128位哈希方案,以结果唯一并且不能返回到其原始格式的方式来转换数据(加密)。MD5比MD4来的复杂,并且速度较之要慢一点,但更安全,再抗分析和抗差分方面表现更好。

SHA-1

由美国国家标准发布的密码散列函数。SHA-1会从一个最大位元的信息中产生一串160位元的摘要,设计时基于和MD4相同的原理,模仿该算法。

这些哈希算法在信息安全方面应用主要体现在:1)文件校验2)数字签名3)鉴权协议

A)哈希算法是否可以用来加密?

哈希(Hash)算法把任意长度的输入通过哈希算法,变换成固定长度的输出,该输出就是哈希值(散列值)。(这种转换是一种压缩映射,使得散列值的空间通常远小于输入空间,不同的散列值可能会散列成相同的输出,而不能从散列值唯一确定输入值)。哈希算法是一种消息摘要算法,虽然哈希算法不是一种加密算法,但由于单向运算,具有一定的不可逆性使其成为加密算法中的一个重要构成部分。

B)处理冲突的方法中需要说明的两点:(1)在开放定址的情况下,不能随便删除表中已用元素,因为若删除元素将会截断其他具有相同散列地址的元素的查找地址。所以若想删除一个元素,给他做一个删除标记,进行逻辑删除。这样做的副作用是,执行多次删除后,表上看起来散列表很满,实际由很多位置没有利用,因此需要定期维护散列表,要把做删除标记的元素物理删除。(2)计算查找成功的平均查找长度ASL时,平均的概率时对表中当前非空元素而言的,并非整个表长。计算查找失败的平均查找长度ASL时,平均的概念时针对表长。

C)采用练地址法处理长度的时候,哈希表查找成功的平均长度与哪些因素有关?

与哈希表的装填因子有关。装填因子=表中填入的记录数/哈希表的长度。

7.一致性哈希(分布式存储方式)

如何快速定位数据在集群中的存储位置,关系到集群的性能。下面是几种常见的分布式存储方式。

(1)普通集群:把固定的Key映射到固定的结点上,结点只存放各自key的数据。

上图nodeA 中存放key值为a1,a2,a3......a8的数据。此种方法将key和结点的关系作为一张单独的表格进行维护,当其中一个结点宕机,节点上的数据需要迁移,此表格也要重新维护。

问题:当需要查找某个key值对应的数据时,必须遍历所有表格,直到寻找到存放此key值的结点,然后再去对应结点取数据,可见查找速度慢。

(2)hash集群:为了不维护普通集群表格,降低复杂性及其开销,容易想到对数据的key(假设是整形,如果不是,可通过一个哈希函数映射为整型)进行哈希(对结点数取模)。比如4个结点,如下图所示:

NodeA,NodeB等为服务器(结点),key1,key2等为数据的key,在寻找数据时,只需将key值对结点数取模,然后访问对应结点即可。

不足:假设某个时候其中一个结点宕机了,那这个结点的数据完全不可用(可通过数据迁移解决),如果采用数据迁移,因为此时结点少了,结点数变为3,对key重新模的话,大部分数据都要迁移,此时整个集群的数据都重新映射一遍才能达到效果。再者,如果整个分布式集群负载很高,希望增加结点来解决问题,这时迁移工作很麻烦复杂。(采用一致性哈希)

(3)一致性哈希

是一种哈希算法,在移除和添加一个结点时,他能够尽可能小的改变已存在key的映射关系。一致性哈希将整个哈希哈希值空间组织成一个虚拟的圆环,现将设某哈希函数Hash的值空间为0~-1(即哈希值是一个32位无符号整型)哈希空间环如图所示:

他的基本思想是:使用相同的哈希算法(哈希函数Hash)将数据和结点都映射到上图的环形哈希空间中。

a.把数据映射到哈希空间:通过哈希函数计算哈希值key在环上的分布,HASH(object1)=key1;

如图16-8.

B.把结点映射到哈希空间:具体选择结点服务器IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置。

假设当前由三台服务器(结点),其映射结果如16-9所示,他们在哈希空间中,以对应的哈希值排列。HASH(node A)=keyA;HASH(node B)=keyB;HASH(node C)=keyC;

c.把数据映射到结点

现在的结点和数据都已经通过同一哈希算法Hash映射到哈希数值空间中了,接下来进行将数据映射到结点。在环形空间中,沿顺时针方向(也可逆时针方向)从数据key出发,直到遇到一个结点机器,就将该数据存储在这个结点,因为数据和结点的哈希值是固定的,因此这个结点必然是唯一确定的。(数据结点的一一对应关系)如图:16-10.

将object1存储在结点NodeA.ojbect2,object3存储在结点nodeC.object4存储在NodeB .

一致性哈希中的三种操作:

A.移除结点

如果nodeB出现问题,受影响的将是沿nodeB逆时针遍历直到下一个node(nodeA)之间的数据。即本来映射到nodeB上的数据。变动object4,将其重新映射到nodeC上即可。如图16-11

b.添加结点

在已有nodeA,B,C的情况下,再添加一台新的node D的情况,假设再这个环形哈希空间中,nodeD被映射在数据object2和object3之间。这时受影响的仅是那些沿nodeD逆时针遍历直到下一个node(nodeB)之间的数据(他们本来映射到C)将这些数据重新映射到nodeD上。因为仅需变动数据object2,将其重新映射到nodeD上。如图16-12.

c.虚拟结点

在上边例子中将设仅部署nodeA,nodeC,则在4个数据中nodeA仅存储了object1,而nodeC存储object2,3,4可见分布式不平衡的。一致性哈希引入虚拟节点概念。

虚拟结点是实际结点在哈希空间的复制品,一个实际结点对应了若干“虚拟节点”。这个对应个数也称为复制个数,虚拟结点在哈希空间中以哈希值排列。仅部署nodeA和nodeC的情况为例,现在我们引入虚拟节点。并设置复制个数为2,意味着有4个虚拟结点,nodeA1,nodeA2代表nodeA.nodeC1,nodeC2代表nodeC.见图16-13

数据到虚拟结点的映射关系为:object1->node A2;object2->nodeA1;object3->nodeC1;object4->nodeC2.

因此object1,2映射到nodeA 上,而object3,4映射到nodeC上平衡性有了很大提高。

8.海量数据处理

是基于海量数据的查找,统计,运算等操作。所谓海量数据是指数据量太大,所以导致要么是无法在较短时间内迅速解决,要么数据太大,导致无法一次性装入内存。(传统os无法实现)

1)分治——Hash映射

散列函数特性:如果两个散列值是不相同的(同意哈希函数)则这两个散列值的原始输入也是不相同的。(此特性使散列函数具有确定性结果)(p289)

在对大文件进行处理时,如果文件过大,无法一次性装入内存,可考虑采取Hash映射的方法,将文件中的元素映射到不同小文件中,在依此处理小文件,最后合并处理降低问题规模。

Top k问题:在大规模数据处理中经常遇到一类问题,寻找最大的前k个数,或最小的k个数。若数据能一次性读入内存,则快排一次排序是时间复杂度为O(n)的解决办法。或使用堆(最大k个数用小顶堆,最小k个数用大顶堆)时间复杂度:O(nlogk),空间复杂度O(1)

在面对海量数据时,快排的一次划分不能再使用,依然可以使用堆,故堆是海量数据处理经常采用的工具

2)Bit-map

原理是使用位数组来表示某些元素是否存在,由于采用位bit为单位来存储数据,因此在存储方面,可以大大节省,故适用海量数据的快速查找,判重,删除等。

0

0

0

0

0

0

0

0

案例:将设我们要对值区间0~7的5个元素(4,7,2,5,3)排序。(假设这些元素没有重复)。可以采用bitmap的方法来达到排序的目的。要表示8个数,只需要8个bit(1bytes)

首先开辟1bytes的空间,将这些空间的所有bit位都设置为0

左侧为高位(第8位),右侧为低位(第一位)遍历5个元素,首先第一个元素为4,就把4对应的位置置1,因为从0开始,所以把第五位置1.

0

0

0

1

0

0

0

0

1

0

1

1

1

1

0

0

然后再处理第二个元素7,将第八位置为1.直到处理完所有元素,将相应的位置置1.这时内存bit位状态如下:

现在遍历一遍bit区域,将该为是1的编码输出(2,3,4,5,7)这样达到了排序的目的。位图排序的时间复杂度O(n)以空间换时间(需要一个n位的串)

例1:如何使用位逻辑运算实现Bit-map.要求表示的最大值为10,000,000.

#define BITWORD 32

#define SHIFT 5

#define MASK 0x1F

#define N 10000000

int a[1+N/BITWORD];//申请一个N位的串

void set(int i)  //将第i位设置为1,i>=0

{

a[i >> SHIFT] |= (1<<(i&MASK));   }

void clr(int i)  //将第i位设置为0

{

a[i >> SHIFT] &= ~(1 << (i&MASK));  }

void test(int i)  //返回第i位的状态

{

return a[i >> SHIFT] & (1 << (i&MASK));

}    //理解bitmap https://www.cnblogs.com/myseries/p/10880641.html

 

在程序设计中,经常需要判断集合中是否存在重复的问题,当数据量比较大时,位图较合适,还可用来快速判断集合中某个数据是否存在。应用见p291,294,295)

3)Bloom Filter(布隆过滤器):可以看作对Bitmap的扩展。

在判断一个元素是否在一个集合中,通常做法是把所有元素保存下来,然后比较他是否在集合中,链表何树都是基于这种思路,随着集合内元素个数的增加,需要的时间和空间线性变大,检索速度也越来越慢。

Bit-Map的做法是申请一个N位(集合中最大整数)数组,然后每一位对应一个特定整数。Bloom filter的基本原理是位数组与Hash函数联合使用。具体:bloom filter是一个包含m位的位数组,数组的每一位都初始化为0,然后定义k个不同的哈希函数,每个哈希函数都可以将集合中的元素映射到数组中的某一位。

当向集合中插入一个元素时,根据k个hash函数可以得到位数组中的k个位,将这些位全部置1.当要查询某个元素是否属于集合时,就使用k个哈希函数得到此元素对应得k个位,如果所有点都是1,那么元素在集合内,如果有0则元素不在集合内。

Bloom Filter是如何用位数组表示集合得。初始状态时,bloom Filter是一个包含m位的位数组。每一位都为0.

0

0

0

0

0

0

0

0

0

0

0

0

总结:bloom Filter的位数m通常要比集合中的最大元素小得多,bloom Filter是一种空间效率和时间效率很高的随机数据结构。但这种高效的代价是:在判断一个元素是否属于某个集合时,有可能把不属于这个集合的元素误认为属于这个集合。所以bloom filter不适合那些零误差场合。在容忍低错误率的场合下,bloom filter通过极少的错误换取村粗空间的极大节省。

4)倒排索引法

倒排索引也常被称为反向索引(置入档案或反向档案)是一种索引方法,被用来存储在全文检索下某个单词在一个文档或一组文档中的存储位置的映射。他是文档检索中最常用的数据结构。适用范围:搜索引擎的关键字查询。

案例:要被检索的文本为:T0=”it is what it is”   T1=”what is it”   T2=”it is a banana”

得到的反向文件索引:”a”:{2}  “banana”:{2}  “is”:{0,1,2}  “it”:{0,1,2}  “what”:{0,1}

当用户检索的条件为“what”,is和“it”则分别查询这三个关键字对应的文本集合,然后求对应集合的交集。可见,倒排索引在处理复杂的多关键字查询时,可在倒排表中先完成查询的交,并等逻辑,得到结果再对记录进行存取。

总结:倒排索引时相对正向索引而言的,正向索引是用来存储每个文档的单词的列表。在正向索引中,文档占据中心位置,每个文档指向一个他所包含的索引项的序列。也就是说文档指向了它包含的那些单词。而反向(倒排)索引则是单词指向了包含他的文档。

常识汇总:数据库中建立索引常用的数据结构是树。数组支持随机存取,链表不支持随机存取。数组插入,删除的时间复杂度为O(n)。哈希表不支持范围查询。

如果需要对磁盘上的1000万条记录构建索引,使用B-Ttree(数据结构)来存储索引最为合适。

发布了81 篇原创文章 · 获赞 17 · 访问量 6047

猜你喜欢

转载自blog.csdn.net/hopegrace/article/details/97888351