数据结构总结

数据结构

首先要清楚两种储存结构:
1.顺序储存结构,也就是我们用的数组.
2.链式储存结构,也就是链表.
两个存储结构的访问方式也有差异
顺序存储结构利用变量以及变量的下标进行访问.
链式则使用指针进行访问,也就是访问结构体的地址.(指针储存的就是地址)

基本的数据结构有以下几种

1.线性表

哎,就是链表嘛,定义一个结构体储存元素和下一个结构体的指针就ok了

struct node
{
int data;//数据自己想填什么填什么我这里用int代替
struct node *next;
}

复杂一点的就是双向链表和循环链表
双向链表就再在结构体中加一个previous指针指向前驱节点

struct node
{
int data;//数据自己想填什么填什么我这里用int代替
struct node *next,*previous;
}

循环链表将尾节点的next指针指向头节点就行了.

2.栈

什么是栈?举一个形象的比喻,栈好比一个杯子,一个只有一个开口的容器,元素放进去叫做入栈,拿出来叫做出栈.


image.png

image.png

根据图示可以看出栈需要3个东西1.栈顶.2.栈底.3.数据.
其实栈底也可以不要,它可以起到一个哨兵的作用.

#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int SElemType; /* SElemType 根据实际情况而定,这里假设为int */

/* 顺序栈结构 */
typedef struct 
{
    SElemType data[MAXSIZE];
    int top; /* 用于栈顶指针 */
} SqStack;

这里注意一下当存储结构用的是数组的话,"top"就用栈顶元素数组的下标,如果存储结构用链表的话"top"就应该是栈顶的指针.
栈顶是变化的栈底是不变的,所以每次进栈与出栈"top"都会随之升高或降低.
如何判断是否是一个空栈??简单,顺序存储结构top=-1,链式存储结构top=NULL
栈有一个特征,先进后出,后进先出.好好看看图就明白了
递归也就是一种栈的实现.想想是不是这样.

3.队列

如果说栈是一个杯子 ,那队列就是一个水管.这个比喻够形象了.栈是先进后出,而队列 则是先进先出.
但由于数组自身的限制如果用顺序存储结构来构造队列会出现一些问题.1.大小是开始就定义好的,这样就会有溢出的风险.2.对队列的实现太耗cpu了.所以我建议使用链式存储结构进行实现.

//先建立一个链表的结构体
struct node
{
int data;
struct node *next;
}
//再建立一个队列的结构体
{
struct node *head,*tail;//一个头指针,一个尾指针
}

队列的结构就建好了,怎么用就要发挥你的想象力了.其实就是一个储存数据的结构,所以叫数据结构.
就是用头,尾指针的移动来进行数据的储存和输出.一头用来储存一头用来输出.至于具体哪一头,看你心情.
不知道聪明的你有没有发现一个问题,所谓的数据结构就是在基础的顺序存储结构与链式存储结构的基础上进行封装,使其实现新的功能.没错这就是为什么我一开始就介绍两种存储结构的原因.

4.串

串(string)是由零个或多个字符组成的有限序列,又名叫字符串。(没错,这就是cope来的定义)
很好理解

#define MAXSIZE 40 /* 存储空间初始分配量 */


typedef char String[MAXSIZE + 1]; /*  0号单元存放串的长度 */

/* 生成一个其值等于chars的串T */
Status StrAssign(String T, char *chars)
{
    int i;
    if (strlen(chars) > MAXSIZE)
        return 0;
    else
    {
        T[0] = strlen(chars); //长度
        for (i = 1; i <= T[0]; i++)
            T[i] = *(chars + i - 1);
        return 1;
    }
}

用顺序存储结构(数组)还是要遇到老问题,浪费时间还容易溢出.万一你写的串超过了定义的长度呢?
哎~指针可是一个好东西,结构体也是一个好东西.

struct string
{
    char *data;
    int lenth;
};

