数据结构有哪些(常见的10种数据结构介绍)

 分类:IT知识时间:2023-03-13 07:31:01点击:

1、数组

数组:数组是一种聚合数据类型,它是将具有相同类型的若干变量有序地组织在一起的集合。

定义结构体数组的方法很简单,同定义结构体变量是一样的,只不过将变量改成数组。

或者说同普通数组的定义是一模一样的,如:

struct STUDENT stu[10];

这就定义了一个结构体数组,共有 10 个元素,每个元素都是一个结构体变量,都包含所有的结构体成员。

结构体数组的引用与引用一个结构体变量在原理上是一样的。只不过结构体数组中有多个结构体变量,我们只需利用 for 循 环一个一个地使用结构体数组中的元素。

下面编写一个程序,编程要求:从键盘输入 5 个学生的基本信息,如姓名、年龄、性别、学号,然后将学号最大的学生的基本信息输出到屏幕(如果相同则输出最后一个)。

#define_CRT_SECURE_NO_WARNINGS //避免scanf报错# include<stdio.h># include<string.h>/* 编程要求: 从键盘输入 5 个学生的基本信息,如姓名、年龄、性别、学号,然后将学号最大的学生的基本信息输出到屏幕(如果相同则输出最后一个)。 */structSTU{charname[20]; intage; charsex; charnum[20]; }; voidOutputSTU(struct STU stu[5]); //函数声明, 该函数的功能是输出学号最大的学生信息intmain(void){ inti; structSTUstu[5];for(i = 0; i<5; ++i) { printf("请输入第%d个学生的信息:", i + 1); scanf("%s%d %c%s", stu[i].name, &stu[i].age, &stu[i].sex, stu[i].num);/*%c前面要加空格, 不然输入时会将空格赋给%c*/} OutputSTU(stu); system("PAUSE");//结束不退出} voidOutputSTU(struct STU stu[5]){ structSTUstumax= stu[0];intj; for(j = 1; j<5; ++j) { if(strcmp(stumax.num, stu[j].num) < 0) //strcmp函数的使用{ stumax = stu[j]; } } printf("学生姓名:%s 学生年龄:%d 学生性别:%c 学生学号:%s\n", stumax.name, stumax.age, stumax.sex, stumax.num); }

2、链表

链表:链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上存在非连续的特点。

链表,别名链式存储结构或单链表,用于存储逻辑关系为 "一对一" 的数据。链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。

例如,使用链表存储 {1,2,3},数据的物理存储状态如图 1 所示:

我们看到,图 1 根本无法体现出各数据之间的逻辑关系。

对此,链表的解决方案是,每个数据元素在存储时都配备一个指针,用于指向自己的直接后继元素。如图 2 所示:

像图 2 这样,数据元素随机存储,并通过指针表示数据之间逻辑关系的存储结构就是链式存储结构。

链表有 单链表、双向链表、静态链表,展开说的话比较多,这里就演示单链表,想要学习更多链表直接点击链接进入学习。

链表的节点

从图 2 可以看到,链表中每个数据的存储都由以下两部分组成:

  1. 数据元素本身,其所在的区域称为数据域;
  2. 指向直接后继元素的指针,所在的区域称为指针域;

即链表中存储各数据元素的结构如图 3 所示:

图 3 所示的结构在链表中称为节点。也就是说,链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中,如图 4 所示:

因此,链表中每个节点的具体实现,需要使用 C 语言中的结构体,具体实现代码为:

