【数据结构】散列表

      散列表是典型的以空间换取时间的数据结构;它在插入、删除、查找操作上也具有常数平均时间的表现,但是这种表现是以统计为基础的


基本知识

(1)负载系数,指元素的个数除以表格大小,除非采用开链法(拉链法),否则负载系数应该在0~1之间;

(2)对应像字符串类型、float这样的类型,如何映射到表上,这时候使用散列函数hash function;hash function带来的问题是可能有不同的元素被映射到相同的位置,这是无法避免的,因为元素的个数有可能会大于表的大小,这就是碰撞问题;如nginx中有这样一个散列函数:

#define ngx_hash(key,c)   ((ngx_uint_t) key * 31 + c)

//求槽的位置
ngx_uint_t
ngx_hash_key(u_char *data,size_t len)
{
    ngx_uint_t  i,key;

    key = 0;

    for (i = 0; i < len; i++) {
        key = ngx_hash(key,data[i]);	//key的循环利用
    }

    return key;
}

(3)解决碰撞的问题,线性探测(线性探测是逐个往下找,遇到惰性删除记号或空元素即可;因此要求表格足够大,但是该假设比较不符合实际,会带来一次群集问题);二次探测(H为计算的槽位置,二次探测是使用H+1^2, H-1^2, H+2^2, H-2^2,......H+i^2, .H-i^2;但是也会带来二次群集的问题);

(4)在nginx中散列表的建立会预先采用探测的方法来计算合适的表大小,因为nginx中的散列表不支持删除,只能够一次性创建,能够利用探测的方法计算出合适的空间大小;详情请见nginx专题中的hash的介绍;

(5)采用开链法,对每一个槽对于冲突的元素建立一个链表,只要链表足够短,速度还是够快的在stl中,对于元素个数过多时,会动态调整表的大小,使得冲突的链表元素尽可能小;采用开链法,表格的负载系数将会大于1;


代码分析

(本节选取stl中hashtable的相关代码进行分析)

表格的质数表

static const int __stl_num_primes = 28;
//先将28个质数计算好
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
  53,97,193,389,769,1543,3079,6151,12289,24593,49157,98317,196613,393241,786433,1572869,3145739,6291469,12582917,25165843,50331653,100663319,201326611,402653189,805306457,1610612741,3221225473ul,4294967291ul
};
说明几点

(1)在stl中,表格的大小选取为一个最靠近某个数,并大于等于某个数的质数;通过上述表格来获取,总共有28个质数;


获得合适的表格大小

inline unsigned long __stl_next_prime(unsigned long n)
{
  const unsigned long* first = __stl_prime_list;
  const unsigned long* last = __stl_prime_list + __stl_num_primes;
  const unsigned long* pos = lower_bound(first,last,n);   //找到的是第一个小于等于n的某个质数,若n不存在,返回一个不小于n的元素
  return pos == last ? *(last - 1) : *pos;		//先判别pos是否与last相等,相等则返回最后一个质数,否则返回相应位置的质数
}


桶节点

//这个是hashtable的桶所维护的节点
template <class Value>
struct __hashtable_node
{
  __hashtable_node* next;   //连接下一个桶
  Value val;                //元素值
};  

  //创建节点
  node* new_node(const value_type& obj)			//节点空间配置
  {
    node* n = node_allocator::allocate();		//申请一块内存
    n->next = 0;
    __STL_TRY {
      construct(&n->val,obj);		//构造
      return n;
    }
    __STL_UNWIND(node_allocator::deallocate(n));
  }
  
  //删除节点
  void delete_node(node* n)			//节点空间释放
  {
    destroy(&n->val);
    node_allocator::deallocate(n);
  }

hastable中的表结构

  typedef __hashtable_node<Value> node;

  vector<node*,Alloc> buckets;		//buckets是以vector来做桶的


构造散列表

  //hashtable的构造函数
  hashtable(size_type n,const HashFcn&    hf,const EqualKey&   eql)
    : hash(hf),equals(eql),get_key(ExtractKey()),num_elements(0)
  {
    initialize_buckets(n);
  }

  size_type next_size(size_type n) const { return __stl_next_prime(n); }    //获得表的大小

  void initialize_buckets(size_type n)
  {
    const size_type n_buckets = next_size(n);		//知道合适的表大小
    buckets.reserve(n_buckets);			            //创建表
    buckets.insert(buckets.end(),n_buckets,(node*) 0);  //每一个表插入节点,为空指针
    num_elements = 0;                            //元素的个数为0
  }