这样定义就不用担心溢出的问题了.
讲到串怎么能放过匹配算法呢.
什么是匹配算法?
要想知道一个大串中是否包含一个小串,就像是集合中的包含与被包含关系
比如主串S="goodgoogle"中,找到T="google"这个子串的位置。
就要用的匹配算法,下面的链接就介绍了匹配算法.我就不再写一遍了.
https://www.jianshu.com/p/6c24cf1aad5f

5.树

树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
一个树节点可以有多个子节点,但是每定义一个子节点都要在结构体上新定义一个指针,,不雅观,不好看,最主要的还是不好用,所以最常用的树自然是二叉树.
下面链接是二叉树和线索二叉树的建立过程
https://www.jianshu.com/p/d2d0acdbbe33
二叉树的建立过程和遍历过程都用的了递归的思想,递归是个好东西,复杂的问题就迎刃而解了
我个人觉得二叉树线索化有点多此一举,将空指针利用起来可以节省空间,但感觉有点不像树了.算了.
其实还有更加复杂的平衡二叉树和b树在后面的查找中会介绍到.

6.图

图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
图的几个定义要注意:1.顶点 2.边 3.权
用数组就比较好定义

struct Graph
{
char peak[MAXSIZE];//顶点,储存元素
int  weight[MAXSIZE][MAXSIZE];//边结构,储存权值
}

也可以用链式储存结构来定义,这样可以减少不必要的空间浪费.

//建立顶点结构
struct node
{
char data;
struct brim *brim
}
struct brim //边结构
{
int weight;//权值
struct brim *next;
}

图的遍历有两种方式
1.深度优先
2.广度优先
顾名思义,深度就是先遍历一个出度,再回来遍历其他出度,广度就是把所有的出度遍历完再遍历下一个节点.
它们都可以用的递归的方法.

#include 
#include 
typedef struct brim
{
    int sign;   // 入度的下标(箭头)
    int weight; //储存权值
    struct brim *next;
} brim;
typedef struct node //建立图的框架(顶点结构)
{
    char data;
    int judge;
    struct brim *brim //用双指针来储存边信息
} node[7];

typedef struct Graph //用一个节点来储存顶点
{
    node node;
} Graph;
struct brim *c(struct brim *p, int s, int w)
{
    p = (struct brim *)malloc(sizeof(struct brim));
    p->sign = s;
    p->weight = w;
    return p;
}
void cr(struct Graph *Graph)//构造函数
{
    Graph->node[0].data = 'a';
    Graph->node[1].data = 'b';
    Graph->node[2].data = 'c';
    Graph->node[3].data = 'd';
    Graph->node[4].data = 'e';
    Graph->node[5].data = 'f';
    Graph->node[6].data = 'g';
    Graph->node[0].brim = c(&(*(Graph->node[0].brim)), 1, 2);
    Graph->node[1].brim = c(&(*(Graph->node[1].brim)), 2, 2);
    Graph->node[2].brim = c(&(*(Graph->node[2].brim)), 3, 2);
    Graph->node[3].brim = c(&(*(Graph->node[3].brim)), 6, 2);
    Graph->node[4].brim = c(&(*(Graph->node[4].brim)), 0, 2);
    Graph->node[5].brim = c(&(*(Graph->node[5].brim)), 2, 2);
    Graph->node[6].brim = c(&(*(Graph->node[6].brim)), 4, 2);
    Graph->node[1].brim->next = c(&(*(Graph->node[1].brim->next)), 5, 2);
    Graph->node[3].brim->next = c(&(*(Graph->node[3].brim->next)), 4, 2);
}
void visit(Graph *Graph, struct brim *p)//遍历访问
{
    if (Graph->node[p->sign].data != 'a' && Graph->node[p->sign].judge < 1)
    {
        int i = p->sign;
        printf("%c  ", Graph->node[p->sign].data);
        Graph->node[p->sign].judge++;
        if (p->next != NULL)
        {
            visit(&*(Graph), &(*(p->next)));
        }
        visit(&*(Graph), &(*(Graph->node[p->sign].brim)));
    }
}
void BL(struct Graph *Graph)//遍历函数
{
    printf("%c  ", Graph->node[0].data);
    visit(&*(Graph), Graph->node[0].brim);
}

