文章目录
- 什么是单向链表,如何判断两个单向链表是否相交
- https://blog.youkuaiyun.com/tanrui519521/article/details/80980135
- 红黑树的性质还有左右旋转
- 红黑树和AVL树的定义,特点,以及二者区别
- 哈夫曼编码
- map底层为什么用红黑树实现
- B+树
- map和unordered_map的底层实现
- map和unordered_map优点和缺点
- epoll怎么实现的
- Top(K)问题
- stack overflow,并举个简单例子导致栈溢出
- 栈和堆的区别,以及为什么栈要快
- 两个栈实现一个队列
- Array&List, 数组和链表的区别
- 一个长度为n的整形数组,数组中每个元素的取值范围是[0,n-1],判断该数组否有重复的数
- 快排算法;以及什么是稳定性排序,快排是稳定性的吗;快排算法最差情况推导公式
- 手写一下快排的代码
- 介绍一下各种排序算法及时间复杂度
- hash表的实现,包括STL中的哈希桶长度常数
- hash表如何rehash,以及怎么处理其中保存的资源
- 哈希表的桶个数为什么是质数,合数有何不妥?
- 解决hash冲突的方法
- 最长公共连续子序列
- 求一个字符串最长回文子串
- 合并两个有序链表
- 反转链表
- 判断一个链表是否为回文链表
- 小根堆特点
- 哈希冲突的解决方法
- 加密方法都有哪些
- LRU缓存
- 洗牌算法
什么是单向链表,如何判断两个单向链表是否相交
1、单向链表
单向链表(单链表)是链表的一种,其特点是链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始;链表是使用指针进行构造的列表;又称为结点列表,因为链表是由一个个结点组装起来的;其中每个结点都有指针成员变量指向列表中的下一个结点。
列表是由结点构成,head指针指向第一个成为表头结点,而终止于最后一个指向nuLL的指针。
2、判断两个链表是否相交
1)方法1:
链表相交之后,后面的部分节点全部共用,可以用2个指针分别从这两个链表头部走到尾部,最后判断尾部指针的地址信息是否一样,若一样则代表链表相交!
2)方法2:
可以把其中一个链表的所有节点地址信息存到数组中,然后把另一个链表的每一个节点地址信息遍历数组,若相等,则跳出循环,说明链表相交。进一步优化则是进行hash排序,建立hash表。
https://blog.youkuaiyun.com/tanrui519521/article/details/80980135
红黑树的性质还有左右旋转
1)平衡二叉树(AVL树):
红黑树是在AVL树的基础上提出来的。
平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。
AVL树中所有结点为根的树的左右子树高度之差的绝对值不超过1。
将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
2)红黑树:
红黑树是在AVL树的基础上发展而来的。红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。
性质:
-
每个节点非红即黑
-
根节点是黑的;
-
每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;
-
如果一个节点是红色的,则它的子节点必须是黑色的。
-
对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点;
从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。恢复红黑属性需要少量(O(log n))的颜色变更(这在实践中是非常快速的)并且不超过三次树旋转(对于插入是两次)。这允许插入和删除保持为 O(log n) 次,
3)红黑树较AVL树的优点:
AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。
所以红黑树在查找,插入删除的性能都是O(logn),且性能稳定,所以STL里面很多结构包括map底层实现都是使用的红黑树。
4)红黑树旋转:
旋转:红黑树的旋转是一种能保持二叉搜索树性质的搜索树局部操作。有左旋和右旋两种旋转,通过改变树中某些结点的颜色以及指针结构来保持对红黑树进行插入和删除操作后的红黑性质。
左旋:对某个结点x做左旋操作时,假设其右孩子为y而不是T.nil:以x到y的链为“支轴”进行。使y成为该子树新的根结点,x成为y的左孩子,y的左孩子成为x的右孩子。
右旋:对某个结点x做右旋操作时,假设其左孩子为y而不是T.nil:以x到y的链为“支轴”进行。使y成为该子树新的根结点,x成为y的右孩子,y的右孩子成为x的左孩子。
红黑树和AVL树的定义,特点,以及二者区别
平衡二叉树(AVL树):
平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。一句话表述为:**以树中所有结点为根的树的左右子树高度之差的绝对值不超过1。**将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
红黑树:
红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。
性质:
-
每个节点非红即黑
-
根节点是黑的;
-
每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;
-
如果一个节点是红色的,则它的子节点必须是黑色的。
-
对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点;
区别:
AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。
哈夫曼编码
哈夫曼编码是哈夫曼树的一种应用,广泛用于数据文件压缩。哈夫曼编码算法用字符在文件中出现的频率来建立使用0,1表示个字符的最优表示方式,其具体算法如下:
(1)哈夫曼算法以自底向上的方式构造表示最优前缀码的二叉树T。
(2)算法以|C|个叶结点开始,执行|C|-1次的“合并”运算后产生最终所要求的树T。
(3)假设编码字符集中每一字符c的频率是f©。以f为键值的优先队列Q用在贪心选择时有效地确定算法当前要合并的2棵具有最小频率的树。一旦2棵具有最小频率的树合并后,产生一棵新的树,其频率为合并的2棵树的频率之和,并将新树插入优先队列Q。经过n-1次的合并后,优先队列中只剩下一棵树,即所要求的树T。
给定一个数字数组,返回哈夫曼树的头指针
struct BTreeNode* CreateHuffman(ElemType a[], int n)
{
int i, j;
struct BTreeNode **b, *q;
b = malloc(n*sizeof(struct BTreeNode));
for (i = 0; i < n; i++)
{
b[i] = malloc(sizeof(struct BTreeNode));
b[i]->data = a[i];
b[i]->left = b[i]->right = NULL;
}
for (i = 1; i < n; i++)
{
int k1 = -1, k2;
for (j = 0; j < n; j++)
{
if (b[j] != NULL && k1 == -1)
{
k1 = j;
continue;
}
if (b[j] != NULL)
{
k2 = j;
break;
}
}
for (j = k2; j < n; j++)
{
if (b[j] != NULL)
{
if (b[j]->data < b[k1]->data)
{
k2 = k1;
k1 = j;
}
else if (b[j]->data < b[k2]->data)
k2 = j;
}
}
q = malloc(sizeof(struct BTreeNode));
q->data = b[k1]->data + b[k2]->data;
q->left = b[k1];
q->right = b[k2];
b[k1] = q;
b[k2] = NULL;
}
free(b);
return q;
}
map底层为什么用红黑树实现
1、红黑树:
红黑树是一种二叉查找树,但在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,因此,红黑树是一种弱平衡二叉树,相对于要求严格的AVL树来说,它的旋转次数少,所以对于搜索,插入,删除操作较多的情况下,通常使用红黑树。
性质:
-
每个节点非红即黑
-
根节点是黑的;
-
每个叶节点(叶节点即树尾端NULL指针或NULL节点)都是黑的;
-
如果一个节点是红色的,则它的子节点必须是黑色的。
-
对于任意节点而言,其到叶子点树NULL指针的每条路径都包含相同数目的黑节点;
2、平衡二叉树(AVL树):
红黑树是在AVL树的基础上提出来的。
平衡二叉树又称为AVL树,是一种特殊的二叉排序树。其左右子树都是平衡二叉树,且左右子树高度之差的绝对值不超过1。
AVL树中所有结点为根的树的左右子树高度之差的绝对值不超过1。
将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的。
3、红黑树较AVL树的优点:
AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树不是高度平衡的,算是一种折中,插入最多两次旋转,删除最多三次旋转。
所以红黑树在查找,插入删除的性能都是O(logn),且性能稳定,所以STL里面很多结构包括map底层实现都是使用的红黑树。
B+树
B+是一种多路搜索树,主要为磁盘或其他直接存取辅助设备而设计的一种平衡查找树,在B+树中,**每个节点的可以有多个孩子,并且按照关键字大小有序排列。所有记录节点都是按照键值的大小顺序存放在同一层的叶节点中。**相比B树,其具有以下几个特点:
每个节点上的指针上限为2d而不是2d+1(d为节点的出度)
内节点不存储data,只存储key
叶子节点不存储指针
map和unordered_map的底层实现
map底层是基于红黑树实现的,因此map内部元素排列是有序的。而unordered_map底层则是基于哈希表实现的,因此其元素的排列顺序是杂乱无序的。
map和unordered_map优点和缺点
对于map,其底层是基于红黑树实现的,优点如下:
1)有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
2)map的查找、删除、增加等一系列操作时间复杂度稳定,都为logn
缺点如下:
1)查找、删除、增加等操作平均时间复杂度较慢,与n相关
对于unordered_map来说,其底层是一个哈希表,优点如下:
查找、删除、添加的速度快,时间复杂度为常数级O©
缺点如下:
因为unordered_map内部基于哈希表,以(key,value)对的形式存储,因此空间占用率高
Unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O©,取决于哈希函数。极端情况下可能为O(n)
map | unorder_map | |
---|---|---|
数据结构 | 红黑树有序 | 哈希表无序 |
速度 | 稳定logn | c到n之间 |
epoll怎么实现的
Linux epoll机制是通过红黑树和双向链表实现的。 首先通过epoll_create()系统调用在内核中创建一个eventpoll类型的句柄,其中包括红黑树根节点和双向链表头节点。然后通过epoll_ctl()系统调用,向epoll对象的红黑树结构中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。最后通过epoll_wait()系统调用判断双向链表是否为空,如果为空则阻塞。当文件描述符状态改变,fd上的回调函数被调用,该函数将fd加入到双向链表中,此时epoll_wait函数被唤醒,返回就绪好的事件。
Top(K)问题
1、直接全部排序(只适用于内存够的情况)
当数据量较小的情况下,内存中可以容纳所有数据。则最简单也是最容易想到的方法是将数据全部排序,然后取排序后的数据中的前K个。
这种方法对数据量比较敏感,当数据量较大的情况下,内存不能完全容纳全部数据,这种方法便不适应了。即使内存能够满足要求,该方法将全部数据都排序了,而题目只要求找出top K个数据,所以该方法并不十分高效,不建议使用。
2、快速排序的变形 (只使用于内存够的情况)
这是一个基于快速排序的变形,因为第一种方法中说到将所有元素都排序并不十分高效,只需要找出前K个最大的就行。
这种方法类似于快速排序,首先选择一个划分元,将比这个划分元大的元素放到它的前面,比划分元小的元素放到它的后面,此时完成了一趟排序。如果此时这个划分元的序号index刚好等于K,那么这个划分元以及它左边的数,刚好就是前K个最大的元素;**如果index > K,**那么前K大的数据在index的左边,那么就继续递归的从index-1个数中进行一趟排序;如果index < K,那么再从划分元的右边继续进行排序,直到找到序号index刚好等于K为止。再将前K个数进行排序后,返回Top K个元素。这种方法就避免了对除了Top K个元素以外的数据进行排序所带来的不必要的开销。
3、最小堆法
这是一种局部淘汰法。先读取前K个数,建立一个最小堆。然后将剩余的所有数字依次与最小堆的堆顶进行比较,如果小于或等于堆顶数据,则继续比较下一个;否则,删除堆顶元素,并将新数据插入堆中,重新调整最小堆。当遍历完全部数据后,最小堆中的数据即为最大的K个数。
4、分治法
将全部数据分成N份,前提是每份的数据都可以读到内存中进行处理,找到每份数据中最大的K个数。此时剩下N*K个数据,如果内存不能容纳NK个数据,则再继续分治处理,分成M份,找出每份数据中最大的K个数,如果MK个数仍然不能读到内存中,则继续分治处理。直到剩余的数可以读入内存中,那么可以对这些数使用快速排序的变形或者归并排序进行处理。
5、Hash法
如果这些数据中有很多重复的数据,可以先通过hash法,把重复的数去掉。这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间。处理后的数据如果能够读入内存,则可以直接排序;否则可以使用分治法或者最小堆法来处理数据。
stack overflow,并举个简单例子导致栈溢出
栈溢出概念:
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致栈中与其相邻的变量的值被改变。
栈溢出的原因:
-
**局部数组过大。**当函数内部的数组过大时,有可能导致堆栈溢出。局部变量是存储在栈中的,因此这个很好理解。解决这类问题的办法有两个,一是增大栈空间,二是改用动态分配,使用堆(heap)而不是栈(stack)。
-
递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
-
指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。
栈溢出例子:
#include <stdio.h>
#include <string.h>
int main(int argc, char* argv[]) {
char buf[256];
strcpy(buf,argv[1]);
printf("Input:%s\n",buf);
return 0;
}
上述代码中的strcpy(buf,argv[1]);这一行发生了缓冲区溢出错误,因为源缓冲区内容是用户输入的。
栈和堆的区别,以及为什么栈要快
堆和栈的区别:
堆是由低地址向高地址扩展;栈是由高地址向低地址扩展
堆中的内存需要手动申请和手动释放;栈中内存是由OS自动申请和自动释放,存放着参数、局部变量等内存
堆中频繁调用malloc和free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片
堆的分配效率较低,而栈的分配效率较高
堆 | 栈 | |
---|---|---|
扩展方向 | 低地址到高地址 | 高地址到低地址 |
申请方式 | 手动申请释放速度慢 | 自动申请释放速度快 |
内存碎片 | malloc\new产生 | 程序局部变量不产生 |
效率 | 低 | 高 |
栈的效率高的原因:
栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。
两个栈实现一个队列
class Solution
{
public:
void push(int node) {
stack1.push(node);
}
int pop() {
if(stack2.size()!=0){
int tmp = stack2.top();
stack2.pop();
return tmp;
}
else{
while(stack1.size()!=0){
int tmp = stack1.top();
stack1.pop();
stack2.push(tmp);
}
return pop();
}
}
private:
stack<int> stack1;
stack<int> stack2;
};
Array&List, 数组和链表的区别
数组的特点:
数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。数组的插入数据和删除数据效率低,插入数据时,这个位置后面的数据在内存中都要向后移。删除数据时,这个数据后面的数据都要往前移动。但数组的随机读取效率很高。因为数组是连续的,知道每一个数据的内存地址,可以直接找到给地址的数据。如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。数组需要预留空间,在使用前要先申请占内存的大小,可能会浪费内存空间。并且数组不利于扩展,数组定义的空间不够时要重新定义数组。
链表的特点:
链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。如果要访问链表中一个元素,需要从第一个元素开始,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单了,只要修改元素中的指针就可以了。如果应用需要经常插入和删除元素你就需要用链表数据结构了。不指定大小,扩展方便。链表大小不用定义,数据随意增删。
各自的优缺点
数组的优点:
-
随机访问性强
-
查找速度快
数组的缺点:
-
插入和删除效率低
-
可能浪费内存
-
内存空间要求高,必须有足够的连续内存空间。
-
数组大小固定,不能动态拓展
链表的优点:
-
插入删除速度快
-
内存利用率高,不会浪费内存
-
大小没有固定,拓展很灵活。
链表的缺点:
不能随机查找,必须从第一个开始遍历,查找效率低
数组 | 链表 | |
---|---|---|
内存 | 连续 | 不连续 |
查找元素 | 速度快 | 慢,一个一个找 |
插入删除 | 麻烦,内存连续 | 容易 |
扩展 | 不支持,重新定义 | 简单 |
浪费 | 可能会浪费内存 | 不会浪费 |
一个长度为n的整形数组,数组中每个元素的取值范围是[0,n-1],判断该数组否有重复的数
考虑时间空间复杂度最小的方法
bool IsDuplicateNumber(int *array, int n)
{
if(array==NULL) return false;
int i,temp;
for(i=0;i<n;i++)
{
while(array[i]!=i)
{
if(array[array[i]]==array[i])
return true;
temp=array[array[i]];
array[array[i]]=array[i];
array[i]=temp;
}
}
return false;
}
快排算法;以及什么是稳定性排序,快排是稳定性的吗;快排算法最差情况推导公式
1、快排算法
根据哨兵元素,用两个指针指向待排序数组的首尾,首指针从前往后移动找到比哨兵元素大的,尾指针从后往前移动找到比哨兵元素小的,交换两个元素,直到两个指针相遇,这是一趟排序,经常这趟排序后,比哨兵元素大的在右边,小的在左边。经过多趟排序后,整个数组有序。
稳定性:不稳定
平均时间复杂度:O(nlogn)
2、稳定排序
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
快排算法是不稳定的排序算法。例如:
待排序数组:int a[] ={1, 2, 2, 3, 4, 5, 6};
若选择a[2](即数组中的第二个2)为枢轴,而把大于等于比较子的数均放置在大数数组中,则a[1](即数组中的第一个2)会到pivot的右边, 那么数组中的两个2非原序。
若选择a[1]为比较子,而把小于等于比较子的数均放置在小数数组中,则数组中的两个2顺序也非原序。
3、快排最差情况推倒
在快速排序的早期版本中呢,最左面或者是最右面的那个元素被选为枢轴,那最坏的情况就会在下面的情况下发生啦:
1)数组已经是正序排过序的。 (每次最右边的那个元素被选为枢轴)
2)数组已经是倒序排过序的。 (每次最左边的那个元素被选为枢轴)
3)所有的元素都相同(1、2的特殊情况)
因为这些案例在用例中十分常见,所以这个问题可以通过要么选择一个随机的枢轴,或者选择一个分区中间的下标作为枢轴,或者(特别是对于相比更长的分区)选择分区的第一个、中间、最后一个元素的中值作为枢轴。有了这些修改,那快排的最差的情况就不那么容易出现了,但是如果输入的数组最大(或者最小元素)被选为枢轴,那最坏的情况就又来了。
快速排序,在最坏情况退化为冒泡排序,需要比较O(n2)次(n(n - 1)/2次)。
手写一下快排的代码
void quicksort(int left, int right, vector<int> &arr)
{
if (left >= right) { return; }
int i = left;
int j = right;
while (i < j)
{
while (arr[j] >= arr[left] && i < j) { j--; }
while (arr[i] <= arr[left] && i<j) { i++; }
if (i < j) { swap(arr[i], arr[j]); }
}
swap(arr[i], arr[left]);
quicksort(left, i-1,arr );//留意
quicksort(i+1, right,arr);
}
介绍一下各种排序算法及时间复杂度
插入排序:对于一个带排序数组来说,其初始有序数组元素个数为1,然后从第二个元素,插入到有序数组中。对于每一次插入操作,从后往前遍历当前有序数组,如果当前元素大于要插入的元素,则后移一位;如果当前元素小于或等于要插入的元素,则将要插入的元素插入到当前元素的下一位中。
希尔排序:先将整个待排序记录分割成若干子序列,然后分别进行直接插入排序,待整个序列中的记录基本有序时,在对全体记录进行一次直接插入排序。其子序列的构成不是简单的逐段分割,而是将每隔某个增量的记录组成一个子序列。希尔排序时间复杂度与增量序列的选取有关,其最后一个值必须为1.
归并排序:该算法采用分治法;对于包含m个元素的待排序序列,将其看成m个长度为1的子序列。然后两两合归并,得到n/2个长度为2或者1的有序子序列;然后再两两归并,直到得到1个长度为m的有序序列。
冒泡排序:对于包含n个元素的带排序数组,重复遍历数组,首先比较第一个和第二个元素,若为逆序,则交换元素位置;然后比较第二个和第三个元素,重复上述过程。每次遍历会把当前前n-i个元素中的最大的元素移到n-i位置。遍历n次,完成排序。
快速排序:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
选择排序:每次循环,选择当前无序数组中最小的那个元素,然后将其与无序数组的第一个元素交换位置,从而使有序数组元素加1,无序数组元素减1.初始时无序数组为空。
**堆排序:**堆排序是一种选择排序,利用堆这种数据结构来完成选择。其算法思想是将带排序数据构造一个最大堆(升序)/最小堆(降序),然后将堆顶元素与待排序数组的最后一个元素交换位置,此时末尾元素就是最大/最小的值。然后将剩余n-1个元素重新构造成最大堆/最小堆。
各个排序的时间复杂度、空间复杂度及稳定性如下:
hash表的实现,包括STL中的哈希桶长度常数
hash表的实现主要包括构造哈希和处理哈希冲突两个方面:
对于构造哈希来说,主要包括直接地址法、平方取中法、除留余数法等。
对于处理哈希冲突来说,最常用的处理冲突的方法有开放定址法、再哈希法、链地址法、建立公共溢出区等方法。SGL版本使用链地址法,使用一个链表保持相同散列值的元素。
虽然链地址法并不要求哈希桶长度必须为质数,但SGI STL仍然以质数来设计哈希桶长度,并且将28个质数(逐渐呈现大约两倍的关系)计算好,以备随时访问,同时提供一个函数,用来查询在这28个质数之中,“最接近某数并大于某数”的质数。
哈希表(Hash table,也叫散列表)
, 是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
哈希表hash table(key,value) 的做法其实很简单,就是把Key通过一个固定的算法函数既所谓的哈希函数转换成一个整型数字,然后就将该数字对数组长度进行取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。
而当使用哈希表进行查询的时候,就是再次使用哈希函数将key转换为对应的数组下标,并定位到该空间获取value,如此一来,就可以充分利用到数组的定位性能进行数据定位。
哈希表最大的优点,就是把数据的存储和查找消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。然而在当前可利用内存越来越多的情况下,用空间换时间的做法是值得的。另外,编码比较容易也是它的特点之一。 哈希表又叫做散列表,分为“开散列” 和“闭散列”。
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一 个元素“分类”,然后将这个元素存储在相应“类”所对应的地方。
但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现**对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,**换句话说,就是把不同的元素分在了相同的“类”之中。后面我们将看到一种解决“冲突”的简便做法。 总的来说,“直接定址”与“解决冲突”是哈希表的两大特点。
哈希函数构造
就是映射函数构造,看某个元素具体属于哪一个类别。
除余法: 选择一个适当的正整数 p ,令 h(k ) = k mod p ,这里, p 如果选取的是比较大的素数,效果比较好。而且此法非常容易实现,因此是最常用的方法。最直观的一种,上图使用的就是这种散列法,公式:
index = value % 16
学过汇编的都知道,求模数其实是通过一个除法运算得到的,所以叫“除法散列法”。
平方散列法
求index是非常频繁的操作,而乘法的运算要比除法来得省时(对现在的CPU来说,估计我们感觉不出来),所以我们考虑把除法换成乘法和一个位移操作。公式:
index = (value * value) >> 28
( 右移,除以2^28。记法:左移变大,是乘。右移变小,是除)
数字选择法: 如果关键字的位数比较多,超过长整型范围而无法直接运算,可以选择其中数字分布比较均匀的若干位,所组成的新的值作为关键字或者直接作为函数值。
斐波那契(Fibonacci)散列法:平方散列法的缺点是显而易见的,所以我们能不能找出一个理想的乘数,而不是拿value本身当作乘数呢?答案是肯定的。
1,对于16位整数而言,这个乘数是40503
2,对于32位整数而言,这个乘数是2654435769
3,对于64位整数而言,这个乘数是11400714819323198485
这几个“理想乘数”是如何得出来的呢?这跟一个法则有关,叫黄金分割法则,而描述黄金分割法则的最经典表达式无疑就是著名的斐波那契数列,即如此形式的序列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377, 610, 987, 1597, 2584, 4181, 6765, 10946,…。另外,斐波那契数列的值和太阳系八大行星的轨道半径的比例出奇吻合。
对我们常见的32位整数而言,公式:
index = (value * 2654435769) >> 28
冲突处理
线性重新散列技术易于实现且可以较好的达到目的。令数组元素个数为 S ,则当 h(k) 已经存储了元素的时候,依次探查 (h(k)+i) mod S , i=1,2,3…… ,直到找到空的存储单元为止(或者从头到尾扫描一圈仍未发现空单元,这就是哈希表已经满了,发生了错误。当然这是可以通过扩大数组范围避免的)。
举例
哈希表支持的运算主要有:初始化(makenull)、哈希函数值的运算(h(x))、插入元素(insert)、查找元素(member)。 设插入的元素的关键字为 x ,A 为存储的数组。
伪代码
初始化:
复制代码
const empty=maxlongint; // 用非常大的整数代表这个位置没有存储元素
p=9997; // 表的大小
procedure makenull;
var i:integer;
begin
for i:=0 to p-1 do
A[i]:=empty;
End;
复制代码
哈希函数值的运算根据函数的不同而变化,例如除余法的一个例子:
function h(x:longint):Integer;
begin
h:= x mod p;
end;
我们注意到,插入和查找首先都需要对这个元素定位,即如果这个元素若存在,它应该存储在什么位置,因此加入一个定位的函数 locate:
复制代码
function locate(x:longint):integer;
var orig,i:integer;
begin
orig:=h(x);
i:=0;
while (i<S)and(A[(orig+i)mod S]<>x)and(A[(orig+i)mod S]<>empty) do
inc(i);
//当这个循环停下来时,要么找到一个空的存储单元,要么找到这个元
//素存储的单元,要么表已经满了
locate:=(orig+i) mod S;
end;
复制代码
插入元素 :
复制代码
procedure insert(x:longint);
var posi:integer;
begin
posi:=locate(x); //定位函数的返回值
if A[posi]=empty then A[posi]:=x
else error; //error 即为发生了错误,当然这是可以避免的
end;
复制代码
查找元素是否已经在表中:
复制代码
procedure member(x:longint):boolean;
var posi:integer;
begin
posi:=locate(x);
if A[posi]=x then member:=true
else member:=false;
end;
复制代码
当数据规模接近哈希表上界或者下界的时候,哈希表完全不能够体现高效的特点,甚至还不如一般算法。但是如果规模在中央,它高效的特点可以充分体现。试验表明当元素充满哈希表的 90% 的时候,效率就已经开始明显下降。这就给了我们提示:如果确定使用哈希表,应该尽量使数组开大,但对最太大的数组进行操作也比较费时间,需要找到一个平衡点。通常使它的容量至少是题目最大需求的 120% ,效果比较好(这个仅仅是经验,没有严格证明)。
什么时候适合应用哈希表呢?如果发现解决这个问题时经常要询问:“某个元素是否在已知集合中?”,也就是需要高效的数据存储和查找,则使用哈希表是最好不过的了!那么,在应用哈希表的过程中,值得注意的是什么呢?
哈希函数的设计很重要。一个不好的哈希函数,就是指造成很多冲突的情况,从前面的例子已经可以看出来,解决冲突会浪费掉大量时间,因此我们的目标 就是尽力避免冲突。前面提到,在使用“除余法”的时候,h(k)=k mod p ,p 最好是一个大素数。这就是为了尽力避免冲突。为什么呢?假设 p=1000 ,则哈希函数分类的标准实际上就变成了按照末三位数分类,这样最多1000类,冲突会很多。一般地说,如果 p 的约数越多,那么冲突的几率就越大。
简单的证明:假设 p 是一个有较多约数的数,同时在数据中存在 q 满足 gcd(p,q)=d >1 ,即有 p=ad , q=bd, 则有 q mod p= q – p* [q div p] =q – p*[b div a] . ① 其中 [b div a ] 的取值范围是不会超过 [0,b] 的正整数。也就是说, [b div a] 的值只有 b+1 种可能,而 p 是一个预先确定的数。因此 ① 式的值就只有 b+1 种可能了。这样,虽然mod 运算之后的余数仍然在 [0,p-1] 内,但是它的取值仅限于 ① 可能取到的那些值。也就是说余数的分布变得不均匀了。容易看出, p 的约数越多,发生这种余数分布不均匀的情况就越频繁,冲突的几率越高。而素数的约数是最少的,因此我们选用大素数。记住“素数是我们的得力助手”。
另一方面,一味的追求低冲突率也不好。理论上,是可以设计出一个几乎完美,几乎没有冲突的函数的。然而,这样做显然不值得,因为这样的函数设计 很浪费时间而且编码一定很复杂,与其花费这么大的精力去设计函数,还不如用一个虽然冲突多一些但是编码简单的函数。因此,函数还需要易于编码,即易于实 现。综上所述,设计一个好的哈希函数是很关键的。而“好”的标准,就是较低的冲突率和易于实现。另外,使用哈希表并不是记住了前面的基本操作就能以不变应万变的。有的时候,需要按照题目的要求对哈希表的结构作一些改进。往往一些简单的改进就可以带来巨大的方便。
这些只是一般原则,真正遇到试题的时候实际情况千变万化,需要具体问题具体分析才行。
当然,以上讲解的都是闭散列,如果使用链表,做开散列的话就可以更方便存储和删除了。其实这个和之前做18-600的malloc里面说的东西很类似。
拉链法
上面的方法使用数组实现的,其实很多时候需要使用数组链表来做。开一个数组,数组每个元素都是一个链表。(hash函数选择,针对字符串,整数,排列,具体相应的hash方法。 碰撞处理,一种是open hashing,也称为拉链法;另一种就是closed hashing,也称开地址法,opened addressing。)
使用除法散列:
使用斐波那契散列:
使用扩展法:
d-left hashing中的d是多个的意思,我们先简化这个问题,看一看2-left hashing。2-left hashing指的是将一个哈希表分成长度相等的两半,分别叫做T1和T2,给T1和T2分别配备一个哈希函数,h1和h2。在存储一个新的key时,同时用两个哈希函数进行计算,得出两个地址h1[key]和h2[key]。这时需要检查T1中的h1[key]位置和T2中的h2[key]位置,哪一个位置已经存储的(有碰撞的)key比较多,然后将新key存储在负载少的位置。如果两边一样多,比如两个位置都为空或者都存储了一个key,就把新key 存储在左边的T1子表中,2-left也由此而来。在查找一个key时,必须进行两次hash,同时查找两个位置。
hash索引跟B树索引的区别。
Hash 索引结构的特殊性,其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。
(1)Hash 索引仅仅能满足”=”,”IN”和”<=>”查询,不能使用范围查询。
由于 Hash 索引比较的是进行 Hash 运算之后的 Hash 值,所以它只能用于等值的过滤,不能用于基于范围的过滤,因为经过相应的 Hash 算法处理之后的 Hash 值的大小关系,并不能保证和Hash运算前完全一样。
(2)Hash 索引无法被用来避免数据的排序操作。
由于 Hash 索引中存放的是经过 Hash 计算之后的 Hash 值,而且Hash值的大小关系并不一定和 Hash 运算前的键值完全一样,所以数据库无法利用索引的数据来避免任何排序运算;
(3)Hash 索引不能利用部分索引键查询。
对于组合索引,Hash 索引在计算 Hash 值的时候是组合索引键合并后再一起计算 Hash 值,而不是单独计算 Hash 值,所以通过组合索引的前面一个或几个索引键进行查询的时候,Hash 索引也无法被利用。
(4)Hash 索引在任何时候都不能避免表扫描。
前面已经知道,Hash 索引是将索引键通过 Hash 运算之后,将 Hash运算结果的 Hash 值和所对应的行指针信息存放于一个 Hash 表中,由于不同索引键存在相同 Hash 值,所以即使取满足某个 Hash 键值的数据的记录条数,也无法从 Hash 索引中直接完成查询,还是要通过访问表中的实际数据进行相应的比较,并得到相应的结果。
(5)Hash 索引遇到大量Hash值相等的情况后性能并不一定就会比B-Tree索引高。
对于选择性比较低的索引键,如果创建 Hash 索引,那么将会存在大量记录指针信息存于同一个 Hash 值相关联。这样要定位某一条记录时就会非常麻烦,会浪费多次表数据的访问,而造成整体性能低下。
实现
问题描述:设计哈希表实现电话号码查询系统,实现下列功能:
(1) 假定每个记录有下列数据项:电话号码、用户名、地址。
(2) 一是从数据文件old.txt(自己现行建好)中读入各项记录,二是由系统随机产生各记录,并且把记录保存到new.txt文件中以及显示到屏幕上,记录条数不要少于30,然后分别以电话号码和用户名为关键字建立哈希表。
(3) 分别采用伪随机探测再散列法和再哈希法解决冲突。
(4) 查找并显示给定电话号码的记录;查找并显示给定用户名的记录。
(5) 将没有查找的结果保存到结果文件Out.txt中,显示查找结果前,要有提示语句。
复制代码
// MyHashTable.cpp : 定义控制台应用程序的入口点。
////设计哈希表实现电话号码查询系统
//说明:一是从文件old.txt中读取的数据自己在程序运行前建立,
// 二是由系统随机生成数据,在程序运行由随机数产生器生成,并且将产生的记录保存到 new.txt文件。
//存在的问题:使用随机产生的文件,在显示时出现乱码
#include "stdafx.h"
#include<fstream>//文件流
#include<iostream>
#include <string>
using namespace std;
const int D[] = {3,5,8,11,13,14,19,21};//预定再随机数
const int HASH_MAXSIZE = 50;//哈希表长度
//记录信息类型
class DataInfo
{
public:
DataInfo();//默认构造函数
friend ostream& operator<<(ostream& out, const DataInfo& dataInfo); //重载输出操作符
//friend class HashTable;
//private:
string name;//姓名
string phone;//电话号码
string address;//地址
char sign;//冲突的标志位,'1'表示冲突,'0'表示无冲突
};
DataInfo::DataInfo():name(""), phone(""), address(""), sign('0')
{
}
ostream& operator<<(ostream& out, const DataInfo& dataInfo) //重载输出操作符
{
cout << "姓名:" << dataInfo.name << " 电话:" << dataInfo.phone
<< " 地址:" << dataInfo.address << endl;
return out;
}
//存放记录的哈希表类型
class HashTable
{
public:
HashTable();//默认构造函数
~HashTable();//析构函数
int Random(int key, int i);// 伪随机数探测再散列法处理冲突
void Hashname(DataInfo *dataInfo);//以名字为关键字建立哈希表
int Rehash(int key, string str);// 再哈希法处理冲突 注意处理冲突还有链地址法等
void Hashphone(DataInfo *dataInfo);//以电话为关键字建立哈希表
void Hash(char *fname, int n);// 建立哈希表
//fname 是数据储存的文件的名称,用于输入数据,n是用户选择的查找方式
int Findname(string name);// 根据姓名查找哈希表中的记录对应的关键码
int Findphone(string phone);// 根据电话查找哈希表中的记录对应的关键码
void Outhash(int key);// 输出哈希表中关键字码对应的一条记录
void Outfile(string name, int key);// 在没有找到时输出未找到的记录
void Rafile();// 随机生成文件,并将文件保存在 new.txt文档中
void WriteToOldTxt();//在运行前先写入数据
//private:
DataInfo *value[HASH_MAXSIZE];
int length;//哈希表长度
};
HashTable::HashTable():length(0)//默认构造函数
{
//memset(value, NULL, HASH_MAXSIZE*sizeof(DataInfo*));
for (int i=0; i<HASH_MAXSIZE; i++)
{
value[i] = new DataInfo();
}
}
HashTable::~HashTable()//析构函数
{
delete[] *value;
}
void HashTable::WriteToOldTxt()
{
ofstream openfile("old.txt");
if (openfile.fail())
{
cout << "文件打开错误!" << endl;
exit(1);
}
string oldname;
string oldphone;
string oldaddress;
for (int i=0; i<30; i++)
{
cout << "请输入第" << i+1 << "条记录:" << endl;
cin >> oldname ;
cin >> oldphone;
cin >> oldaddress;
openfile << oldname << " " << oldphone << " " << oldaddress << "," << endl;
}
openfile.close();
}
int HashTable::Random(int key, int i)// 伪随机数探测再散列法处理冲突
{//key是冲突时的哈希表关键码,i是冲突的次数,N是哈希表长度
//成功处理冲突返回新的关键码,未进行冲突处理则返回-1
int h;
if(value[key]->sign == '1')//有冲突
{
h = (key + D[i]) % HASH_MAXSIZE;
return h;
}
return -1;
}
void HashTable::Hashname(DataInfo *dataInfo)//以名字为关键字建立哈希表
{//利用除留取余法建立以名字为关键字建立的哈希函数,在发生冲突时调用Random函数处理冲突
int i = 0;
int key = 0;
for (int t=0; dataInfo->name[t]!='\0'; t++)
{
key = key + dataInfo->name[t];
}
key = key % 42;
while(value[key]->sign == '1')//有冲突
{
key = Random(key, i++);//处理冲突
}
if(key == -1) exit(1);//无冲突
length++;//当前数据个数加
value[key]->name = dataInfo->name;
value[key]->address = dataInfo->address;
value[key]->phone = dataInfo->phone;
value[key]->sign = '1';//表示该位置有值
//cout << value[key]->name << " " << value[key]->phone << " " << value[key]->address << endl;
}
int HashTable::Rehash(int key, string str)// 再哈希法处理冲突
{//再哈希时使用的是折叠法建立哈希函数
int h;
int num1 = (str[0] - '0') * 1000 + (str[1] - '0') * 100 + (str[2] - '0') * 10 + (str[3] - '0');
int num2 = (str[4] - '0') * 1000 + (str[5] - '0') * 100 + (str[6] - '0') * 10 + (str[7] - '0');
int num3 = (str[8] - '0') * 100 + (str[9] - '0') * 10 + (str[10] - '0');
h = num1 + num2 + num3;
h = (h + key) % HASH_MAXSIZE;
return h;
}
void HashTable::Hashphone(DataInfo *dataInfo)//以电话为关键字建立哈希表
{//利用除留取余法建立以电话为关键字建立的哈希函数,在发生冲突时调用Rehash函数处理冲突
int key = 0;
int t;
for(t=0; dataInfo->phone[t] != '\0'; t++)
{
key = key + dataInfo->phone[t];
}
key = key % 42;
while(value[key]->sign == '1')//有冲突
{
key = Rehash(key, dataInfo->phone);
}
length++;//当前数据个数加
value[key]->name = dataInfo->name;
value[key]->address = dataInfo->address;
value[key]->phone = dataInfo->phone;
value[key]->sign = '1';//表示该位置有值
}
void HashTable::Outfile(string name, int key)//在没有找到时输出未找到的记录
{
ofstream fout;
if((key == -1)||(value[key]->sign == '0'))//判断哈希表中没有记录
{
fout.open("out.txt",ios::app);//打开文件
if(fout.fail())
{
cout << "文件打开失败!" << endl;
exit(1);
}
fout << name << endl;//将名字写入文件,有个问题,每次写入的时候总是将原来的内容替换了
fout.close();
}
}
void HashTable::Outhash(int key)//输出哈希表中关键字码对应的记录
{
if((key==-1)||(value[key]->sign=='0'))
cout << "没有找到这条记录!" << endl;
else
{
for(unsigned int i=0; value[key]->name[i]!='\0'; i++)
{
cout << value[key]->name[i];
}
for(unsigned int i=0; i<10; i++)
{
cout << " ";
}
cout << value[key]->phone;
for(int i=0; i<10; i++)
{
cout << " ";
}
cout << value[key]->address << endl;
}
}
void HashTable::Rafile()//随机生成文件,并将文件保存在new.txt文档中
{
ofstream fout;
fout.open("new.txt");//打开文件,等待写入
if(fout.fail())
{
cout << "文件打开失败!" << endl;
exit(1);
}
for(int j=0; j<30; j++)
{
string name = "";
for(int i=0; i<20; i++)//随机生成长个字的名字
{
name += rand() % 26 + 'a';//名字是由个字母组成
}
fout << name << " ";//将名字写入文件
string phone = "";
for(int i=0; i<11; i++)//随机生成长位的电话号码
{
phone += rand() % 10 + '0';//电话号码是纯数字
}
fout << phone << " ";//将电话号码写入文件
string address = "";
for(int i=0; i<29; i++)//随机生成长个字的名字
{
address += rand() % 26 + 'a';//地址是由个字母组成
}
address += ',';
fout << address << endl;//将地址写入文件
}
fout.close();
}
void HashTable::Hash(char *fname, int n)//建立哈希表
//fname是数据储存的文件的名称,用于输入数据,n是用户选择的查找方式
//函数输入数据,并根据选择调用Hashname或Hashphone函数进行哈希表的建立
{
ifstream fin;
int i;
fin.open(fname);//读文件流对象
if(fin.fail())
{
cout << "文件打开失败!" << endl;
exit(1);
}
while(!fin.eof())//按行读入数据
{
DataInfo *dataInfo = new DataInfo();
char* str = new char[100];
fin.getline(str, 100, '\n');//读取一行数据
if(str[0] == '*')//判断数据结束
{
break;
}
i = 0;//记录字符串数组的下标
//a-z:97-122 A-Z:65-90
//本程序的姓名和地址都使用小写字母
while((str[i] < 97) || (str[i] > 122))//读入名字
{
i++;
}
for(; str[i]!=' '; i++)
{
dataInfo->name += str[i];
}
while(str[i] == ' ')
{
i++;
}
for(int j=0; str[i]!=' '; j++,i++)//读入电话号码
{
dataInfo->phone += str[i];
}
while(str[i] == ' ')
{
i++;
}
for(int j=0; str[i]!=','; j++,i++)//读入地址
{
dataInfo->address += str[i];
}
if(n == 1)
{
Hashname(dataInfo);
}
else
{
Hashphone(dataInfo);//以电话为关键字
}
delete []str;
delete dataInfo;
}
fin.close();
}
int HashTable::Findname(string name)//根据姓名查找哈希表中的记录对应的关键码
{
int i = 0;
int j = 1;
int t;
int key = 0;
for(key=0, t=0; name[t] != '\0'; t++)
{
key = key + name[t];
}
key = key % 42;
while((value[key]->sign == '1') && (value[key]->name != name))
{
key = Random(key, i++);
j++;
if(j >= length) return -1;
}
return key;
}
int HashTable::Findphone(string phone)//根据电话查找哈希表中的记录对应的关键码
{
int key = 0;
int t;
for(t=0; phone[t] != '\0' ; t++)
{
key = key + phone[t];
}
key = key % 42;
int j = 1;
while((value[key]->sign == '1') && (value[key]->phone != phone))
{
key = Rehash(key, phone);
j++;
if(j >= length)
{
return -1;
}
}
return key;
}
void main()
{
//WriteToOldTxt();
int k;
int ch;
char *Fname;
HashTable *ht = new HashTable;
while(1)
{
system("cls");//cls命令清除屏幕上所有的文字
cout << "欢迎使用本系统!" << endl << endl;
cout << "请选择数据" << endl;
cout << "1.使用已有数据文件" << endl;
cout << "2.随机生成数据文件" << endl;
cout << "0.结束" << endl;
cout << "输入相应序号选择功能:";
cin >> k;
switch(k)
{
case 0:
return;
case 1:
Fname = "old.txt";//从数据文件old.txt(自己现行建好)中读入各项记录
break;
case 2:
ht->Rafile();
Fname = "new.txt";//由系统随机产生各记录,并且把记录保存到new.txt文件中
break;
default:
cout << "输入序号有误,退出程序。" << endl;
return;
}
do
{
system("cls");
cout << " 请选择查找方式" << endl;
cout << "1.通过姓名查找" << endl;
cout << "2.通过电话查找" << endl;
cout << "输入相应序号选择功能:";
cin >> ch;
if((ch != 1) && (ch != 2))
cout << "输入序号有误!" << endl;
}while((ch != 1) && (ch != 2));
ht->Hash(Fname, ch);
while(ch == 1)
{
int choice;
cout << endl << "请选择功能" << endl;
cout << "1.输入姓名查找数据" << endl;
cout << "2.显示哈希表" << endl;
cout << "0.退出"<<endl;
cout << "输入相应序号选择功能:";
cin >> choice;
switch(choice)
{
case 1:
{//注意此处应该加上大括号
int key1;
string name;
cout << "请输入姓名:";
cin >> name;
key1 = ht->Findname(name);
ht->Outfile(name, key1);
ht->Outhash(key1);
}
break;
case 2:
{
for(int i=0; i<HASH_MAXSIZE; i++)
{
if(ht->value[i]->sign!='0')
{
ht->Outhash(i);
}
}
}
break;
default:
cout << endl << "您的输入有误!" << endl;
}
if(choice == 0)
{
return;
}
}
while(ch == 2)
{
int choice;
cout << endl << "请选择功能" << endl;
cout << "1.输入电话查找数据" << endl;
cout << "2.显示哈希表"<<endl;
cout << "0.退出"<<endl;
cout << "输入相应序号选择功能:";
cin >> choice;
switch(choice)
{
case 1:
{
int key2;
string phone;
cout << "请输入11位的电话号码:";
do
{
cin >> phone;
if(phone.length() != 11)
{
cout << "电话号码应为11位!\n请重新输入:";
}
}while(phone.length() != 11);
key2 = ht->Findphone(phone);
ht->Outfile(phone, key2);
ht->Outhash(key2);
}
break;
case 2:
{
for(int i=0; i<HASH_MAXSIZE; i++)
{
if(ht->value[i]->sign != '0')
{
ht->Outhash(i);
}
}
}
break;
default:
cout << endl << "您的输入有误!" << endl;
}
if(choice == 0)
{
return;
}
}
while((ch != 1) && (ch != 2))
{
cout << "您的输入有误!请输入相应需要选择功能:";
}
}
system("pause");
}
hash表如何rehash,以及怎么处理其中保存的资源
C++的hash表中有一个负载因子loadFactor,当loadFactor<=1时,hash表查找的期望复杂度为O(1). 因此,每次往hash表中添加元素时,我们必须保证是在loadFactor <1的情况下,才能够添加。
因此,当Hash表中loadFactor==1时,Hash就需要进行rehash。rehash过程中,会模仿C++的vector扩容方式,Hash表中每次发现loadFactor ==1时,就开辟一个原来桶数组的两倍空间,称为新桶数组,然后把原来的桶数组中元素全部重新哈希到新的桶数组中。
哈希表的桶个数为什么是质数,合数有何不妥?
哈希表的桶个数使用质数,可以最大程度减少冲突概率,使哈希后的数据分布的更加均匀。如果使用合数,可能会造成很多数据分布会集中在某些点上,从而影响哈希表效率。
解决hash冲突的方法
当哈希表关键字集合很大时,关键字值不同的元素可能会映象到哈希表的同一地址上,这样的现象称为哈希冲突。目前常用的解决哈希冲突的方法如下:
开放定址法: 当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
**再哈希法:**当发生哈希冲突时使用另一个哈希函数计算地址值,直到冲突不再发生。这种方法不易产生聚集,但是增加计算时间,同时需要准备许多哈希函数。
链地址法:将所有哈希值相同的Key通过链表存储。key按顺序插入到链表中
建立公共溢出区:采用一个溢出表存储产生冲突的关键字。如果公共溢出区还产生冲突,再采用处理冲突方法处理。
最长公共连续子序列
class Solution {
public:
int lengthOflongestCommonSubstring(string& str1, string& str2){
int m = str1.size(), n = str2.size();
int res = 0;
vector<vector<int> > dp(m+1, vector<int>(n+1, 0));
for(int i = 1; i <= m; ++i){
for(int j = 1; j <= n; ++j){
if(str1[i-1] == str2[j-1])
dp[i][j] = dp[i-1][j-1] + 1;
else
dp[i][j] = 0;
if(res < dp[i][j])
res = dp[i][j];
}
}
return res;
}
};
然后再用滑动窗口进行遍历用find查找就可以了;
有几点注意的地方:
1.如果简单的建一个【len1,len2】的矩阵存放0和1 ,最后还要求对角线1连续最长,很麻烦,可以在建立的时候就考虑,每个元素就是其左上方的值,但是一旦不相等就要立马变0
2.注意用容器建立矩阵的方法
3.为了第一行有左上方元素就要多设计一圈,但是索引不能从0开始而是1开始,此时字符串的索引也必须要改变才行;
注意子串和子序列的不同:
最长公共子序列
给定两个字符串S1和S2,求两个字符串的最长公共子序列的长度。
输入样例
ABCD
AEBD
输出样例
3
解释
S1和S2的最长公共子序列为ABD,长度为3
思路
动态规划
LCS(m,n)LCS(m ,n)LCS(m,n)表示S1[0…m]S1[0…m]S1[0…m]和S2[0…n]S2[0…n]S2[0…n]的最长公共子序列的长度
S1[m]==S2[n]:LCS(m,n)=1+LCS(m−1,n−1)S1[m] == S2[n]: LCS(m, n) = 1 + LCS(m - 1, n - 1)S1[m]==S2[n]:LCS(m,n)=1+LCS(m−1,n−1)
S1[m]!=S2[n]:LCS(m,n)=max(LCS(m−1,n),LCS(m,n−1))S1[m] != S2[n]: LCS(m, n) = max(LCS(m - 1, n), LCS(m, n - 1))S1[m]!=S2[n]:LCS(m,n)=max(LCS(m−1,n),LCS(m,n−1))
代码
#include <iostream>
#include <vector>
#include <set>
using namespace std;
class Solution{
public:
int lengthOflongestCommonSequence(string& str1, string& str2){
int m = str1.length(), n = str2.length();
if(m == 0 || n == 0)
return 0;
dp = vector<vector<int> >(m+1, vector<int>(n+1, 0));
for(int i = 1; i < m+1; ++i){
for(int j = 1; j < n+1; ++j){
if(str1[i-1] == str2[j-1])
dp[i][j] = dp[i-1][j-1] + 1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
return dp[m][n];
}
// 找出所有的LCS的序列
// 这里形参lcs_str不可以为引用,这里需要每次调用lcs_str都重新生成一个对象
void PrintAllLCS(string& str1, string& str2, int i, int j, string lcs_str){
while(i > 0 && j > 0){
if(str1[i-1] == str2[j-1]){
lcs_str = str1[i-1] + lcs_str;
--i;
--j;
}
else{
if(dp[i-1][j] > dp[i][j-1]) //向左走
--i;
else if(dp[i-1][j] < dp[i][j-1]) //向上走
--j;
//此时向上向右均为LCS的元素
else{
PrintAllLCS(str1, str2, i-1, j, lcs_str);
PrintAllLCS(str1, str2, i, j-1, lcs_str);
return;
}
}
}
all_lcs.insert(lcs_str);
}
vector<vector<int>> dp;
set<string> all_lcs;
};
int main()
{
string s1 = "abcfbc";
string s2 = "abfcab";
Solution fir;
string lcs_str;
int res = fir.lengthOflongestCommonSequence(s1, s2);
cout << res << endl;
fir.PrintAllLCS(s1, s2, s1.length(), s2.length(), lcs_str);
set<string>::iterator iter = fir.all_lcs.begin();
while (iter != fir.all_lcs.end()) {
cout << *iter++ << endl;
}
return 0;
}
/*
4
abcb
abfb
abfc
*/
原文链接:https://blog.youkuaiyun.com/SCS199411/article/details/99222676
int substr(string & str1, string &str2)
{
int len1 = str1.length();
int len2 = str2.length();
vector<vector<int>>dp(len1, vector<int>(len2, 0));
for (int i = 0; i < len1; i++)
{
dp[i][0] = str1[i] == str1[0] ? 1 : 0;
}
for (int j = 0; j <= len2; j++)
{
dp[0][j] = str1[0] == str2[j] ? 1 : 0;
}
for (int i = 1; i < len1; i++)
{
for (int j = 1; j < len2; j++)
{
if (str1[i] == str2[j])
{
dp[i][j] = dp[i - 1][j - 1] + 1;
}
}
}
int longest = 0;
int longest_index = 0;
for (int i = 0; i < len1; i++)
{
for (int j = 0; j < len2; j++)
{
if (longest < dp[i][j])
{
longest = dp[i][j];
longest_index = i;
}
}
}
//字符串为从第i个开始往前数longest个
for (int i = longest_index - longest + 1; i <= longest_index; i++)
{
cout << str1[i] << endl;
}
return longest;
}
求一个字符串最长回文子串
法一 :先将字符串翻转,再求最长公共子串
法二:
int LongestPalindromicSubstring(string & a)
{
int len = a.length();
vector<vector<int>>dp(len, vector<int>(len, 0));
for (int i = 0; i < len; i++)
{
dp[i][i] = 1;
}
int max_len = 1;
int start_index = 0;
for (int i= len - 2; i >= 0; i--)
{
for (int j = i + 1; j < len; j++)
{
if (a[i] == a[j])
{
if (j - i == 1)
{
dp[i][j] = 2;
}
else
{
if (j - i > 1)
{
dp[i][j] = dp[i + 1][j - 1] + 2;
}
}
if (max_len < dp[i][j])
{
max_len = dp[i][j];
start_index = i;
}
}
else
{
dp[i][j] = 0;
}
}
}
cout << "max len is " << max_len << endl;
cout << "star index is" << start_index << endl;
return max_len;
}
合并两个有序链表
Node * MergeRecursive(Node *head1, Node *head2)
{
if (head1 == NULL)
return head2;
if (head2 == NULL)
return head1;
Node *head = NULL;
if (head1->data < head2->data){
head = head1;
head->next = MergeRecursive(head1->next, head2);
}
else {
head = head2;
head->next = MergeRecursive(head1, head2->next);
}
return head;
}
反转链表
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) :
* val(x), next(NULL) {
* }
* };
*/
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
ListNode* curnode = head;
ListNode* nextnode = head->next;
while (nextnode != NULL) //这里必须是这样!!
{
ListNode* temp = nextnode->next;
nextnode->next = curnode;
curnode = nodenext;
nextnode = temp;
}
//curnode->next = NULL;
head->next = NULL;//链表尾
head = curnode; //重新定义链表头
//head->next = NULL;
}
判断一个链表是否为回文链表
思路:使用栈存储链表前半部分,然后一个个出栈,与后半部分元素比较,如果链表长度未知,可以使用快慢指针的方法,将慢指针指向的元素入栈,然后如果快指针指向了链表尾部,此时慢指针指向了链表中间
bool is_palindromic_list2(mylist *a_list)
{
if (a_list == nullptr)
{
return false;
}
stack<int>list_value;
mylist * fast = a_list;
mylist *slow = a_list;
while (fast->next != nullptr && fast->next->next != nullptr)
{
list_value.push(slow->next->value);
slow = slow->next;
fast = fast->next->next;
}
cout << "middle elem value is " << slow->next->value << endl;
if (fast->next != nullptr)
{
cout << "the list has odd num of node" << endl;
slow = slow->next;
}
int cur_value;
while (!list_value.empty())
{
cur_value = list_value.top();
cout << "stack top value is" << cur_value << endl;
cout << "list value is " << slow->next->value << endl;
if (cur_value != slow->next->value)
{
return false;
}
list_value.pop();
slow = slow->next;
}
return true;
}
小根堆特点
堆是一棵完全二叉树(如果一共有h层,那么1~h-1层均满,在h层可能会连续缺失若干个右叶子)。
1)小根堆
若根节点存在左子女则根节点的值小于左子女的值;若根节点存在右子女则根节点的值小于右子女的值。
2)大根堆
若根节点存在左子女则根节点的值大于左子女的值;若根节点存在右子女则根节点的值大于右子女的值。
哈希冲突的解决方法
1、开放定址
开放地址法有个非常关键的特征,就是所有输入的元素全部存放在哈希表里,也就是说,位桶的实现是不需要任何的链表来实现的,换句话说,也就是这个哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法。
有几种常用的探查序列的方法:
①线性探查
dii=1,2,3,…,m-1;这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
②二次探查
di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 );这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
③ 伪随机探测
di=伪随机数序列;具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),生成一个位随机序列,并给定一个随机数做起点,每次去加上这个伪随机数++就可以了。
2、链地址
每个位桶实现的时候,采用链表或者树的数据结构来去存取发生哈希冲突的输入域的关键字,也就是被哈希函数映射到同一个位桶上的关键字。
紫色部分即代表哈希表,也称为哈希数组,数组的每个元素都是一个单链表的头节点,链表是用来解决冲突的,如果不同的key映射到了数组的同一位置处,就将其放入单链表中,即链接在桶后。
3、公共溢出区
建立一个公共溢出区域,把hash冲突的元素都放在该溢出区里。查找时,如果发现hash表中对应桶里存在其他元素,还需要在公共溢出区里再次进行查找。
4、再hash
再散列法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次散列,直至不发生冲突位置。
缺点:每次冲突都要重新散列,计算时间增加。
加密方法都有哪些
1、单向加密
单向加密又称为不可逆加密算法,其密钥是由加密散列函数生成的。单向散列函数一般用于产生消息摘要,密钥加密等,常见的有:
MD5(Message Digest Algorithm 5):是RSA数据安全公司开发的一种单向散列算法,非可逆,相同的明文产生相同的密文;
SHA(Secure Hash Algorithm):可以对任意长度的数据运算生成一个160位的数值。其变种由SHA192,SHA256,SHA384等;
CRC-32,主要用于提供校验功能;
算法特征:
输入一样,输出必然相同;
雪崩效应,输入的微小改变,将会引起结果的巨大变化;
定长输出,无论原始数据多大,结果大小都是相同的;
不可逆,无法根据特征码还原原来的数据;
2、对称加密
采用单钥密码系统的加密方法,同一个密钥可以同时用作信息的加密和解密,这种加密方法称为对称加密,也称为单密钥加密。
特点:
1、加密方和解密方使用同一个密钥;
2、加密解密的速度比较快,适合数据比较长时的使用;
3、密钥传输的过程不安全,且容易被破解,密钥管理也比较麻烦;
优点:对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。
缺点:对称加密算法的缺点是在数据传送前,发送方和接收方必须商定好秘钥,然后使双方都能保存好秘钥。其次如果一方的秘钥被泄露,那么加密信息也就不安全了。另外,每对用户每次使用对称加密算法时,都需要使用其他人不知道的唯一秘钥,这会使得收、发双方所拥有的钥匙数量巨大,密钥管理成为双方的负担。
3、非对称加密
非对称密钥加密也称为公钥加密,由一对公钥和私钥组成。公钥是从私钥提取出来的。可以用公钥加密,再用私钥解密,这种情形一般用于公钥加密,当然也可以用私钥加密,用公钥解密。常用于数字签名,因此非对称加密的主要功能就是加密和数字签名。
特征:
1)秘钥对,公钥(public key)和私钥(secret key)
2)主要功能:加密和签名
发送方用对方的公钥加密,可以保证数据的机密性(公钥加密)。
发送方用自己的私钥加密,可以实现身份验证(数字签名)。
3)公钥加密算法很少用来加密数据,速度太慢,通常用来实现身份验证。
常用的非对称加密算法
RSA:由 RSA公司发明,是一个支持变长密钥的公共密钥算法,需要加密的文件块的长度也是可变的;既可以实现加密,又可以实现签名。
DSA(Digital Signature Algorithm):数字签名算法,是一种标准的 DSS(数字签名标准)。
ECC(Elliptic Curves Cryptography):椭圆曲线密码编码。
LRU缓存
LRU(最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高
实现:使用一个链表保存缓存数据,将新数据插入到头部,每当缓存命中时,则将命中的数据移动到链表头部,当链表满的时候,将链表尾部的数据丢弃
使用双向链表和哈希表实现LRU算法
双向链表用于保存key-value,队头的元素表示最近一次被访问的元素,队尾的元素表示最近最久未使用的元素
哈希表提供key到key对应节点在双向链表中的位置的映射
实现了get操作和put操作
get:传入key,得到value,并将对应节点移动到双向链表头部(因为这个节点是最近被访问的节点)
put:传入key-value对,分三种情况处理
若key已存在,则先删除对应节点,再把key-value插入双向链表头部(因为这个节点是最近被访问的节点)
若双向链表长度大于或等于最大元素个数,则删除双向链表队尾元素(最近最久未使用的元素),再把key-value插入双向链表头部
若上面两个条件不满足,只需要把key-value插入双向链表头部
与有的博客中实现方式不同的是,我用一个变量size_保存双向链表的长度,因为std::list的size方法是O(n)时间复杂度的(具体可看我另一篇转载的博客)。做了这个改进后,get和put的时间复杂度都为O(1)
思路: map 存储数据 ,实现查找效率O(1),双向链表实现算法逻辑
算法逻辑: 1.新数据会插入到链表头部
2.当缓存数据被访问,将该缓存数据移到链表头部
3.当新数据插入时达到缓存上限了,将尾部数据删除掉(也就是最近最少使用的),新数据放在头部。
原文链接:https://blog.youkuaiyun.com/qq_38506897/article/details/81218683
//********************
//**Filename:LRUCache.h
//**Discribe: LRUCache实现文件:双向链表+map
//**Date: 2018.7.26
//**@author: Mr.xiong
//*****
#include<iostream>
#include<map>
using namespace std;
//** 链表节点结构体
struct ListNode //双向链表
{
int m_key; //key,value 形式方便map存储。
int m_value;
ListNode* pPre;
ListNode* pNext;
ListNode(int key ,int value)
{
m_key = key;
m_value=value;
pPre=NULL;
pNext=NULL;
}
};
//* LRU缓存实现类 双向链表。
class LRUCache
{
public:
//** 构造函数初始化缓存大小
LRUCache(int size)
{
m_capacity = size;
pHead == NULL;
pTail == NULL;
}
~LRUCache()
{
//** 一定要注意,map释放内存时,先释放内部new的内存,在释放map的内存
map<int , ListNode*>::iterator it =mp.begin();
for(;it!=mp.end();)
{
delete it->second;
it->second = NULL;
mp.erase(it++); //** 注意:一定要这样写,it++ 放在其他任何一个地方都会导致其迭代器失效。
}
delete pHead;
pHead == NULL;
delete pTail;
pTail ==NULL;
}
//** 这里只是移除,并不删除节点
void Remove(ListNode* pNode)
{
// 如果是头节点
if(pNode->pPre == NULL)
{
pHead = pNode->pNext;
pHead->pPre = NULL;
}
// 如果是尾节点
if(pNode->pNext == NULL)
{
pTail = pNode->pPre;
pTail->pNext = NULL;
}
else
{
pNode->pPre->pNext=pNode->pNext;
pNode->pNext->pPre = pNode->pPre;
}
}
// 将节点放到头部,最近用过的数据要放在队头。
void SetHead(ListNode* pNode)
{
pNode->pNext = pHead;
pNode->pPre = NULL;
if(pHead == NULL)
{
pHead = pNode;
}
else
{
pHead->pPre=pNode;
pHead = pNode;
}
if(pTail == NULL)
{
pTail = pHead;
}
}
// * 插入数据,如果存在就只更新数据
int Set(int key, int value)
{
map<int,ListNode*>::iterator it=mp.find(key); //找到这个key对应的迭代器
if(it != mp.end()) //如果就是在最后
{
ListNode* Node=it->second; //?
Node->m_value=value;
Remove(Node);
SetHead(Node);
}
else
{
ListNode* NewNode = new ListNode(key,value);
if(mp.size() >= m_capacity) //
{
map<int,ListNode*>::iterator it=mp.find(pTail->m_key);
//从链表移除
Remove(pTail);
//删除指针指向的内存
delete it->second;
//删除map元素
mp.erase(it);
}
//放到头部
SetHead(NewNode);
mp[key] = NewNode;
}
}
//获取缓存里的数据
int Get(int key)
{
map<int , ListNode*>:: iterator it = mp.find(key);
if(it != mp.end())
{
ListNode* Node=it->second;
Remove(Node);
SetHead(Node);
return Node->m_value;
}
else
{
return -1; //这里不太好,有可能取得值也为-1
}
}
int GetSize()
{
return mp.size();
}
private:
int m_capacity; //缓存容量
ListNode* pHead; //头节点
ListNode* pTail; //尾节点
map<int, ListNode*> mp; //mp用来存数据,达到find为o(1)级别。
};
洗牌算法
1、Fisher-Yates Shuffle算法
最早提出这个洗牌方法的是 Ronald A. Fisher 和 Frank Yates,即 Fisher–Yates Shuffle,其基本思想就是从原始数组中随机取一个之前没取过的数字到新的数组中,具体如下:
1)初始化原始数组和新数组,原始数组长度为n(已知)。
2)从还没处理的数组(假如还剩k个)中,随机产生一个[0, k)之间的数字p(假设数组从0开始)。
3)从剩下的k个数中把第p个数取出。
4)重复步骤2和3直到数字全部取完。
5)从步骤3取出的数字序列便是一个打乱了的数列。
时间复杂度为O(n*n),空间复杂度为O(n)。
2)Knuth-Durstenfeld Shuffle
Knuth 和 Durstenfeld 在Fisher 等人的基础上对算法进行了改进,在原始数组上对数字进行交互,省去了额外O(n)的空间。该算法的基本思想和 Fisher 类似,每次从未处理的数据中随机取出一个数字,然后把该数字放在数组的尾部,即数组尾部存放的是已经处理过的数字。
算法步骤为:
-
建立一个数组大小为 n 的数组 arr,分别存放 1 到 n 的数值;
-
生成一个从 0 到 n - 1 的随机数 x;
-
输出 arr 下标为 x 的数值,即为第一个随机数;
-
将 arr 的尾元素和下标为 x 的元素互换;
-
同2,生成一个从 0 到 n - 2 的随机数 x;
-
输出 arr 下标为 x 的数值,为第二个随机数;
-
将 arr 的倒数第二个元素和下标为 x 的元素互换;
……
如上,直到输出m 个数为止
时间复杂度为O(n),空间复杂度为O(1),缺点必须知道数组长度n。