数据结构与算法实战 6.查找与散列
1 *****查找与散列***** 2 常用查找:顺序查找、二分查找 3 查找树:<delete>BST</delete>(这玩意其实并不适合查找,因为可能会长斜了逐渐靠近一维) 4 AVL树、红黑树 5 外存查找:B+树 B树 6 查找树的效率一般能达到O(logN),以k为底,因为B树不一定是二叉树,是多叉树 7 8 ***散列*** 9 目的:为了更快查找 O(1) 10 注意 不一定能达到O(1),当hash算出来的地址就是这个元素的地址的时候是O(1),当需要遍历的时候就O(n)了 11 拿空间换时间,提高时间效率 12 实际上散列是一个(key-value pair)的集合,例如map也可以用散列实现. 13 根据一个值算空间位置 14 15 散列函数有两个特点:第一,计算位置要尽量快 第二,元素要尽量散,否则会发生大量位置冲突 16 因为空间不能过分浪费,散列分配到的空间就有限,那么冲突是不可避免的。 17 那么冲突如何解决呢? 18 解决冲突一共有三个方法: 19 20 开放寻址法(Open Addressing) 允许一个元素在它本应在的位置被占之后去其他位置(一般是线性探测,往下一个去找) 21 注意,如果采用开放寻址方式,删除是lazy deletion,例如如果%11存进散列, 22 输入11,1,12,那么就是11,1,12(本来应该在1的位置,但是被1给占了),当我们查找余数为1的数时候, 23 我们先找到了1,然后继续往下找到了12,假设我们删除了1,我们找余数为1的数,就会先找到个空格,表示不存在 24 但是12还在啊,所以我们采用开放寻址的时候不能直接把元素给删除,只能把它标记上删除 25 26 分离链(Seperate Chaining) 一个位置上用一个链连接数据 可以想象每个位置上都是一条垂直往下的链 27 28 其它(公共溢出区等) 29 公共溢出区:指定一块公共的地方去储存那些本来位置被占用的元素 30 31 #include<cstdio> 32 #include<cstdlib> 33 34 enum GridStatus{Active, Removed, Empty}; 35 typedef enum GridStatus Status; 36 37 struct HashTable{ 38 int *key;//因为是C语言 简单用int表示key就完事了,C++和JAVA可以用模板/泛型 39 Status *status;//存格子的状态,单纯用int,还得规定哪个数字表示什么状态,不好使 40 int size; 41 //为了方便 我们可以用一个变量记录表满不满 来判断是否能插入 42 int remains; 43 }; 44 45 typedef struct HashTable* HT; 46 47 HT initHashTable(int size){ 48 HT h; 49 h = (HT)malloc(sizeof(struct HashTable)); 50 if(!h) return NULL;//空间不足 分配失败 为NULL 51 h->size = h->remains = size; 52 h->key = (int*)malloc(sizeof(int) * size); 53 if(!h->key){//可用空间不足,分配失败,为NULL 54 free(h); 55 return NULL; 56 } 57 h->status = (Status*)malloc(sizeof(Status) * size); 58 if(!h->status){//可用空间不足,分配失败,为NULL 59 free(h->key); 60 return NULL; 61 } 62 //初始化状态 为空,不然你不初始化的话,你创建之后你怎么判断能不能插入? 63 for(int i = 0; i < size; i ++){ 64 h->status[i] = Empty; 65 } 66 67 return h; 68 } 69 70 int isFull(const HT h){ 71 return h->remains == 0; 72 } 73 74 int hash(int x, int p){//prime素数 75 return x % p; 76 } 77 78 int insertX(int x, HT h){ 79 if(isFull(h)) return 0;//满了 没法插入 同时也保证了下面找位置一定能找得到 80 //如果没有isFull,我们也能判断满了与否,比如找到本应在的位置被占之后,用临时变量记录下来 81 //接着用这个位置继续往下找,找了一圈之后等于临时变量,那么就是满了,但是这种写法比较麻烦. 82 int pos = hash(x, h->size); 83 //有可能出现这样的情况 一个散列的存储位置除了第一个,后面都满了,那么我们找位置要循环回至0 84 while(h->status[pos] == Active){ 85 //被占了 86 //找下一个 87 pos = (pos + 1) % h->size; 88 } 89 h->key[pos] = x; 90 h->status[pos] = Active; 91 h->remains --; 92 return 1; 93 } 94 95 void printHT(const HT h){ 96 //打印下标 打印元素 打印标记 97 for(int i = 0; i < h->size; i ++){ 98 printf("%4d",i); 99 } 100 101 printf("\n"); 102 103 for(int i = 0; i < h->size; i ++){ 104 if(h->status[i] == Active) printf("%4d",h->key[i]); 105 else if(h->status[i] == Removed) printf(" X");//删除了 106 else printf(" -");//待插入 107 } 108 109 printf("\n"); 110 } 111 112 int findX(int x, HT h){ 113 //现在是线性探测,如果找本来x应该在的那个位置被其他元素占据了的话就继续一个个往下找 114 //当找了一圈,即又回到最初的七点,都没有找到 就是失败了 115 //另一种情况,既然前面没有这个元素的话,那么后面应该有这个元素,但是 116 //如果再在往前找的过程中碰到了空的格子,却没有这个元素,显然,这个元素不存在 117 int pos, index;//pos用来往后找 index用来标记刚开始的位置 118 index = pos = hash(x, h->size); 119 //碰到空格 or 找了一圈都算失败 120 while(h->status[pos] != Empty){ 121 if(h->key[pos] == x && h->status[pos] == Active) return pos; 122 pos = (pos + 1) % h->size; 123 if(pos == index) break;//转了一圈 124 } 125 return -1; 126 } 127 128 int removeX(int x,HT h){ 129 //删除一个不存在的元素的时候失败 130 int pos = findX(x, h); 131 if(pos == -1) return 0; 132 //不必要把那个位置清零,只要标记上就好了 133 //这个道理和计算机删除文件的道理是一致的 134 //为什么拷贝文件比删除文件要慢的多?因为删除文件只是操作系统把这些文件占用的空间标记为释放 135 h->status[pos] = Removed; 136 h->remains ++; 137 return 1; 138 } 139 140 int main(){ 141 /* 142 补充一点:一个数据结构创建好之后,肯定是存在有东西的,不存在真空的情况 143 所以我们要添加一个标记,告诉使用者,位置是空的,可以存放key 144 */ 145 //参数应该要有表的长度;另外哈希表长度数据为一个素数的时候,元素分布得较均匀 146 //但是用户调用的时候,传进来的不一定是一个素数,我们可以内部实现求用户输入的数后面的第一个素数就完事了 147 //mod 一个素数(用户输入的数的后面自然数里面的第一个素数) 148 //为了讲课方便,专注于哈希,代码不做素数求解 149 HT h = initHashTable(11);//在这里,简单把哈希函数H(x) = x mod 11 150 insertX(5, h); 151 insertX(16, h); 152 insertX(10, h); 153 insertX(21, h); 154 insertX(9, h); 155 insertX(20, h); 156 printHT(h); 157 removeX(21, h); 158 printHT(h);//测试这个是因为 删除 可能会 影响查找的操作 159 //查找 160 int i; 161 i = findX(21, h);//可能也会失败,规定返回-1失败 162 printf("%d\n",i); 163 removeX(21,h);//在h里面删除21 164 return 0; 165 } 166 167 ***散列在C++和JAVA中的使用*** 168 要注意散列只是一个数据的组织形式,并不真的是一个数据结构 169 另外,map和set在底层源码使用树实现,会排序; 170 如果我们对数据的顺序没有要求,只是对大负荷数据查找有要求,可以使用unordered_set unordered_map,是使用散列实现的 171 172 #include<iostream> 173 #include<map> 174 #include<set> 175 #include<unordered_map> 176 #include<unordered_set> 177 178 using namespace std; 179 180 int main(){ 181 unordered_set<int> s; 182 unordered_map<int,int> m; 183 //要注意下面方框的并不是下标 而是value 184 m[101] = 33; 185 m[-10] = 2223; 186 return 0; 187 } 188 189 import java.util.Map; 190 import java.util.Set; 191 import java.util.HashMap; 192 import java.util.TreeMap; 193 import java.util.HashSet; 194 import java.util.TreeSet; 195 //大量查找的时候hashmap是O(N),treemap是O(logN) 因为是后者是树的组织形式 196 //Map和Set都是JAVA的接口,要用下面的那些类去实体化 197 //注意JAVA里面没有m[value] = key的用法,只有put