int main(int argc, char const *argv[])
{
    struct Graph *Graph = (struct Graph *)malloc(sizeof(struct Graph));
    cr(&(*Graph));
    BL(&(*Graph));
    printf("\n");
    return 0;
}

最小生成树
什么是最小生成树?
以路径最短的方式走完整个图的一个树;原理就是找到每个顶点的最小出度或最小入度
主要有2个算法

/* Prim算法生成最小生成树 */
  void MiniSpanTree_Prim(MGraph G)
  {
      int min, i, j, k;
      /* 保存相关顶点下标 */
      int adjvex[MAXVEX];                        
      /* 保存相关顶点间边的权值 */
      int lowcost[MAXVEX];                       
      /* 初始化第一个权值为0,即v0加入生成树 */
      /* lowcost的值为0,在这里就是此下标的顶点已经加入生成树 */
      lowcost[0] = 0;                            
      /* 初始化第一个顶点下标为0 */
      adjvex[0] = 0;                             
      /* 循环除下标为0外的全部顶点 */
      for (i = 1; i < G.numVertexes; i++)        
      {
         /* 将v0顶点与之有边的权值存入数组 */
         lowcost[i] = G.arc[0][i];              
         /* 初始化都为v0的下标 */
         adjvex[i] = 0;                         
     }
     for (i = 1; i < G.numVertexes; i++)
     {
         /* 初始化最小权值为∞, */
         /* 通常设置为不可能的大数字如32767、65535等 */
         min = INFINITY;                        
         j = 1; k = 0;
         /* 循环全部顶点 */
         while (j < G.numVertexes)              
         {
             /* 如果权值不为0且权值小于min */
             if (lowcost[j] != 0 && lowcost[j] < min)
             {                                  
                 /* 则让当前权值成为最小值 */
                 min = lowcost[j];              
                 /* 将当前最小值的下标存入k */
                 k = j;                         
             }
             j++;
         }
         /* 打印当前顶点边中权值最小边 */
         printf("(%d,%d)", adjvex[k], k);       
         /* 将当前顶点的权值设置为0,表示此顶点已经完成任务 */
         lowcost[k] = 0;                        
         /* 循环所有顶点 */
         for (j = 1; j < G.numVertexes; j++)    
         {
             /* 若下标为k顶点各边权值小于此前这些顶点未被加入生成树权值 */
             if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j])
             {                                  
                 /* 将较小权值存入lowcost */
                 lowcost[j] = G.arc[k][j];      
                 /* 将下标为k的顶点存入adjvex */
                 adjvex[j] = k;                 
             }
         }
     }
 }

还有一个我就不想写了;(还没搞明白)

最短路径:
最短路径算法也有2种1.迪杰斯特拉(Dijkstra)算法 2.弗洛伊德(Floyd)算法
我觉得要找到最短路径需要计算和比较不同的路径,不然会出问题.就像有一条路权为22直接到,有一条路是11还要走一个12加起来23,
当然选择22直接到的,.但是如果另一条是10+11=21呢?所以需要比较不同路径的权值.这样也会让算法变得麻烦.
我也不写算法实现了,有兴趣可以去查一下...

关键路径:
与最短路径相反,它是求最长的路径.
图可以说是最复杂的一种数据结构了,由于它复杂的关系网,决定它可以实现复杂的功能,同样也为功能的实现增加了难度.

7.查找

最开始的查找时间复杂度为O(n)就一个个的找嘛?

/* 有哨兵顺序查找 */
int Sequential_Search2(int *a, int n, int key)
{
    int i;
    /* 设置a[0]为关键字值,我们称之为“哨兵” */
    a[0] = key;    
    /* 循环从数组尾部开始 */
    i = n;         
    while (a[i] != key)
    {
        i--;
    }
    /* 返回0则说明查找失败 */
    return i;      
}

