-
数据结构概念
数据结构(data structure
)是相互之间存在一种或多种特定关系的数据元素的集合。
-
数据结构的逻辑结构
根据元素之间关系的不同特性,通常有下列4
类基本结构:
1.集合:结构中的数据元素之间除了"同属于一个集合"的关系外,别无其他关系
2. 线性结构:结构中的数据元素之间存在一对一的关系
3. 树形结构:结构中的数据元素之间存在一对多的关系
4. 图状结构或网状结构:结构中的数据元素之间存在多对多的关系
-
数据结构的存储
数据元素之间的关系有两种不同的表示方法:顺序映象和非顺序映象,并由此得到两种不同的存储结构:顺序存储结构和链式存储结构。数据的存储结构是指数据的逻辑结构在计算机中的表示
1.顺序存储结构
顺序存储方法它是把逻辑上相邻的结点存储在物理位置相邻的存储单元里,结点间的逻辑关系由存储单元的邻接关系来体现,由此得到的存储表示称为顺序存储结构。顺序存储结构是一种最基本的存储表示方法,通常借助于程序设计语言来实现。
2.链式存储结构
链接存储方法它不要求逻辑上相邻的结点在物理位置上亦相邻,结点间的逻辑关系是由附加的指针字段表示的。由此得到的存储表示称为链式存储结构,链式存储结构通常借助于程序设计语言中的指针类型来实现。
常见的数据结构如下:
-
链表
概念:链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针
链接次序实现的链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域
,另一个是存储下一个结点地址的指针域
1.单链表:单链表是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据。
//结点的结构体
typedef struct node{
int data; //数据域
struct node *next; //指针域
}NODE_t;
2. 双向链表:双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
typedef struct Dulnode {
ElemType data;
struct Dulnode *prior ,*next;
}DulNode;
3.循环链表:循环链表是另一种形式的链式存贮结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。
-
堆、栈和队列
1.堆
堆通常是一个可以被看做一棵树的数组对象。堆总是满足下列性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。
堆是非线性数据结构,相当于一维数组,有两个直接后继。
堆的定义如下:
n个元素的序列{k1,k2,ki,…,kn}当且仅当满足下关系时,称之为堆。
(ki <= k2i,ki <= k2i+1)或者(ki >= k2i,ki >= k2i+1), (i = 1,2,3,4...n/2)
2.栈
栈(stack)栈是限定仅在表尾进行插入和删除操作的线性表。因此,对栈来说,表尾端具有特色含义,称为栈顶
(top),相应的,表头端成为栈底
(bottom)。不含元素的空表称为空栈
。
假设栈 S=(a1,a2,···,an),则称a1为栈底元素,an为栈顶元素。栈中元素按a1,a2,···,an的次序进栈,退栈的第一个元素应为栈顶元素。换句话说,栈的修改是按后进先出的原则进行的(如图(a)所示)。因此,栈又称为后进先出(last in first out)的线形表(简称LIFO结构),它的这个特点可用图(b)所示的铁路调度站形象的表示。
下面是一个C的链表栈
#include
#include
struct Stack{
int size;
struct List{
int item;
struct List *next;
} *head;
};
typedef struct Stack *slink;
//创建一个链表栈
slink create_stack(void){
slink stack=malloc(sizeof(struct Stack));
if(!stack)
return stack;
stack->size=0;
stack->head=NULL;
return stack;
}
//判断栈中的元素是否为空
int empty(slink stack){
return stack->size==0;
}
//压栈
void push_stack(slink stack,int item){
struct List *new=malloc(sizeof(struct List));
if(!new)
return;
new->item=item;
new->next=stack->head;
stack->head=new;
stack->size++;
}
//出栈
int pop_stack(slink stack){
if(empty(stack))
return -1;
struct List *torm=stack->head;
int retval=stack->head->item;
stack->head=stack->head->next;
free(torm);
stack->size--;
return retval;
}
3.1队列
队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front
)进行删除操作,而在表的后端(rear
)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out
)
下面是一个C的普通链表队列
#include
#include
struct List{
int item;
struct List *next;
};
struct Queue{
int size;
struct List *head;
struct List *rear;
};
typedef struct Queue *qlink;
//创建队列
qlink create_queue(void){
qlink queue=malloc(sizeof(struct Queue));
if(!queue)
return queue;
queue->size=0;
queue->head=queue->rear=NULL;
return queue;
}
//查看队列是否为空
int empty(qlink queue){
return queue->size==0;
}
//enter queue
void enqueue(qlink queue,int item){
struct List *new=malloc(sizeof(struct List));
if(!new)
return;
new->item=item;
new->next=NULL;
if(queue->head)
queue->rear->next=new;
else
queue->head=new;
queue->rear=new;
queue->size++;
}
//delete queue
int dequeue(qlink queue){
if(empty(queue))
return -1;
int retval=queue->head->item;
struct List *torm=queue->head;
if(queue->head==queue->rear)
queue->rear=NULL;
queue->head=queue->head->next;
free(torm);
queue->size--;
return retval;
}
3.2优先级队列
优先级队列(priority queue) 是0个或多个元素的集合,每个元素都有一个优先权,对优先级队列执行的操作有
(1)查找
(2)插入一个新元素
(3)删除
一般情况下,查找操作用来搜索优先权最大的元素,删除操作用来删除该元素 。对于优先权相同的元素,可按先进先出次序处理或按任意优先权进行。
-
哈希表
散列表(Hash table
,也叫哈希表),是根据关键码值(Key value
)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
基本概念
- 若关键字为
k
,则其值存放在f(k)
的存储位置上。由此,不需比较便可直接取得所查记录。称这个对应关系f为散列函数,按这个思想建立的表为散列表。 - 对不同的关键字可能得到同一散列地址,即
k1≠k2
,而f(k1)=f(k2)
,这种现象称为冲突(英语:Collision
)。具有相同函数值的关键字对该散列函数来说称做同义词。综上所述,根据散列函数f(k)和处理冲突的方法将一组关键字映射到一个有限的连续的地址集(区间)上,并以关键字在地址集中的“像”作为记录在表中的存储位置,这种表便称为散列表,这一映射过程称为散列造表或散列,所得的存储位置称散列地址。 - 若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,则称此类散列函数为均匀散列函数(
Uniform Hash function
),这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。
常用方法
散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。
实际工作中需视不同的情况采用不同的哈希函数,通常考虑的因素有:
· 计算哈希函数所需时间
· 关键字的长度
· 哈希表的大小
· 关键字的分布情况
· 记录的查找频率
1.直接寻址法
取关键字或关键字的某个线性函数值为散列地址。即H(key)=key
或H(key) = a·key + b
,其中a
和b
为常数(这种散列函数叫做自身函数)。若其中H(key)
中已经有值了,就往下一个找,直到H(key)
中没有值了,就放进去。
例如:关键字集合为{100, 400,600, 200, 800, 900}
,利用直接定址法,若选取散列函数为H(x) = x/100
,则散列表如下:
2. 数字分析法
分析一组数据,比如一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数字差别很大,如果用后面的数字来构成散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
3. 平方取中法
当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以较高的概率产生不同的哈希地址。
例:我们把英文字母在字母表中的位置序号作为该英文字母的内部编码。例如K的内部编码为11
,E
的内部编码为05
,Y
的内部编码为25
,A的内部编码为01
, B
的内部编码为02
。由此组成关键字“KEYA”的内部代码为11052501
,同理我们可以得到关键字“KYAB”、“AKEY”、“BKEY”的内部编码。之后对关键字进行平方运算后,取出第7
到第9
位作为该关键字哈希地址,如下图所示
4. 折叠法
将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址。数位叠加可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。
5.随机数法
选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
6. 除留余数法
取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p,p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
处理冲突
在选取散列函数时,由于很难选取一个既均匀分布又简单,同时保证关键字和散列地址一一对应的散列表,所以冲突时不可避免的。如果具有不同关键字的k
个数据元素的散列地址完全相同,就必须为 k-1
个数据元素重新分配存储单元。通常称其为"溢出"的数据元素。
常用的处理冲突的方法有两种:
- 将溢出的数据元素存放到散列表中没有使用的单元中
· 线性探针法:在数组中从映射到的位置开始顺序查找空位置,如果需要,从最后一个位置绕回第一个位置。这种方法碰撞可能引起连锁反应,使表中形成一些较长的连续被占用的单元,如:从1---10
的地址全部被使用。从而使散列表的性能降低。
· 二次探测发:不直接检查下一个单元,而是检查远离初始探测点的某一单元,以消除线性探测法中的初始聚集的问题.
· 再散列法:有两个散列函数H1
和H2
。H1
用来计算探测序列的起始地址,H2
用来计算下一个位置的步长。第二个散列函数必须仔细选择。例如,它永远不能计算出0
,必须保证所有单元能够探测到。
- 将映射到同一地址的数据元素分别保存到散列表以外的各自线性表中
由于地址相同的数据元素个数变化比较大,因此通常采用链表的方式。散列表本身只保存一个指向各自链表中第一个节点的指针。这种方法称为“开散列表",或拉链法,可以理解为“链表的数组”。
开散列表将具有同一散列地址的数据元素都存储在一个单链表中。在散列表中插入、查找或删除一个元素,就是在对应的单链表中进行的。为了方便操作,将单链表设计为带头结点的单链表。
例如: 一个规模为11
的开散列表中依次插入关键字17、21、23、60、29、38......
,散列函数为
H(Key)= key mod 11
,散列表如下:
在开散列表中,插入、删除和查找操作的实现都相当简单。
插入x时,首先计算H(x),将x插入到H(x)指向的单链表的表头。
查找时,也是先计算H(x),然后顺序查找H(x)指向的单链表。
删除操作同样简单,先计算H(x),然后顺序查找H(x)指向的单链表,
找到x后删除
算法的时间和空间复杂度参考链接