基于八数码问题的hash判重

来源:互联网 发布:linux home空间 编辑:程序博客网 时间:2024/05/22 15:51
数据结构:数据间关系+数据存储方式

选择何种数据结构,取决于需要解决什么样的问题,任何一个数据结构都有它的优势,这个优势说白了就是“本数据结构在进行XX操作时快”,而选择何种数据结构就看要解决的问题需要在数据结构上进行何种操作来决定,哈希表就是体现这个道理的一个很好的例子。

哈希表提供这么一种其它数据结构并不擅长的操作:
“在理想情况下,能用常量时间查找到指定值的数据”。

普通数据结构如线性表、树、图等,其结点内的数据与数据所存储的位置之间的关系是随机的,所以要想提供“查找某已知值的数据的位置”只能通过“比较”方式进行,如:对于未排序的线性表,要从头至尾逐一比较是否“==”;对于二分查找,则要比较“>”“<”还是“==”。这样,时间复杂度是n或者logn。
而哈希表通过“在存储位置和数据值之间建立映射关系”这一手段,将该操作的时间复杂度降低为O(1)。
这个描述“在存储位置和数据值之间建立映射关系”的映射关系就是哈希函数。


将数据存入哈希表时,利用哈希函数为该数据安排存储位置;查找指定值数据时,也按照哈希函数得到目标索引。
实际操作起来时,由于数值域和索引域大小不同,所以不能简单地线性映射,而是需要建立较复杂的哈希函数,这就有可能造成“冲突”——这是哈希面临的主要问题。
好的哈希函数应该让随机数据值得到的哈希结果尽可能地随即和分散,而且减少冲突。

=======================================================================================

有这样一类问题,查找是否有出现多于一次的数据。
笨方法只能从头至尾逐一比对,复杂性为O(N*N)。
聪明一点的方法,如果数据不是很分散,可以用做记号的方法,用一个位数组(可用bool实现)记录各个位所代表的数据是否出现过,如果读入一个已经出现过的数据则说明它出现多于一次,用int代替bool,还可以记录下每个数据出现的次数,在遍历一次便可得到出现最多次的数据,复杂度为O(N)。
但是如果数据很分散,这种线性映射就不管用了,因为内存会严重浪费,如果数据过于夸张,会造成很大BUFFER的申请,但是这种映射记录的思想还是可取的,这时就需要
一个从很大的 《数据域》 向相对很小的《地址域》映射的工具来辅助,自然就是“哈希”。

紫书P198的八数码问题:

先附上代码:
#include <stdio.h>  #include <string.h>  #include <stdlib.h>  const int N = 1000000, HN = 1000003;  int head[HN], next[N];//链表(用于哈希)  int st[N][9], goal[9];  int dis[N];  const int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};    int Hash(int *st) {      int v = 0;      for(int i = 0; i < 9; i++)          v = v*10 + st[i];//恰如其分得将9个数字映射成9位数      return v % HN;//确保hash值不超过hash表大小  }    bool try_insert(int rear) {      int h = Hash(st[rear]);      int u = head[h];      while(u) {          if(!memcmp(st[u], st[rear], sizeof(st[0])))              return 0;//重复,返回假          u = next[u];      }      next[rear] = head[h];//rear指向旧的head[h]      head[h] = rear;//rear成为新的head[h],如此一来,就把rear插到链表的头上了      return 1;  }    int bfs() {      memset(head, 0, sizeof(head));//初始化查找表,其实就是表头们      int fron = 1, rear = 2;  //头和尾,可根据程序进一步理解    while (fron < rear) {          if (!memcmp(goal, st[fron], sizeof(st[0])))              return fron;//找到目标图          int z;          for(z = 0; z < 9; z++)          if(!st[fron][z])//找到白格              break;//更新z为队首的白格          int x = z / 3, y = z % 3;          for(int d = 0; d < 4; d++) {              int nx = x + dx[d], ny = y + dy[d], nz = 3*nx + ny;              if(nx >= 0&&nx < 3&&ny >= 0&&ny < 3) {//判断边界                  memcpy(&st[rear], &st[fron], sizeof(st[0]));                  st[rear][nz] = st[fron][z];                  st[rear][z] = st[fron][nz];//这是一次移动的尝试                  dis[rear] = dis[fron] + 1;                  if(try_insert(rear))//判重,若不重,则进队                      rear++;              }          }          fron++;//完成队首的尝试,队首出队。这个bfs和普通的bfs不太一样,st[][]其实就是队列,就是很多张图      }      return 0;  }    int main() {        for(int i = 0; i < 9; i++)          scanf("%d", &st[1][i]);      for(int i = 0; i < 9; i++)          scanf("%d", &goal[i]);      int ans = bfs();      if(ans > 0)          printf("%d\n", dis[ans]);      else          puts("-1");      return 0;  }
然后分析该问题的核心:判重。
常规方法是set判重,但每次判断都需要遍历一遍数据,效率很低。
代码:
set<int> vis;void init_look_up_table() { vis.clear(); }//初始化查找表int try_to_insert(int s) {    int v = 0;    for (int i = 0; i < 9; i++) v = v * 10 + st[s][i];//把每个结点的“图”变为整数以节省存储空间,后面会详细说明    if (vis.count(v)) return 0;//遍历一遍判断v是否出现过    vis.insert(v);    return 1;}