.但是都知道一寸光阴一寸金,时间是非常宝贵的,所以人们就想方设法的节省时间提高效率.
在有序表中我们可以采用二分法求值的方式进行折半查找.
基于这个思想,人们又对这个方法进行了改进和优化从而诞生了,插值查找法,斐波那契查找(黄金分割查找),

#include 
#include 

void order_surch(int a[10], int key) //顺序查找
{
    for (int i = 0; i < 10; i++)
    {
        if (a[i] == key)
        {
            printf("key的位置:%d\n", i);
            return;
        }
    }
    printf("not found.\n");
}
int high = 9, low = 0;//全局变量用来储存最高与最低值的下标
void half_surch(int a[10], int key)//折半查找
{
    int mid = (high + low) / 2;
    if (a[mid] == key)
    {
        printf("key的位置:%d\n",mid);
        return;
    }
    else if (a[mid] < key)
    {
        low=mid;
        half_surch( a, key);
    }
    else
    {
        high=mid;
        half_surch( a, key);
    }
}
 
void in_surch(int a[10], int key)//插值查找
{
    int mid = low+(high-low)*(key-a[low])/(a[high]-a[low]);
    if (a[mid] == key)
    {
        printf("key的位置:%d\n",mid);
        return;
    }
    else if (a[mid] < key)
    {
        low=mid;
        half_surch( a, key);
    }
    else
    {
        high=mid;
        half_surch( a, key);
    }
}


int main(int argc, char const *argv[])
{
    int a[10]={1,2,6,9,10,34,56,76,89,99};
    int key=56;
    printf("顺序查找:");
    order_surch(a,key);
    printf("折半查找:");
    half_surch(a,key);
    high = 9;low = 0;//还原high和low
    printf("插值查找:");
    in_surch(a,key);
    return 0;
}

事实证明有序比无序效率更高,但是需要处理的数据非常巨大,有上千亿个数据怎么办,即使用了这些方法,还是需要很长的时间.这时索引就诞生了.
什么是索引??
数据结构的最终目的是提高数据的处理速度,索引是为了加快查找速度而设计的一种数据结构。索引就是把一个关键字与它对应的记录相关联的过程,一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。索引技术是组织大型数据库以及磁盘文件的一种重要技术。

索引按照结构可以分为线性索引、树形索引和多级索引。我们这里就只介绍线性索引技术。所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。我们重点介绍三种线性索引:稠密索引、分块索引和倒排索引.

二叉排序树
啥??讲到排序了??非也.二叉排序树还属于查找范围,建立一种有序的二叉树,将小于自身的值放在左子树,大于自身的值放在右子树上,这样方便查找.二叉排序树是一个用于查找的数据结构.后来人们对二叉排序树进行改进,就有了平衡二叉树和B树
下面链接是平衡二叉树的实现
https://www.jianshu.com/p/e48b31df2faa
比较复杂,

散列表查找(哈希表)
将数据计算成一个值,可以通过值取到该数据.大概就是这么个原理;
它的效率是最高的,一直为O(1).
散列函数的构造方法
1.直接定址法
2.数字分析法
3.平方取中法
4.折叠法
5.除留余数法
6.随机数法
大话数据结构-程杰 (neat-reader.cn)
不记得就再看看.

处理散列冲突的方法
强烈推荐链地址法(好用)
1.开放定址法
2.再散列函数法
3.链地址法
4.公共溢出区法
下面的简易的map中有运用到散列表实现

#include 
#include "stdlib.h"
struct map
{
    //横起来的数组的最大容量
    int maxNum;
    //横起来节点,链表
    struct node **nodes;
} map;