typedefstructLink{charelem; //代表数据域structLink* next;//代表指针域,指向直接后继元素}link; //link为节点名,每个节点都是一个 link 结构体

提示,由于指针域中的指针要指向的也是一个节点,因此要声明为 Link 类型(这里要写成 struct Link* 的形式)。

头节点,头指针和首元节点

其实,图 4 所示的链表结构并不完整。一个完整的链表需要由以下几部分构成:

  1. 头指针:一个普通的指针,它的特点是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据;
  2. 节点:链表中的节点又细分为头节点、首元节点和其他节点:头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点。对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题;首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义;其他节点:链表中其他的节点;

因此,一个存储 {1,2,3} 的完整链表结构如图 5 所示:

注意:链表中有头节点时,头指针指向头节点;反之,若链表中没有头节点,则头指针指向首元节点。

明白了链表的基本结构,下面我们来学习如何创建一个链表。

链表的创建(初始化)

创建一个链表需要做如下工作:

  1. 声明一个头指针(如果有必要,可以声明一个头节点);
  2. 创建多个存储数据的节点,在创建的过程中,要随时与其前驱节点建立逻辑关系;

例如,创建一个存储 {1,2,3,4} 且无头节点的链表,C 语言实现代码如下:

link * initLink(){ link * p=NULL;//创建头指针link * temp = (link*)malloc(sizeof(link));//创建首元节点//首元节点先初始化temp->elem = 1; temp->next = NULL; p = temp;//头指针指向首元节点//从第二个节点开始创建for(inti=2; i<5; i++) { //创建一个新节点并初始化link *a=(link*)malloc(sizeof(link)); a->elem=i; a->next=NULL; //将temp节点与新建立的a节点建立逻辑关系temp->next=a; //指针temp每次都指向新链表的最后一个节点,其实就是 a节点,这里写temp=a也对temp=temp->next; } //返回建立的节点,只返回头指针 p即可,通过头指针即可找到整个链表returnp; }

3、堆

堆:堆是一种特殊的树形数据结构,一般讨论的堆都是二叉堆。

二叉堆是完全二元树或者是近似完全二元树,它分为两种:最大堆和最小堆。

最大堆:父结点的键值总是大于或等于任何一个子节点的键值;

最小堆:父结点的键值总是小于或等于任何一个子节点的键值。示意图如下:

假设在最大堆[90,80,70,60,40,30,20,10,50]种添加85,需要执行的步骤如下:

如上图所示,当向最大堆中添加数据时:先将数据加入到最大堆的最后,然后尽可能把这个元素往上挪,直到挪不动为止!

将85添加到[90,80,70,60,40,30,20,10,50]中后,最大堆变成了[90,85,70,60,80,30,20,10,50,40]。

/* * 最大堆的向上调整算法(从start开始向上直到0,调整堆) * * 注:数组实现的堆中,第N个节点的左孩子的索引值是(2N+1),右孩子的索引是(2N+2)。 * * 参数说明: * start -- 被上调节点的起始位置(一般为数组中最后一个元素的索引) */staticvoidmaxheap_filterup(intstart){ intc = start; // 当前节点(current)的位置intp = (c-1)/2; // 父(parent)结点的位置 inttmp = m_heap[c]; // 当前节点(current)的大小while(c > 0) { if(m_heap[p] >= tmp) break; else{ m_heap[c] = m_heap[p]; c = p; p = (p-1)/2; } } m_heap[c] = tmp; } /* * 将data插入到二叉堆中 * * 返回值: * 0,表示成功 * -1,表示失败 */intmaxheap_insert(intdata){ // 如果"堆"已满,则返回if(m_size == m_capacity) return-1; m_heap[m_size] = data; // 将"数组"插在表尾maxheap_filterup(m_size); // 向上调整堆m_size++; // 堆的实际容量+1return0; }

4、栈

栈:栈是一种特殊的线性表,它只能在一个表的一个固定端进行数据结点的插入和删除操作。

栈是限制插入和删除只能在一个位置上进行的线性表。其中,允许插入和删除的一端位于表的末端,叫做栈顶(top),不允许插入和删除的另一端叫做栈底(bottom)。

对栈的基本操作有 PUSH(压栈)POP (出栈),前者相当于表的插入操作(向栈顶插入一个元素),后者则是删除操作(删除一个栈顶元素)。

栈是一种后进先出(LIFO)的数据结构,最先被删除的是最近压栈的元素。

栈就像是一个箱子,往里面放入一个小盒子就相当于压栈操作,往里面取出一个小盒子就是出栈操作,取盒子的时候,最后放进去的盒子会最先被取出来,最先放进去的盒子会最后被取出来,这即是后入先出。

下面是一个栈的示意图:

如下入栈出栈示例:

#define _CRT_SECURE_NO_WARNINGS //避免scanf报错#include <stdio.h>//元素elem进栈 intpush(int* a, inttop, intelem){ a[++top] = elem; returntop; } //数据元素出栈 intpop(int* a, inttop){ if(top == -1) { printf("空栈"); return-1; } printf("弹栈元素:%d\n", a[top]); top--; returntop; } intmain() { inta[100]; inttop = -1; top = push(a, top, 1); top = push(a, top, 2); top = push(a, top, 3); top = push(a, top, 4); top = pop(a, top); top = pop(a, top); top = pop(a, top); top = pop(a, top); top = pop(a, top); system("PAUSE");//结束不退出 }

5、队列

队列:队列和栈类似,也是一种特殊的线性表。和栈不同的是,队列只允许在表的一端进行插入操作,而在另一端进行删除操作。

与栈结构不同的是,队列的两端都"开口",要求数据只能从一端进,从另一端出,如图 1 所示:

通常,称进数据的一端为 "队尾",出数据的一端为 "队头",数据元素进队列的过程称为 "入队",出队列的过程称为 "出队"。

不仅如此,队列中数据的进出要遵循 "先进先出" 的原则,即最先进队列的数据元素,同样要最先出队列。

拿图 1 中的队列来说,从数据在队列中的存储状态可以分析出,元素 1 最先进队,其次是元素 2,最后是元素 3。

此时如果将元素 3 出队,根据队列 "先进先出" 的特点,元素 1 要先出队列,元素 2 再出队列,最后才轮到元素 3 出队列。

栈和队列不要混淆,栈结构是一端封口,特点是"先进后出";而队列的两端全是开口,特点是"先进先出"。

因此,数据从表的一端进,从另一端出,且遵循 "先进先出" 原则的线性存储结构就是队列。

如下示例:

#define_CRT_SECURE_NO_WARNINGS //避免scanf报错#include<stdio.h>intenQueue(int*a, intrear, intdata){ a[rear] = data; rear++; returnrear; } voiddeQueue(int*a, intfront, intrear){ //如果 front==rear,表示队列为空while(front != rear) { printf("出队元素:%d\n", a[front]); front++; } } intmain(){ inta[100]; intfront, rear; //设置队头指针和队尾指针,当队列中没有元素时,队头和队尾指向同一块地址front = rear = 0; //入队rear = enQueue(a, rear, 1); rear = enQueue(a, rear, 2); rear = enQueue(a, rear, 3); rear = enQueue(a, rear, 4); //出队deQueue(a, front, rear); system("PAUSE");//结束不退出}

6、散列表(哈希表)

散列表:散列表源自于散列函数(Hash function),其思想是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,

这样就可以不用进行比较操作而直接取得所查记录。

构造散列函数考虑的因素:

  • 执行速度
  • 关键字长度
  • 散列表大小
  • 关键字的分布情况
  • 查找频率

根据元素集合的特性构造

  • 要求一:n 个数据原仅占用 n 个地址,虽然散列查找是以空间换时间,但仍希望散列的地址空间尽量小
  • 要求二:无论用什么方法储存,目的都是尽量均匀地存放元素,以避免冲突

解决冲突的办法:

  • 直接定址法
  • (取关键字的某个线性函数值为散列地址:f (key) = a*key + b )
  • 简单、均匀也不会有冲突但需要事先知道关键字的排布,适合表较小且连续的情况
  • 数字分析法
  • 平方取中法
  • 折叠法
  • 除留取余法
  • Hash(key) = key % p (p 是整数),设表长为 m ,取 p <= m 且为质数
  • 随机数法

结构:

#defineMAXSIZE 1000#defineNULLKEY -65535typedefstruct{int* elem; //数据元素存储基址,数组intsize; //元素个数}HashTable; intm = 0;//散列表表长

散列表的创建:

voidIniHashTable(HashTable* H){ inti; m = MAXSIZE; H->size = m; H->elem = (int*)malloc(m* sizeof(int)); for(i = 0; i < m; i++) { H->elem[i] = NULLKEY; } }

散列函数:

根据不同的情况改变算法

intHash(intkey){ returnkey % m; //除留取余法}

插入元素:

voidInsertHash(HashTable* H, intkey){ intaddr = Hash(key); //求散列地址while(H->elem[addr] != NULLKEY) //不为空则冲突{ addr = (addr + 1) % m; //开放地址法的线性探测} H->elem[addr] = key; //发现有空位后插入}

查找元素:

intSearch(HashTable* H, intkey){ intaddr = Hash(key); //求散列地址while(H->elem[addr] != key) //不为空则冲突{ addr = (addr + 1) % m; //开放地址法的线性探测if(H->elem[addr] == NULLKEY || addr == Hash(key)) { returnfalse; } } returntrue; }

7、二叉树

二叉树:二叉树是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。

二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树 。

二叉排序树要么是空二叉树,要么具有如下特点:

  • 二叉排序树中,如果其根结点有左子树,那么左子树上所有结点的值都小于根结点的值;
  • 二叉排序树中,如果其根结点有右子树,那么右子树上所有结点的值都大小根结点的值;
  • 二叉排序树的左右子树也要求都是二叉排序树;

例如,图 1 就是一个二叉排序树:

使用二叉排序树查找关键字

二叉排序树中查找某关键字时,查找过程类似于次优二叉树,在二叉排序树不为空树的前提下,首先将被查找值同树的根结点进行比较,会有 3 种不同的结果:

  • 如果相等,查找成功;
  • 如果比较结果为根结点的关键字值较大,则说明该关键字可能存在其左子树中;
  • 如果比较结果为根结点的关键字值较小,则说明该关键字可能存在其右子树中;

实现函数为:(运用递归的方法)

BiTree SearchBST(BiTree T,KeyType key){ //如果递归过程中 T 为空,则查找结果,返回NULL;或者查找成功,返回指向该关键字的指针if(!T || key==T->data) { returnT; }elseif(key<T->data){ //递归遍历其左孩子returnSearchBST(T->lchild, key); }else{ //递归遍历其右孩子returnSearchBST(T->rchild, key); } }

使用二叉排序树在查找表中做查找操作的时间复杂度同建立的二叉树本身的结构有关。即使查找表中各数据元素完全相同,但是不同的排列顺序,构建出的二叉排序树大不相同。

例如:查找表 {45,24,53,12,37,93} 和表 {12,24,37,45,53,93} 各自构建的二叉排序树图下图所示:

使用二叉排序树实现动态查找操作的过程,实际上就是从二叉排序树的根结点到查找元素结点的过程,所以时间复杂度同被查找元素所在的树的深度(层次数)有关。

为了弥补二叉排序树构造时产生如图 5 右侧所示的影响算法效率的因素,需要对二叉排序树做“平衡化”处理,使其成为一棵平衡二叉树。

更多详情点击 二叉排序树(二叉查找树)及C语言实现

8、跳表

跳表:跳表是一个随机化的数据结构,可以被看做二叉树的一个变种,它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,目前在Redis和LeveIDB中都有用到。

跳表使用空间换时间的设计思路,通过构建多级索引来提高查询的效率,实现了基于链表的“二分查找”。

跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都是 O(logn)。跳表的空间复杂度是 O(n)。

不过,跳表的实现非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。

详细见:跳表C语言实现详解

9、图

图:图是另一种非线性数据结构。在图结构中,数据结点一般称为顶点,而边是顶点的有序偶对。

使用图结构表示的数据元素之间虽然具有“多对多”的关系,但是同样可以采用顺序存储,也就是使用数组有效地存储图。

使用数组存储图时,需要使用两个数组,一个数组存放图中顶点本身的数据(一维数组),另外一个数组用于存储各顶点之间的关系(二维数组)。

存储图中各顶点本身数据,使用一维数组就足够了;存储顶点之间的关系时,要记录每个顶点和其它所有顶点之间的关系,所以需要使用二维数组。

不同类型的图,存储的方式略有不同,根据图有无权,可以将图划分为两大类:图和网 。

图,包括无向图和有向图;

网,是指带权的图,包括无向网和有向网。

存储方式的不同,指的是:在使用二维数组存储图中顶点之间的关系时,如果顶点之间存在边或弧,在相应位置用 1 表示,反之用 0 表示;

如果使用二维数组存储网中顶点之间的关系,顶点之间如果有边或者弧的存在,在数组的相应位置存储其权值;反之用 0 表示。

结构代码表示:

#defineMAX_VERtEX_NUM 20 //顶点的最大个数#defineVRType int //表示顶点之间的关系的变量类型#defineInfoType char //存储弧或者边额外信息的指针变量类型#defineVertexType int //图中顶点的数据类型typedefenum{DG,DN,UDG,UDN}GraphKind; //枚举图的 4 种类型typedefstruct{VRType adj; //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。InfoType * info; //弧或边额外含有的信息指针}ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM]; typedefstruct{VertexType vexs[MAX_VERtEX_NUM]; //存储图中顶点数据AdjMatrix arcs; //二维数组,记录顶点之间的关系intvexnum,arcnum; //记录图的顶点数和弧(边)数GraphKind kind; //记录图的种类}MGraph;

例如,存储图 1 中的无向图(B)时,除了存储图中各顶点本身具有的数据外,还需要使用二维数组存储任意两个顶点之间的关系。

由于 (B) 为无向图,各顶点没有权值,所以如果两顶点之间有关联,相应位置记为 1 ;反之记为 0 。构建的二维数组如图 2 所示。

在此二维数组中,每一行代表一个顶点,依次从 V1 到 V5 ,每一列也是如此。比如 arcs[0][1] = 1 ,表示 V1 和 V2 之间有边存在;而 arcs[0][2] = 0,说明 V1 和 V3 之间没有边。

对于无向图来说,二维数组构建的二阶矩阵,实际上是对称矩阵,在存储时就可以采用压缩存储的方式存储下三角或者上三角。

通过二阶矩阵,可以直观地判断出各个顶点的度,为该行(或该列)非 0 值的和。例如,第一行有两个 1,说明 V1 有两个边,所以度为 2。

存储图 1 中的有向图(A)时,对应的二维数组如图 3 所示:

更多详情点击 图的顺序存储结构(包含C语言实现)

10、Trie树

Trie树:又称单词查找树,Trie树,字典树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),

所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

如下代码,定义结构体、初始化Trie树、插入、查找、删除:

#define_CRT_SECURE_NO_WARNINGS //避免scanf报错#include<stdio.h>#defineMAX 26 //26个字母#defineSLEN 100 //节点中存储的字符串长度//Trie结构体定义structTrie{structTrie*next[MAX];chars[SLEN]; //节点处存储的字符串intisword; //节点处是否为单词charval; //节点的代表字符} *root; //初始化Trie树struct Trie *init(){ structTrie*root= (structTrie*)malloc(sizeof(structTrie));inti; for(i = 0; i < MAX; i++) { root->next[i] = NULL; } root->isword = 0; root->val = 0; returnroot; } //按照指定路径path 插入字符串 svoidinsert(charpath[], chars[]){ structTrie*t, *p= root;inti, j, n = strlen(path); for(i = 0; i < n; i++) { if(p->next[path[i] - 'a'] == NULL) { t = (struct Trie *)malloc(sizeof(struct Trie)); for(j = 0; j < MAX; j++) { t->next[j] = NULL; t->isword = 0; } t->val = path[i]; p->next[path[i] - 'a'] = t; } p = p->next[path[i] - 'a']; } p->isword = 1; strcpy(p->s, s); } //按照指定路径 path 查找char*find(charpath[], intdelflag){ structTrie*p= root;inti = 0, n = strlen(path); while(p && path[i]) { p = p->next[path[i++] - 'a']; } if(p && p->isword) { p->isword = delflag; returnp->s; } returnNULL; } //删除整棵Trie树voiddel(struct Trie *root){ inti; if(!root) return; for(i = 0; i < MAX; i++) { if(root->next[i]) del(root->next[i]); free(root->next[i]); } }
除注明外的文章,均为来源:老汤博客,转载请保留本文地址!
原文地址: