数据结构与算法

数据结构(英语:data structure)是计算机中存储、组织数据的方式。

数据结构是一种具有一定逻辑关系,在计算机中应用某种存储结构,并且封装了相应操作的数据元素集合。它包含三方面的内容,逻辑关系、存储关系及操作。

不同种类的数据结构适合于不同种类的应用,而部分甚至专门用于特定的作业任务。例如,计算机网络依赖于路由表运作,B 树高度适用于数据库的封装。

为什么要学习数据结构和算法?

随着应用程序变得越来越复杂和数据越来越丰富,几百万、几十亿甚至几百亿的数据就会出现,

而对这么大对数据进行搜索、插入或者排序等的操作就越来越慢,数据结构就是用来解决这些问题的。

常见的10种数据结构

1、数组

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

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

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

struct STUDENT stu[10];

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

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

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

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
# include <stdio.h>
# include <string.h>

/*
编程要求:
    从键盘输入 5 个学生的基本信息,如姓名、年龄、性别、学号,然后将学号最大的学生的基本信息输出到屏幕(如果相同则输出最后一个)。
*/
struct STU
{
    char name[20];
    int age;
    char sex;
    char num[20];
};
void OutputSTU(struct STU stu[5]);  //函数声明, 该函数的功能是输出学号最大的学生信息
int main(void)
{
    int i;
    struct STU stu[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");//结束不退出
}
void OutputSTU(struct STU stu[5])
{
    struct STU stumax = stu[0];
    int j;
    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 语言中的结构体,具体实现代码为:

typedef struct Link{
    char elem; //代表数据域
    struct Link * 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 (int i=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即可,通过头指针即可找到整个链表
    return p;
}

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 -- 被上调节点的起始位置(一般为数组中最后一个元素的索引)
 */
static void maxheap_filterup(int start)
{
    int c = start;            // 当前节点(current)的位置
    int p = (c-1)/2;        // 父(parent)结点的位置 
    int tmp = 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,表示失败
 */
int maxheap_insert(int data)
{
    // 如果"堆"已满,则返回
    if(m_size == m_capacity)
        return -1;
 
    m_heap[m_size] = data;        // 将"数组"插在表尾
    maxheap_filterup(m_size);    // 向上调整堆
    m_size++;                    // 堆的实际容量+1

    return 0;
}

更多详情请点击:二叉堆(一)之 图文解析 和 C语言的实现 :https://www.cnblogs.com/skywang12345/p/3610187.html

4、栈

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

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

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

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

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

下面是一个栈的示意图:

 如下入栈出栈示例:

#define _CRT_SECURE_NO_WARNINGS  //避免scanf报错
#include <stdio.h>
//元素elem进栈
int push(int* a, int top, int elem){
    a[++top] = elem;
    return top;
}
//数据元素出栈
int pop(int * a, int top){
    if (top == -1) {
        printf("空栈");
        return -1;
    }
    printf("弹栈元素:%d\n", a[top]);
    top--;
    return top;
}
int main() {
    int a[100];
    int top = -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>
int enQueue(int *a, int rear, int data){
    a[rear] = data;
    rear++;
    return rear;
}
void deQueue(int *a, int front, int rear){
    //如果 front==rear,表示队列为空
    while (front != rear) {
        printf("出队元素:%d\n", a[front]);
        front++;
    }
}
int main() {
    int a[100];
    int front, 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 且为质数
  • 随机数法

结构:

#define MAXSIZE 1000
#define NULLKEY -65535
typedef struct
{
    int* elem;  //数据元素存储基址,数组
    int size;  //元素个数
}HashTable;
int m = 0;//散列表表长

散列表的创建:

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

散列函数:
根据不同的情况改变算法

int Hash(int key)
{
    return key % m;  //除留取余法
}

插入元素:

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

查找元素:

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

7、二叉树

二叉树:二叉树是指树中节点的度不大于2的有序树,它是一种最简单且最重要的树。
二叉树的递归定义为:二叉树是一棵空树,或者是一棵由一个根节点和两棵互不相交的,分别称作根的左子树和右子树组成的非空树;左子树和右子树又同样都是二叉树 。

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

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

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

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

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

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

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

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

结构代码表示:

#define MAX_VERtEX_NUM 20                   //顶点的最大个数
#define VRType int                          //表示顶点之间的关系的变量类型
#define InfoType char                       //存储弧或者边额外信息的指针变量类型
#define VertexType int                      //图中顶点的数据类型
typedef enum{DG,DN,UDG,UDN}GraphKind;       //枚举图的 4 种类型
typedef struct {
    VRType adj;                             //对于无权图,用 1 或 0 表示是否相邻;对于带权图,直接为权值。
    InfoType * info;                        //弧或边额外含有的信息指针
}ArcCell,AdjMatrix[MAX_VERtEX_NUM][MAX_VERtEX_NUM];

typedef struct {
    VertexType vexs[MAX_VERtEX_NUM];        //存储图中顶点数据
    AdjMatrix arcs;                         //二维数组,记录顶点之间的关系
    int vexnum,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>
#define MAX 26    //26个字母
#define SLEN 100   //节点中存储的字符串长度

//Trie结构体定义
struct Trie
{
    struct Trie *next[MAX];
    char s[SLEN];      //节点处存储的字符串
    int isword;         //节点处是否为单词
    char val;           //节点的代表字符
} *root;
//初始化Trie树
struct Trie *init()
{
    struct Trie *root = (struct Trie *)malloc(sizeof(struct Trie));
    int i;
    for (i = 0; i < MAX; i++)
    {
        root->next[i] = NULL;
    }
    root->isword = 0;
    root->val = 0;
    return root;
}
//按照指定路径path 插入字符串 s
void insert(char path[], char s[])
{
    struct Trie *t, *p = root;
    int i, 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(char path[], int delflag)
{
    struct Trie *p = root;
    int i = 0, n = strlen(path);
    while (p && path[i])
    {
        p = p->next[path[i++] - 'a'];
    }
    if (p && p->isword)
    {
        p->isword = delflag;
        return p->s;
    }
    return NULL;
}
//删除整棵Trie树
void del(struct Trie *root)
{
    int i;
    if (!root)
        return;
    for (i = 0; i < MAX; i++)
    {
        if (root->next[i])
            del(root->next[i]);
        free(root->next[i]);
    }
}

下集预告

基础夯实:基础数据结构与算法(二)

 

参考文献

单链表:http://c.biancheng.net/view/3336.html

双向链表:http://c.biancheng.net/view/3342.html

静态链表:http://c.biancheng.net/view/3339.html

二叉堆(一)之 图文解析 和 C语言的实现 :https://www.cnblogs.com/skywang12345/p/3610187.html

二叉排序树(二叉查找树)及C语言实现:http://c.biancheng.net/view/3431.html

跳表C语言实现详解:https://blog.csdn.net/m0_37845735/article/details/103691814

图的顺序存储结构(包含C语言实现):http://c.biancheng.net/view/3407.html

 

欢迎关注订阅微信公众号【熊泽有话说】,更多好玩易学知识等你来取
作者:熊泽-学习中的苦与乐
公众号:熊泽有话说

QQ群:711838388
出处:https://www.cnblogs.com/xiongze520/p/15812527.html
您可以随意转载、摘录,但请在文章内注明作者和原文链接。