//数组的节点
struct node
{
    //真实的 key
    char *key;
    //value,
    char *value;
    //下一个节点
    struct node *nextNode;
} node;
//hash 算法
unsigned int hash(int maxNum, char *s)
{
    unsigned int h = 0;

    for (; *s; s++)
    {
        h = *s + h;
    }
    return h % maxNum; //predefined hash size
}
//set(*map,"xiaoming","xioamig,13,nan,...")
void set(struct map *m, char *key, char *value)
{
    //1.拿到 hash 值
    unsigned int h = hash(m->maxNum, key);
    //初始化 node
    struct node newNode;
    newNode.key = key;
    newNode.value = value;
    newNode.nextNode = NULL;
    //2.检查是是否被使用
    if (m->nodes[h] == NULL)
    {
        //插入
        m->nodes[h] = &newNode;
        //直接退出函数
        return;
    }
    //如果被使用了开始串串
    struct node *oldNode = m->nodes[h];
    while (oldNode->nextNode != NULL)
    {
        oldNode = oldNode->nextNode;
    }
    oldNode->nextNode = &newNode;
}
char *get(struct map *m, char *key)
{
    //1.hash
    unsigned int h = hash(m->maxNum, key);
    //2.取值
    struct node *n1 = m->nodes[h];
    //3.判断字符串
     while (n1 != NULL && n1->key != key)
    {
        n1 = n1->nextNode;
    }
    //3.1 判空
    if (n1 == NULL)
    {
        return NULL;
    }
    else
    {
        return n1->value;
    }
}
//get(*map,"xiaoming") return "xioamig,13,nan,..."
int main(int argc, char const *argv[])
{
    struct map m;
    m.maxNum = 10;
    m.nodes = (struct node **)malloc(sizeof(struct node *)*m.maxNum);
    //set
    set(&m, "xm", "xm,13,nan");
    //get
    char *xm = get(&m, "xm");
    printf("%s\n", xm);
    return 0;
}

8.排序

排序也是数据结构?? 其实排序可以归结为方法一类.也就是算法
主要的排序方法有以下几类


image.png

我这里实现一下快速排序

//快速排序
#include 
#include
#include 
#define MAXSIZE 10000 /* 用于要排序数组个数最大值,可根据需要修改 */
typedef struct
{
    int r[MAXSIZE + 1]; /* 用于存储要排序数组,r[0]用作哨兵或临时变量 */
    int length;         /* 用于记录顺序表的长度 */
} SqList;
/* 交换L中数组r的下标为i和j的值 */
void swap(SqList *L, int i, int j)
{
    int temp = L->r[i];
    L->r[i] = L->r[j];
    L->r[j] = temp;
}
//quiksort快速排序
int partition(SqList *t, int l, int h) //将值调换(大放后,小放前)
{
    if (l == h)
    {
        return -1;
    }
    int e = t->r[(l + h) / 2]; //基点.以左作为基点
    while (t->r[l] != t->r[h])
    {
        while (t->r[h] > e)
        {
            h--;
        }
        while (t->r[l] < e)
        {
            l++;
        }
        if (h == l)
        {
            break;
        }
        swap(t, h, l);
        continue;
    }
    return l;
}
void Qsort(SqList *t, int l, int h)
{
    if (l != h && h > l)
    {
        int pvote;
        pvote = partition(t, l, h);
        if (pvote == -1)
        {
            return;
        }
        Qsort(t, l, pvote - 1);
        Qsort(t, pvote + 1, h);
    }
}

int main(int argc, char const *argv[])
{clock_t start,finish;
    double TheTimes;start=clock();
    SqList t;
    int a[10] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
    for (int i = 0; i < 10; i++)
    {
        t.r[i + 1] = a[i];
    }
    t.length = 10;
    Qsort(&t, 1, t.length);
    for (int i = 1; i < t.length + 1; i++)
    {
        printf("%d  ", t.r[i]);
    }
    printf("\n");
    finish=clock();
    TheTimes=(double)(finish-start)/CLOCKS_PER_SEC;
    printf("%f秒。\n",TheTimes);//计算排序所用的时间
    return 0;
}

没错,每次执行的时间不一定相同,它总是在一定区间波动
下面的链接有各种其他排序算法的实现
C 排序算法 | 菜鸟教程 (runoob.com)
我们同样可以采用空间换取时间的方式,直接建立一个平衡二叉树来进行储存数据,所有是数据就都是有序的状态了就不用排了.
好了数据结构就说到这里了.
感谢大家的支持!!!

你可能感兴趣的:(数据结构总结)