图
图定义:
定义 | 描述 |
图 | 顶点的有穷非空集合和顶点之间边的集合组成:G(V,E)。V顶点集,E边集。 |
无向图 | 图中每条边都没有方向。边是没有方向的,写为(a,b) |
有向图 | 每条边都有方向,边写为<a,b>有向边也称为弧;开始顶点称为弧尾,结束顶点称为弧头 |
简单图 | 不存在指向自己的边、不存在两条重复的边的图。 |
无向完全图 | 每个顶点之间都有一条边的无向图 |
有向完全图 | 每个顶点之间都有两条互为相反的弧 |
稀疏图 | 边相对于顶点来说很少的图 |
稠密图 | 边很多的图 |
权重 | 图中的边可能会带有一个权重,为了区分边的长短。 |
网 | 带有权重的图 |
度 | 与特定顶点相连接的边数。出度、入度:对于有向图的概念,出度表示此顶点为起点的边的数目,入度表示此顶点为终点的边的数目 |
环 | 第一个顶点和最后一个顶点相同的路径 |
简单环 | 除去第一个顶点和最后一个顶点后没有重复顶点的环 |
连通图 | 任意两个顶点都相互连通的图 |
极大连通子图 | 包含竟可能多的顶点(必须是连通的),即找不到另外一个顶点,使得此顶点能够连接到此极大连通子图的任意一个顶点。 |
连通分量 | 极大连通子图的数量 |
强连通图 | 此为有向图的概念表示任意两个顶点a,b,使得a能够连接到b,b也能连接到a 的图 |
生成树 | n个顶点,n-1条边,并且保证n个顶点相互连通(不存在环) |
最小生成树 | 此生成树的边的权重之和是所有生成树中最小的 |
AOV网 | 结点表示活动的网 |
AOE网 | 边表示活动的持续时间的网 |
图的存储结构
1)邻接矩阵:
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(邻接矩阵)存储图中的边或弧的信息。
由邻接矩阵可以容易的判断两定点有无边,及边的度及有向图的出度和入度。n个顶点和e条边的无向网图的创建,时间复杂度为O(n + e),其中对邻接矩阵Grc的初始化耗费了O()的时间。
2)邻接表:
图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过,数组可以较容易的读取顶点的信息,更加方便。图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以,用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。
顶点表的各个结点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点。边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域。对于n个顶点e条边来说,很容易得出是O(n+e)。
3)十字链表:
重新定义顶点表结点结构
data | firstin | firsout |
其中firstin表示入边表头指针,指向该顶点的入边表中第一个结点,firstout表示出边表头指针,指向该顶点的出边表中的第一个结点。
重新定义边表结构
tailvex | headvex | headlink | taillink |
其中,tailvex是指弧起点在顶点表的下表,headvex是指弧终点在顶点表的下标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针域,指向起点相同的下一条边。如果是网,还可以增加一个weight域来存储权值。
十字链表的好处就是因为把邻接表和逆邻接表整合在一起,这样既容易找到以v为尾的弧,也容易找到以v为头的弧,因而比较容易求得顶点的出度和入度。
4)邻接多重表: 重新定义边表节点
ivex | ilink | jvex | jlink |
则邻接表是一个很好的选择,但是如果我们要在邻接表中删除一条边,则需要删除四个顶点(因为无向图)。在邻接多重表中,只需要删除一个节点,即可完成边的删除,因此比较方便。多应用于对边的操作
5)边集数组:
两个一位数组组成,一个存储顶点信息,另个存储边信息。每个边组数据元素有一个边的起点下标begin、终点下标end和权值weight。
3、图的遍历:
图的遍历和树的遍历类似,希望从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫图的遍历。对于图的遍历来说,如何避免因回路陷入死循环,就需要科学地设计遍历方案,通过有两种遍历次序方案:深度优先遍历和广度优先遍历。
深度优先遍历:
也有称为深度优先搜索,简称DFS。其实,就像是一棵树的前序遍历。它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
public class DFSearch {
public void searchTraversing(GraphNode node, List<GraphNode> visited) {
// 判断是否遍历过
if (visited.contains(node)) {
return;
}
visited.add(node);
System.out.println("节点:" + node.getLabel());
for (int i = 0; i < node.edgeList.size(); i++) {
searchTraversing(node.edgeList.get(i).getNodeRight(), visited);
}
}
}
广度优先遍历:
public class BFSearch {
/**
* 广度优先搜索
*/
public void searchTraversing(GraphNode node) {
List<GraphNode> visited = new ArrayList<GraphNode>(); // 已经被访问过的元素
Queue<GraphNode> q = new LinkedList<GraphNode>(); // 用队列存放依次要遍历的元素
q.offer(node);
while (!q.isEmpty()) {
GraphNode currNode = q.poll();
if (!visited.contains(currNode)) {
visited.add(currNode);
System.out.println("节点:" + currNode.getLabel());
for (int i = 0; i < currNode.edgeList.size(); i++) {
q.offer(currNode.edgeList.get(i).getNodeRight());
}
}
}
}
}
4、最小生成树:
prim算法
基本思想:假设N=(V,{E})是联通网,TE是N上的最想生成树中的变得集合。算法从U={u0}(u0属于V),TE={}开始,重复执行下述操作:在所有的u属于U,v属于V-U的边(u,v)属于E中找到一条代价最小的边(u0,v0)并入集合TE,同事v0并入U,直至U=V为止。此时TE中必有n-1条边,则T=(V,{TE}) 为N的最小生成树。(以顶点找边连接起来)下面是以邻接矩阵存储形式的算法。
public void prim(){
int cost[] = new int[9];
int pre[] = new int[9];
for(int i=0;i<g1.vertex.length;i++){
cost[i] = g1.adjMatrix[0][i];
}
cost[0] = 0;
for(int i=1;i<g1.vertex.length;i++){
int min = 65536;
int k = 0;
for(int j=1;j<g1.vertex.length;j++){
if(cost[j]!=0&&cost[j]<min){
min = cost[j];
k = j;
}
}
cost[k] = 0;
System.out.println(pre[k]+","+k);
for(int j=1;j<g1.vertex.length;j++){
if(cost[j]!=0&&g1.adjMatrix[k][j]<cost[j]){
pre[j] = k;
cost[j] = g1.adjMatrix[k][j];
}
}
}
}
Kruskal算法:
克鲁斯卡尔算法从另一个途径求网中的最小生成树。假设联通网N=(V,{E}),则令最小生成树的初始状态为只有n个顶点而无边的非连通图T=(V,{}),途中每个顶点自称一个连通分量。在E中选择代价最小的边,若该边衣服的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条最小的边。以此类推,直至T中所有的顶点都在同一连通分量上为止。(边连顶点的策略)
public class Kruskal {
private Set<String> points=new HashSet<String>();
private List<Edge> treeEdges=new ArrayList<Edge>();
public void buildTree(){
MapBuilder builder=new MapBuilder();
TreeSet<Edge> edges=builder.build();
int pointNum=builder.getPointNum();
for(Edge edge:edges){
if(isCircle(edge)){
continue;
}else{//没有出现回路,将这条边加入treeEdges集合
treeEdges.add(edge);
//如果边数等于定点数-1,则遍历结束
if(treeEdges.size()==pointNum-1){
return;
}
}
}
}
public void printTreeInfo(){
int totalDistance=0;
for(Edge edge:treeEdges){
totalDistance+=edge.getDistance();
System.out.println(edge.toString());
}
System.out.println("总路径长度:"+totalDistance);
}
private boolean isCircle(Edge edge){
int size=points.size();
if(!points.contains(edge.getStart())){
size++;
}
if(!points.contains(edge.getEnd())){
size++;
}
if(size==treeEdges.size()+1){
return true;
}else{
points.add(edge.getStart());
points.add(edge.getEnd());
return false;
}
}
}
5、最短路径:
用于计算一个节点到其他所有节点的最短路径。是典型的最短路径路由算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层扩展,直到扩展到终点为止。
Dijkstra:
Dijkstra算法能得出最短路径的最优解,但由于它遍历计算的节点很多,所以效率低。时间复杂度O(n^2)。
邻接矩阵存储的Dijkstra:
public void Dijkstra(){
int distance[] = new int[9];
int pre[] = new int[9];
boolean finished[] = new boolean[9];
finished[0] = true;
for(int i=0;i<9;i++){
distance[i] = g1.adjMatrix[0][i];
}
int k = 0;
for(int i=1;i<9;i++){
int min = 65536;
for(int j=0;j<9;j++){
if(!finished[j]&&distance[j]<min){
min = distance[j];
k = j;
}
}
finished[k] = true;
System.out.println(pre[k]+","+k);
for(int j=1;j<9;j++){
if(!finished[j]&&(min+g1.adjMatrix[k][j])<distance[j]){
distance[j] = min+g1.adjMatrix[k][j];
pre[j] = k;
}
}
}
}
Fload算法:
多源最短路径,是一种动态规划算法。时间复杂度:O(n^3),空间复杂度:O(n^2)。
public void floyd(Graph1 g) {
int i, j, k;
int length = g.vertex.length;
int dist[][] = new int[length][length];
int pre[][] = new int[length][length];
for (i = 0; i < g.vertex.length; i++) {
for (j = 0; j < g.vertex.length; j++) {
pre[i][j] = j;
dist[i][j] = g.adjMatrix[i][j];
}
}
for (i = 0; i < length; i++) {
for (j = 0; j < g.vertex.length; j++) {
for (k = 0; k < g.vertex.length; k++) {
if (dist[i][j] > dist[i][k] + dist[k][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
pre[i][j] = pre[i][k];
}
}
}
}
System.out.println();
}
6、拓扑排序:
有向图中,图中的顶点代表活动(子工程),图中的有向边代表活动的先后关系,即有向边的起点的活动是终点活动的前序活动,只有当起点活动完成之后,其终点活动才能进行。通常,我们把这种顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网,简称AOV网。对一个有向无环图G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出现在v之前。
7、关键路径:
用顶点表示事件,弧表示活动,弧上的权值表示活动持续的时间的有向图叫AOE网。他的性质有只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始。只有在进入某一顶点的各有向边所代表的活动都已经结束,该顶点所代表的事件才能发生。表示实际工程计划的AOE网应该是无环的,并且存在唯一的入度过为0的开始顶点和唯一的出度为0的完成顶点。关键路径:从源点到汇点的路径长度最长的路径叫关键路径。事件发生的最早时间etv即顶点vk的最早发生时间,对应是事件最晚发生时间ltv。活动最早开工时间ete即弧ak最早发生时间。算法:输入e条弧<j,k>,建立AOE-网的存储结构。
2、拓扑排序,并求得ve[]。从源点V0出发,令ve[0]=0,按拓扑有序求其余各顶点的最早发生时间ve[i]。如果得到的拓扑有序序列中顶点个数小于网中顶点数n,则说明网中存在环,不能求关键路径,算法终止。否则执行步骤3。3、拓扑逆序,求得vl[]。从汇点Vn出发,令vl[n-1] = ve[n-1],按逆拓扑有序求其余各顶点的最迟发生时间vl[i]。
4、求得关键路径。根据各顶点的ve和vl值,求每条弧s的最早开始时间e(s)和最迟开始时间l(s)。若某条弧满足条件e(s) = l(s),则为关键活动。为了能按逆序拓扑有序序列的顺序计算各个顶点的vl值,需记下在拓扑排序的过程中求得的拓扑有序序列,这就需要在拓扑排序算法中,增设一个栈,以记录拓扑有序序列,则在计算求得各顶点的ve值之后,从栈顶到栈底便为逆拓扑有序序列。
public class Grph_CriticalPath
{
Graph_AdjList adjList;
Stack<Integer> T = new Stack<Integer>();
int ve[];
int vl[];
final int max = 10000;
public Grph_CriticalPath(Graph_AdjList adjList) //图的存储结构是用的邻接表
{
this.adjList = adjList;
int length = adjList.vetexValue.length;
ve = new int[length];
vl = new int[length];
for(int i=0;i<length;i++)
{
ve[i] = 0;
vl[i] = max;
}
}
public void getCriticalPath()
{
topologicalOrder();
int t = T.pop();
T.push(t);
vl[t] = ve[t];
while(!T.isEmpty())
{
int j = T.pop();
for(Graph_AdjList.ArcNode p = adjList.vetex[j].firstArc; p!=null ;p = p.next)
{
int k = p.adjvex;
if(vl[k]-p.weight<vl[j])
{
vl[j] = vl[k]-p.weight;
}
}
}
for(int i=0;i<ve.length;i++)
{
for(Graph_AdjList.ArcNode p = adjList.vetex[i].firstArc; p!=null ;p = p.next)
{
int k = p.adjvex;
int ee = ve[i];
int el = vl[k]-p.weight;
if(ee==el)
{
System.out.print(i+","+k+" ");
}
}
}
}
public void topologicalOrder()
{
Stack<Integer> S = new Stack<Integer>();
S.push(0);
int count = 0;
while(!S.isEmpty())
{
int j = S.pop();
T.push(j);
count++;
Graph_AdjList.ArcNode p = null;
for(p = adjList.vetex[j].firstArc; p!=null ;p = p.next)
{
int k = p.adjvex;
if(--adjList.degree[k]==0)
{
S.push(k);
}
if(ve[j]+p.weight>ve[k])
{
ve[k] = ve[j]+p.weight;
}
}
}
if(count<adjList.vetexValue.length)
{
System.out.println("图中存在环路!");
return;
}
}
public void print()
{
while(!T.isEmpty())
{
System.out.print(T.pop()+" ");
}
}
public void printVel()
{
System.out.println();
for(int i=0;i<ve.length;i++)
{
System.out.print(ve[i]+" ");
}
System.out.println();
for(int i=0;i<vl.length;i++)
{
System.out.print(vl[i]+" ");
}
}
}
查找/搜索
1、基本概念
(1)查找:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素或记录。
(2)查找表:同一类型数据元素构成的集合。
(3)关键字:是数据元素中某个数据项的值,用它可以标识一个数据元素。关键码:关键字所标识的数据元素的某个数据项。主关键字唯一标识一个数据元素;主关键码,主关键字所在的数据项。次次关键字,不唯一标识,次关键码,次关键字所在的数据项。
(4)静态查找表:只作查找操作(A):查询某个“特定”数据元素是否存在,(B):检索某个“特定”数据元素及数据项信息。
(5)动态查找表:A)查找时插入数据元素,B)查找时删除数据元素。
2、顺序查找表:
顺序表:线性表属于静态查找表。从头到尾逐个比较查找。(效率低时间复杂度O(N))。
3、有序表的查找:
1) 折半查找(二分查找)要求线性表有序和采用顺序存储。mid=(low+high)/2 = low+(high-low)*1/2。
2) 插值查找:插值公式=(key-a[low])/(a[high]-a[low]),mid=low+(high-low)*(key-a[low])/(a[high]-a[low]),适用:关键字分布比较均匀的查找表。
3)斐波那契查找,黄金分割,mid=low+F[k-1]-1,斐波那契数列:F[K]=F[K-1]+F[K-2]。
1、二分查找
C++的std::binary_search:检查等价于 value
的元素是否出现于范围 [first, last)
中。
可能实现的版本:
//模板1
template<class ForwardIt, class T>
bool binary_search(ForwardIt first, ForwardIt last, const T& value)
{
first = std::lower_bound(first, last, value);
return (!(first == last) && !(value < *first));
}
//模板二
template<class ForwardIt, class T, class Compare>
bool binary_search(ForwardIt first, ForwardIt last, const T& value, Compare comp)
{
first = std::lower_bound(first, last, value, comp);
return (!(first == last) && !(comp(value, *first)));
}
示例:
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std::vector<int> haystack {1, 3, 4, 5, 9};
std::vector<int> needles {1, 2, 3};
int a[10] = { 1, 4, 8, 9, 13, 31, 36, 42, 59, 67 };
std::cout<<std::binary_search(a, 10, 13)<<"\n";
for (auto needle : needles) {
std::cout << "Searching for " << needle << '\n';
if (std::binary_search(haystack.begin(), haystack.end(), needle)) {
std::cout << "Found " << needle << '\n';
} else {
std::cout << "no dice!\n";
}
}
}
//1
//Searching for 1
//Found 1
//Searching for 2
//no dice!
//Searching for 3
//Found 3
内部实现:
#include<iostream>
int Binary_Search(int *a, int n, int key) //二分查找
{
int low, high, mid;
low = 0;
high = n - 1;
while (low<=high)
{
mid = (low + high) / 2;
if (a[mid] < key)
low = mid + 1;
else if (a[mid]>key)
high = mid - 1;
else
return mid;
}
return -1;
}
int recurisonBinarySearch(int *a, int left,int right,int x) //递归
{
if(left>right)
{
return -1;
}
int mid =(left+right)/2;
if(x==a[mid])
{
return mid;
}
if(x>a[mid])
{
return recurisonBinarySearch(a,mid+1,right,x);
}
else
{
return recurisonBinarySearch(a,left,mid-1,x);
}
}
int main()
{
int a[10] = { 1, 4, 8, 9, 13, 31, 36, 42, 59, 67 };
int index;
index=Binary_Search(a, 10, 13);
std::cout<<recurisonBinarySearch(a, 0,10, 13); //递归
if (index != -1)
std::cout<<"find key "<< index;
else
std::cout<<"not find"<<"\n";
return 0;
}
插值查找:
//和折半查找逻辑一致,修改了mid值
int interpolationSearch(int *array, int low, int high, int key)
{
while (low <= high)
{
//优化中间值
int mid = low+(key-array[low])*(high - low)/(array[high]-array[low]);
if (key == array[mid])//中间数
return mid;
else if (key > array[mid]) //数比中间的数大则在下半部分缩小范围
low = mid + 1;
else //数比中间的数小,则在上半部分缩小范围
high = mid - 1;
}
return -1;
}
4、线性索引查找:
索引是对于按先后顺序存储的海量数据,为了加快查找速度而设计的一种查找结构。索引:把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项组成。索引表:a.线性表,b.带指针,c.有序。按结构分类:线性索引,树形索引,多级索引。
1)稠密索引:是指在线性索引中,数据集中的每个记录对应一个索引项
特点:记录无序,索引表中的索引项(按关键字)有序,索引项包含2个数据项:a.关键字,b.指针缺点:索引项个数与数据集中的记录个数相同,空间代价大。
2)分块索引:把数据集的记录按分块有序的原则分成若干块,每一块对应一个索引项,特点:分块有序:a.块内无序,块内记录可以无序,b.块间有序,块与块之间按关键字有序。索引项包含3个数据项:a.最大关键字,b.快中的记录个数,c.指向块首数据元素的指针。最佳情况:分块数m=块中记录数t。
3)倒排索引,最基础的搜索技术,根据属性(字段、次关键码)的值来查找记录,不是由记录确定属性值,而是由属性值确定记录的位置。索引项通用结构,包含2个数据项:a.次关键字,b.记录号表:是指向该记录的指针、是该记录的主关键字。优点:查找记录非常快。缺点:记录表号不定长,维护(插入删除等)比较困难。
5、二叉排序树:
又称二叉查找树它或者是空树或者是若做子树不为空左子树结点值均小于根节点,右子树不为空其值均大于根节点值。构建二叉树并不是为了排序而是为了提高插入和删除关键字的速度。二叉排序树的插入操作:先查找遍历树确定没有该元素在插入。删除元素不用遍历整个树找到即可删除。插入删除性能取决于存储结构,链式存储,修改指针。查找性能取决于二叉排序树的层数/深度:最好情况,深度与完全二叉树相同(平衡状态)、近似折半查找;最坏情况,斜树(二叉树深度等于结点个数)、等同顺序查找最好时O(logn)最坏O(n)。
6、平衡二叉树(AVL树):
是二叉排序树,每个结点的左右子树高度差至多等于1。将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF。查找、插入删除时间复杂度都是O(logn)。
最小不平衡子树:距离插入节点最近的,且平衡因子的绝对值大于1的节点为根的子树,我们称为最小不平衡子树。调整不平衡二叉树思路:
1)BF=正(0、1、2),以BF=1为轴,右旋转/顺时针旋转,得到(0、0、0)。
2)BF=负(0、-1、-2),以BF=-1为轴,左旋转/逆时针旋转,得到(0、0、0)。
3)BF=一正一负(0、1、-2),先以BF=1为轴右旋转,得到(0、-1、-2),再②,得到(0、0、0)。
4)BF=一负一正(0、-1、2),先以BF=-1为轴左旋转,得到(0、1、2),再①,得到(0、0、0)。
7、多路查找树:
a.每个结点的孩子数可以多于2个,且每个结点处可以存储多个元素,b.所有元素之间存在某种特定的排序关系。打破了一个结点只存储一个元素的限制,4种特殊形式:2-3树、2-3-4树、B树、B+树。
1)2-3树:2结点或3结点,2结点:1个元素2个孩子,要么没有孩子要么有2个孩子,不能只有一个孩子,左<右。3结点:2个元素3个孩子,要么没有孩子要么有3个孩子,左<中<右。
2)2-3-4树:2结点或3结点或4结点,4结点:3个元素4个孩子,要么没有孩子要么有4个孩子,左<左中<右中<右。
B树:是平衡的多路查找树,2-3、2-3-4是B树的特例。B树的数据结构是为内外存的数据交互专门设计准备的,适用于外查找的树。内外存的查找性能更多取决于读取次数,所以设计要考虑B树的平衡和层次。
B+树:应文件系统所需而出现的一种B树的变形,解决了所有元素遍历等基本问题。适用:B+树结构特别适合带有范围的查找。前面的树都是没有重复值的,即每一个元素在该树中只出现一次,而B+树不同于之前所讲的树,在B+树结点中的元素会在叶子中再次列出,即元素会重复出现。更多关于B树的问题(微博地址)http://www.cnblogs.com/oldhorse/archive/2009/11/16/1604009.html。
8、散列查找:
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。不用比较,直接通过关键字key得到要查找记录的存储位置。散列函数、哈希(Hash)函数:f,注意:散列技术中最关键的是设计一个简单、均匀、利用率高的散列函数,散列表、哈希表(Hash table):使用散列技术将记录存储在一块连续的存储空间中,该存储空间称为散列表,动态数组。
构造散列函数:原则:a.计算简单,b.散列地址分布均匀
散列函数方法 | 描述 |
直接定址法 | 取关键字的某个线性函数,f(key)=a*key+b;优点:简单、均匀、不会产生冲突,适合事先知道关键字的分布,查找表较小且连续,不常用 |
数字分析法 | 抽取,使用关键字的一部分来计算存储位置,适合事先知道关键字的分布且关键字若干位的分布较均匀,关键字位数较多 |
平方取中法 | 先(关键字^2)再抽取中间位,适合不知道关键字分布,关键字位数较少 |
折叠法 | 适合不知道关键字分布,关键字位数较多 |
除留余数法 | 散列表长m,f(key)=key MOD p,(p≦m),其中可以对关键字取模,也可以在折叠、平方取中后再取模,缺点:p值取的不好,很容易有冲突、出现同义词,取的好也不容易避免冲突,最常用 |
随机数法 | f(key)=random(key),适合关键字长度不等 |
冲突处理方法 | |
开放定址探测 | 单向寻找,线性探测法。改进,di=1^2,-1^2,2^2,-2^2,...,q^2,-q^2,(q≦m/2),双向寻找,二次探测法,伪随机数,随机种子,di=random(di),随机探测法。 |
再散列函数 | 一旦发生冲突就换一个散列函数,优点:使得关键字不会产生聚集。缺点:增加了计算时间。 |
链地址法 | 有冲突的关键字存储在一个单链表中,同义词子表.优点:冲突较多时,不会找不到空地址缺点:查找时可能需要遍历单链表 |
公共溢出区 | 有冲突的关键字都放在公共溢出表,散列表=基本表+溢出表。查找:先通过散列函数得到散列地址在基本表中找,如果没有,再到溢出表中顺序找。适合冲突较少的情况 |
散列表的查找:散列表的查找性能取决于: 1)关键字的分布,2)散列函数的选择,3)处理冲突的方法,4)装填因子α装填因子=记录个数/散列表长度,α=n/m,通常将散列表的空间设置的比查找表/集合大,空间换时间。 |