说明几点

(1)buckets中插入的元素为NULL指针;


判断元素的插入位置

  size_type bkt_num_key(const key_type& key) const
  {
    return bkt_num_key(key,buckets.size());
  }

  //只接受实值
  size_type bkt_num(const value_type& obj) const
  {
    return bkt_num_key(get_key(obj));
  }

  size_type bkt_num_key(const key_type& key,size_t n) const
  {
    return hash(key) % n;		//SGI所有内建的hash()
  }

  //接受实值和buckets个数
  size_type bkt_num(const value_type& obj,size_t n) const
  {
    return bkt_num_key(get_key(obj),n);
  }

散列函数

//对于字符串类型char*的函数的转换函数
inline size_t __stl_hash_string(const char* s)
{
  unsigned long h = 0; 
  for ( ; *s; ++s)
    h = 5*h + *s;
  
  return size_t(h);
}

__STL_TEMPLATE_NULL struct hash<char*>
{
  size_t operator()(const char* s) const { return __stl_hash_string(s); }
};

__STL_TEMPLATE_NULL struct hash<const char*>
{
  size_t operator()(const char* s) const { return __stl_hash_string(s); }
};

__STL_TEMPLATE_NULL struct hash<char> {
  size_t operator()(char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned char> {
  size_t operator()(unsigned char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<signed char> {
  size_t operator()(unsigned char x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<short> {
  size_t operator()(short x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned short> {
  size_t operator()(unsigned short x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<int> {
  size_t operator()(int x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned int> {
  size_t operator()(unsigned int x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<long> {
  size_t operator()(long x) const { return x; }
};
__STL_TEMPLATE_NULL struct hash<unsigned long> {
  size_t operator()(unsigned long x) const { return x; }
};

散列表插入元素(元素值不允许重复)

  //插入元素,元素值不允许重复
  pair<iterator,bool> insert_unique(const value_type& obj)
  {
    resize(num_elements + 1);   //判断是否需要重建表格
    return insert_unique_noresize(obj);   //插入元素,键值不允许重复
  }

//判断是否需要重建表格
template <class V,class K,class HF,class Ex,class Eq,class A>
void hashtable<V,K,HF,Ex,Eq,A>::resize(size_type num_elements_hint)
{
  const size_type old_n = buckets.size();		//buckets中的表格大小
  if (num_elements_hint > old_n) {		//现有的元素个数大于以前的表格大小时
    const size_type n = next_size(num_elements_hint);		//找到下一个新的质数
    if (n > old_n) {
      vector<node*,A> tmp(n,(node*) 0);       //创建新的bucket
      __STL_TRY {
        for (size_type bucket = 0; bucket < old_n; ++bucket) {  //旧的
          node* first = buckets[bucket];		//节点的本身
          while (first) {
            size_type new_bucket = bkt_num(first->val,n);		//计算新的插入位置,因为n改变了
            buckets[bucket] = first->next;			//旧的散列表先指向下一个元素
            first->next = tmp[new_bucket];			//first指向新的散列表中元素指针
            tmp[new_bucket] = first;				    //新的散列表指向first
            first = buckets[bucket];				    //再次指向旧的散列表的指针
          }
        }
        buckets.swap(tmp);      //交换
      }
  }
}

//在不需要重建表格的情况下,元素值是不允许重复的
template <class V,class A>
pair<typename hashtable<V,A>::iterator,bool> 
hashtable<V,A>::insert_unique_noresize(const value_type& obj)
{
  const size_type n = bkt_num(obj);   //计算插入的位置
  node* first = buckets[n];       

  for (node* cur = first; cur; cur = cur->next) 
    if (equals(get_key(cur->val),get_key(obj)))	//元素值是不允许相同的
      return pair<iterator,bool>(iterator(cur,this),false);

  node* tmp = new_node(obj);	//产生新的节点
  tmp->next = first;			//指向头元素
  buckets[n] = tmp;				//表中指针指向tmp
  ++num_elements;			  	//更新节点的个数
  return pair<iterator,bool>(iterator(tmp,true);
}


散列表插入元素(元素值允许重复)

  iterator insert_equal(const value_type& obj)
  {
    resize(num_elements + 1);   //是否重建表格
    return insert_equal_noresize(obj);  //插入元素,元素值可以重复
  }


//允许元素值重复
template <class V,class A>
typename hashtable<V,A>::iterator 
hashtable<V,A>::insert_equal_noresize(const value_type& obj)
{
  const size_type n = bkt_num(obj);   //找到插入位置
  node* first = buckets[n];

  for (node* cur = first; cur; cur = cur->next) 
    if (equals(get_key(cur->val),get_key(obj))) {  //相等时
      
	   //马上插入,并返回
	    node* tmp = new_node(obj);
      tmp->next = cur->next;    //指向相等元素的下一个指向
      cur->next = tmp;          //相等元素指向新的插入
      ++num_elements;
      return iterator(tmp,this);
    }

  //插入新的元素,元素值并没有重复
  node* tmp = new_node(obj);
  tmp->next = first;
  buckets[n] = tmp;
  ++num_elements;
  return iterator(tmp,this);
}

查找元素

  //找到某个键值
  iterator find(const key_type& key) 
  {
    size_type n = bkt_num_key(key);   //找到插入位置
    node* first;
    for ( first = buckets[n];         //从首指针开始查找判断
          first && !equals(get_key(first->val),key);	//判断键值相同的
          first = first->next)
      {}
    return iterator(first,this);
  } 


删除元素

//删除所有与节点键值相同的元素
template <class V,A>::size_type 
hashtable<V,A>::erase(const key_type& key)
{
  const size_type n = bkt_num_key(key);     //找到插入位置
  node* first = buckets[n];     //第一个元素的指针
  size_type erased = 0;

  if (first) {
    node* cur = first;    //当前指针,首指针先跳过
    node* next = cur->next; //下一个指针
    while (next) {  //下一个元素和key来比
      if (equals(get_key(next->val),key)) {    //节点的键值相同
        cur->next = next->next;   //先改变当前指针的指向
        delete_node(next);        //删除
        next = cur->next;         //next
        ++erased;
        --num_elements;
      }
      else {
        cur = next;         
        next = cur->next;  
      }
    }
    if (equals(get_key(first->val),key)) { //若首指针也相同,也要删除
      buckets[n] = first->next;     //buckets指向下一个
      delete_node(first);           //删除
      ++erased;
      --num_elements;
    }
  }
  return erased;
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


【啊哈!算法】算法3:最常用的排序——快速排序       上一节的冒泡排序可以说是我们学习第一个真正的排序算法,并且解决了桶排序浪费空间的问题,但在算法的执行效率上却牺牲了很多,它的时间复杂度达到了O(N2)。假如我们的计算机每秒钟可以运行10亿次,那么对1亿个数进行排序,桶排序则只需要0.1秒,而冒泡排序则需要1千万秒,达到115天之久,是不是很吓人。那有没有既不浪费空间又可以快一点的排序算法
匿名组 这里可能用到几个不同的分组构造。通过括号内围绕的正则表达式就可以组成第一个构造。正如稍后要介绍的一样,既然也可以命名组,大家就有考虑把这个构造作为匿名组。作为一个实例,请看看下列字符串: “08/14/57 46 02/25/59 45 06/05/85 18 03/12/88 16 09/09/90 13“ 这个字符串就是由生日和年龄组成的。如果需要匹配年两而不要生日,就可以把正则
选择排序:从数组的起始位置处开始,把第一个元素与数组中其他元素进行比较。然后,将最小的元素方式在第0个位置上,接着再从第1个位置开始再次进行排序操作。这种操作一直到除最后一个元素外的每一个元素都作为新循环的起始点操作过后才终止。 public void SelectionSort() { int min, temp;
public struct Pqitem { public int priority; public string name; } class CQueue { private ArrayList pqueue; public CQueue() { pqueue
在编写正则表达式的时候,经常会向要向正则表达式添加数量型数据,诸如”精确匹配两次”或者”匹配一次或多次”。利用数量词就可以把这些数据添加到正则表达式里面了。 数量词(+):这个数量词说明正则表达式应该匹配一个或多个紧紧接其前的字符。 string[] words = new string[] { "bad", "boy", "baad", "baaad" ,"bear", "b
来自:http://blog.csdn.net/morewindows/article/details/6678165/归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列
插入排序算法有两层循环。外层循环会啄个遍历数组元素,而内存循环则会把外层循环所选择的元素与该元素在数组内的下一个元素进行比较。如果外层循环选择的元素小于内存循环选择的元素,那么瘦元素都想右移动以便为内存循环元素留出位置。 public void InsertionSort() { int inner, temp;
public int binSearch(int value) { int upperBround, lowerBound, mid; upperBround = arr.Length - 1; lowerBound = 0; while (lowerBound <= upper
虽然从表内第一个节点到最后一个节点的遍历操作是非常简单的,但是反向遍历链表却不是一件容易的事情。如果为Node类添加一个字段来存储指向前一个节点的连接,那么久会使得这个反向操作过程变得容易许多。当向链表插入节点的时候,为了吧数据复制给新的字段会需要执行更多的操作,但是当腰吧节点从表移除的时候就能看到他的改进效果了。 首先需要修改Node类来为累增加一个额外的链接。为了区别两个连接,这个把指
八、树(Tree)树,顾名思义,长得像一棵树,不过通常我们画成一棵倒过来的树,根在上,叶在下。不说那么多了,图一看就懂:当然了,引入了树之后,就不得不引入树的一些概念,这些概念我照样尽量用图,谁会记那么多文字?树这种结构还可以表示成下面这种方式,可见树用来描述包含关系是很不错的,但这种包含关系不得出现交叉重叠区域,否则就不能用树描述了,看图:面试的时候我们经常被考到的是一种叫“二叉树”的结构,二叉
Queue的实现: 就像Stack类的实现所做的一样,Queue类的实现用ArrayList简直是毋庸置疑的。对于这些数据结构类型而言,由于他们都是动态内置的结构,所以ArrayList是极好的实现选择。当需要往队列中插入数据项时,ArrayList会在表中把每一个保留的数据项向前移动一个元素。 class CQueue { private ArrayLis
来自:http://yingyingol.iteye.com/blog/13348911 快速排序介绍:快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地
Stack的实现必须采用一种基本结构来保存数据。因为再新数据项进栈的时候不需要担心调整表的大小,所以选择用arrayList.using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;using System.Collecti
数组类测试环境与排序算法using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Threading.Tasks;namespace Data_structure_and_algorithm{ class CArray { pr
一、构造二叉树 二叉树查找树由节点组成,所以需要有个Node类,这个类类似于链表实现中用到的Node类。首先一起来看看Node类的代码。 public class Node { public int Data; public Node Left; public Node Right; public v
二叉树是一种特殊的树。二叉树的特点是每个结点最多有两个儿子,左边的叫做左儿子,右边的叫做右儿子,或者说每个结点最多有两棵子树。更加严格的递归定义是:二叉树要么为空,要么由根结点、左子树和右子树组成,而左子树和右子树分别是一棵二叉树。 下面这棵树就是一棵二叉树。         二叉树的使用范围最广,一棵多叉树也可以转化为二叉树,因此我们将着重讲解二叉树。二叉树中还有连两种特殊的二叉树叫做满二叉树和
上一节中我们学习了队列,它是一种先进先出的数据结构。还有一种是后进先出的数据结构它叫做栈。栈限定只能在一端进行插入和删除操作。比如说有一个小桶,小桶的直径只能放一个小球,我们现在向小桶内依次放入2号、1号、3号小球。假如你现在需要拿出2号小球,那就必须先将3号小球拿出,再拿出1号小球,最后才能将2号小球拿出来。在刚才取小球的过程中,我们最先放进去的小球最后才能拿出来,而最后放进去的小球却可以最先拿
msdn中的描述如下:(?= 子表达式)(零宽度正预测先行断言。) 仅当子表达式在此位置的右侧匹配时才继续匹配。例如,w+(?=d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。(?(零宽度正回顾后发断言。) 仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?此构造不会回溯。msdn描述的比较清楚,如:w+(?=ing) 可以匹配以ing结尾的单词(匹配结果不包括ing),(
1.引入线索二叉树 二叉树的遍历实质上是对一个非线性结构实现线性化的过程,使每一个节点(除第一个和最后一个外)在这些线性序列中有且仅有一个直接前驱和直接后继。但在二叉链表存储结构中,只能找到一个节点的左、右孩子信息,而不能直接得到节点在任一遍历序列中的前驱和后继信息。这些信息只有在遍历的动态过程中才能得到,因此,引入线索二叉树来保存这些从动态过程中得到的信息。 2.建立线索二叉树 为了保
排序与我们日常生活中息息相关,比如,我们要从电话簿中找到某个联系人首先会按照姓氏排序、买火车票会按照出发时间或者时长排序、买东西会按照销量或者好评度排序、查找文件会按照修改时间排序等等。在计算机程序设计中,排序和查找也是最基本的算法,很多其他的算法都是以排序算法为基础,在一般的数据处理或分析中,通常第一步就是进行排序,比如说二分查找,首先要对数据进行排序。在Donald Knuth 的计算机程序设