为提高效率,我们考虑用一个数组vis来存储某个数字是否出现,如vis【8】,若8出现过则vis【8】为1,否则为0;
如果要存储两个数字的出现情况,则需要一个二维数组,如vis【7】【8】,若7和8都出现过则vis【7】【8】为1,否则为0。
这样可以让数字作为数组下标,从而直接判断该数子是否出现过,无需循环遍历,效率很高。
但是,题目中需要判断9个数字,因此我们需要一个9维数组,即需要9^9个整形空间,这3亿多的空间是无法申请的,因此我们需要一种节约空间的方法,这就是hash技术。
ps:编码解码技术不是这次讨论的重点,因此这里只介绍hash技术。

首先我们要解决九维数组的问题,我们可以把结点变为整数,如某结点的9个数是1 2 3 4 5 6 7 8 0,我们就可以把它变为123456780这个9位的整数,看起来简化了很多,至少可以用一维数组来判断了,那么这样是否解决了空间浪费的问题呢? 答案是没有,因为转化后的整数面值是几亿,想通过数值做下标进行判断还是需要申请几亿的空间。
虽然我们将九维降为了一维,但申请的空间还是太大,但我们是否可以对转化后的整数做处理呢?比如说把它除以10,或是取除以10后的余数,不管怎样,我们的目的是把他缩小,试想我们如果把一亿缩到一百万,那么我们不就可以申请出空间来了吗?空间问题就可以解决了。但是缩小的过程存在很多问题,在这里我举一个例子:


假设你只能申请3个空间vis【3】,即只有vis【0】、vis【1】、vis【2】可以用,现在你需要快速判断(不用循环)3、4、 5、 6这三个数是否出现过,即用数字作为下标


直接判断,这时你需要vis【3】,vis【4】,vis【5】,vis【6】,但根据前提这些空间是分配不出来的,所以我们要将3、 4、 5、 6缩小到空间允许的范围内,可以考虑


取他们除以3的余数,这样就可以用数字做下标直接判断了,但问题来了,取余后这四个数变成了0,1,2,0, 0出现了两次,3和6的哈希值都是0,那么vis【0】究竟对应着


3还是6呢?3和6肯定都要进行判断,因此我们需要在哈希值相同的数之间建立联系,以便将他们都进行判断,这就用到了链表,至此我们来分析八数码问题的程序。

紫书P201的原话是代码的原理:“简单的说,就是要把结点变成整数,但不必是一一对应(例如我上面的例子中没有用vis【4】而用的vis【1】,不用严格的把所给的需要判断
的数作为下标(如4),也可以用他的hash值(如1)),只需设计一个所谓的hash函数,然后将任意结点映射到某个给定范围【0,M-1】的整数即可,其中M是程序员根据内存
大小《自选》的,在理想的情况下,只需开一个M大小的数组就可以完成判重,但此时往往会有不同结点的哈希值相同,因此需要把哈希值相同的状态组织成链表”。


main函数容易理解,bfs函数整体上加了注释也容易懂,关键是bfs中的memset、try_to_insert以及hash函数,还有一些数组的含义难懂,下面我会一一解释(代码和书上的略有
不同,但并不影响理解)


goal是目标数组,用来存储终止情形,dis是每个结点对应的步数,st【】【】是每个结点的情况,即每个结点对应的9个数字(因节点很多所以开的很大),head以hash值作为
下标,存储hash值对应的结点,对于一个hash值对应多个结点的情况,我们用next令他们形成链表,next的下标是结点位置,存储着《前》一个结点的位置。

程序中的hash函数就是一个映射函数,将9个数映射成1个9位数,又将这个9位数缩小,映射到程序员自定的范围HN = 1000003中,十分关键。

我对这个函数做出更详细的解释:
bool try_insert(int rear) {//接受一个结点的位置      int h = Hash(st[rear]); //用h记录这个结点映射的hash值    int u = head[h];//head经memset初始化都为0,因此如果rear结点对应的hash值没出现过u就为0,就可以把rear结点插入,如果出现过,还应判断是不是上边我们讨论的hash值相同问题,因此有了以下循环    while(u) {          if(!memcmp(st[u], st[rear], sizeof(st[0])))              return 0;//重复,返回假          u = next[u];  //这里有些人想问为什么next没有初始化,其实仔细观察下面的程序就可以发现next在每条链的结尾都赋值了0    }//经过上述代码,现在的结点是可以插入的,对应的hash值要么没出现过,要么发生了hash值重复问题但是合法,下面就要进行链表的链接和节点的插入      next[rear] = head[h];//rear指向旧的head[h]      head[h] = rear;//rear成为新的head[h],如此一来,就把rear插到链表的头上了      return 1;  } 
1 0
原创粉丝点击