第8章 数据结构算法专题二

发布时间 2023-03-31 15:18:00作者: sha_ckle

线索二叉树与哈夫曼树

线索二叉树

  1. 线索二叉树的概念

    • 采用某种方法遍历二叉树的结果是一个结点的线性序列。
    • 修改空链域改为存放指向结点的前驱和后继结点的地址。
    • 这样的指向该线性序列中的”前驱“和”后继“的指针,称作线索(thread)。
    • 创建线索的过程称为线索化。
    • 线索化的二叉树称为线索二叉树。
    • 显然线索二叉树与采用的遍历方法相关,有先序线索二叉树、中序线索二叉树和后续线索二叉树。
    • 线索二叉树的目的是提高该遍历过程的效率。

    在结点的存储结构上增加了两个标志位来区分这两种情况:

    左标志ltag:0表示lchild指向左孩子结点/1表示lchild指向前驱结点,即线索

    右标志rtag:0表示rchild指向右孩子结点/1表示rchild指后继结点,即线索

    这样,每个结点的存储结构如下:

    ltag lchild data rchild rtag

    为了方便算法计算,在线索二叉树中再增加一个头结点。

    线索化二叉树中结点的类型定义如下:

    typedef struct node
    {
    	Elemtype data;//结点的数据域
    	int ltag, rtag;//增加的线索标志
    	struct node *lchild;//左孩子或线索指针
    	struct node *rchild;//右孩子或线索指针
    }TBTNode;   //线索树结点类型定义
    
  2. 线索化二叉树

    建立某种次序的线索二叉树过程:

    • 以该遍历方法遍历一颗二叉树。
    • 在遍历的过程中,检查当前访问结点的左、右指针域是否为空:
      1. 如果左指针域为空,将它改为指向前驱结点的线索;
      2. 如果右指针域为空,将它改为指向后继结点的线索。
  3. 建立中序线索二叉树的算法

    1. CreatThread(b)算法:对以二叉存储的二叉树b进行中序线索化,并返回线索化后头结点的指针root。
    2. Thread(p)算法:对以*p为根结点的二叉树子树的中序线索化。

    在中序遍历当中:

    • P总是指向当前线索化地结点。
    • pre作为全局变量,指向刚刚访问过的结点、
    • *pre是*p的中序前驱结点,*p是*pre的中序后继结点。

请添加图片描述

  1. 遍历线索化二叉树

    遍历某种次序的线索二叉树,就是从该次序下的开始结点出发,反复找到该结点在该次序下的后继结点,直到头结点。

    以中序线索二叉树为例,开始结点是根节点的最左下结点。

请添加图片描述

在中序线索二叉树中中序遍历的过程:

p指向根结点;

while p ≠ root时循环

{

​ 找开始结点*p;

​ 访问*p结点;

​ while(*p结点有右线索)

​ 一直访问下去;

​ p转向右孩子结点;

}

哈夫曼树

  1. 哈夫曼树的定义

    设二叉树具有n个带权值的叶结点,那么从根节点到各个叶结点的路径长度与相应结点权值的乘积和,叫做二叉树的带权路径长度。

请添加图片描述

  1. 构造哈夫曼树的原则:

    1. 权值越大的叶结点越靠近根结点
    2. 权值越小的叶节点越远离根结点

    构造哈夫曼树的过程:

    1. 给定的n个权值{W1,W2,...,Wn}构造n棵只有一个叶节点的二叉树,从而得到一个二叉树的集合F=
    2. 在F中选取根结点的权值最小和次小的两棵二叉树作为左、右子树构造一棵新的二叉树,这棵新的二叉树根据结点的权值为其作、右子树根节点权值之和
    3. 在集合F中删除作为左、右子树的两棵二叉树,并将新建立的二叉树加入到集合F中。
    4. 重复(2)、(3)两步,当F中只剩下一颗二叉树时,这棵二叉树便是所要建立的哈夫曼树。
  2. 哈夫曼编码

    ​ 规定哈夫曼树中的左分支为0,右分支为1(哈夫曼编码属0、1二进制编码),则从根节点到每个叶节点所经过的分支对应的0和1组成的序列变为该结点对应的字符的编码。这样的编码称为哈夫曼编码。

图及图的存储结构

图的基础知识

  1. 图的定义:图(Graph)G由顶点集合V(G)和边集合E(G)构成。

    说明:对于n个顶点的图,对每个顶点连续编号,即顶点的编号为0~n-1。通过编号唯一确定一个顶点。

    图抽象数据类型=逻辑结构+基本运算(运算描述)

    图的基本运算如下:

    • InitGraph(&g):图初始化、
    • ClaerGraph(&g):销毁图、
    • DFS(G,V):从顶点v出发深度优先遍历
    • BFS(G,V):从顶点v出发广度优先遍历

    如图G中,如果代表边的顶点对是无序的,则称G为无向图。用圆括号序偶表示无向边。

    如果表示边的顶点对是有序的,则称G为有向图。用尖括号序偶表示有向边。

  2. 图的基本术语

    【端点和邻接点】

    • 无向图:若存在一条边(i,j)——>顶点i和顶点j为端点,它们互为邻接点。
    • 有向图:若存在一条边<i,j>——>顶点i为起始端点(简称为起点),顶点j为终止端点(简称终点),它们互为邻接点。

    【顶点的度、入度和出度】

    • 无向图:以顶点i为端点的边数称为该顶点的度。
    • 有向图:以顶点i为终点的入边的数目,称为该顶点的入度。以顶点i为终点的出边的数目,称为该顶点的出度。一个顶点的入度和出度和和为该顶点的度。
  3. 完全图

    • 无向图:每两个顶点之间都存在着一条边,称为完全无向图,包含有n(n-1)/2条边
    • 有向图:每两个顶点之间都存在着方向相反的两条边,称为完全有向图,包含有n(n-2)条边。
  4. 稠密图、稀疏图

    当一个图接近完全图时,则称为稠密图

    相反,当一个图含有较少的边数(即当e<<n(n-1))时,则称为稀疏图。

  5. 子图

    设有两个图G=(V,E)和G`=(V`,E`),若V`是V的子集,则E`是E的子集,则称G`是G的子图。

  6. 回路或环

    若一条路径上的开始点与结束点为同一个顶点,则此路径被称为回路或环。开始点与结束点相同的简单路径被称为简单回路或简单环。

  7. 连通、连通图和连通分量

    无向图:若从顶点i到顶点j有路径,则称顶点i和j是连通的。

    若图中任意两个顶点都连通,则称为连通图,否则称为非连通图。

    无向图G中的极大连通子图称为G的连通分量。显然,任何一个连通图的连通分量只有一个,即本身,而非连通图有多个连通分量。

  8. 强连通图和强连通分量

    有向图:若从顶点i到顶点j有路径,则称从顶点j到j是连通的。

    若图G中的任意两个顶点i和j都连通,即从顶点i到j和从顶点j到i都存在路径,则称图G是强连通图。

  9. 权和网

    图中每一条边都可以附带有一个对应的数值,这种与边相关的数值称为权。权可以表示从一个顶点到另一个顶点的距离或花费的代价。

    边上带有权的图称为带权图,也称为网。

图的存储结构

图的逻辑结构—(映射)—>图的存储结构(存储每个顶点的信息、存储每条边的信息)

图的两种主要存储结构:邻接矩阵、邻接表。

  1. 邻接矩阵存储方法

    邻接矩阵是表示顶点之间相邻关系的矩阵。G=(V,E)是具有n(n>0)个顶点的图,顶点的编号依次为0~n-1。

    G是邻接矩阵A是n阶方针,其定义如下:

    1. 如果G是无向图,则:A[i][j]=1:若(i,j)∈E(G) 0:其他
    2. 如果G是有向图,则:A[i][j]=1:若<i,j>∈E(G) 0:其他

请添加图片描述

邻接矩阵的主要特点:

一个图是邻接矩阵表示是唯一的;特别适合于稠密图的存储。

图的邻接矩阵存储类型定义如下:

define MAXV<最大顶点个数>

typedef struct//声明顶点类型

{

​ int no;//顶点编号

​ Info Type info;//顶点其他信息

}VertexType;

typedef struct//声明的邻接矩阵类型

{

​ int edges[MAXV][MAXV];//邻接矩阵

​ int n, e;//顶点数、边数

​ VertexType vexs[MAXV];//存放顶点信息

}MatGraph;

  1. 邻接表存储方法

    对图中每个顶点i建立一个单链表,将顶点i的所有邻接表链起来。

请添加图片描述

每个单链表上添加一个表头结点(表示顶点信息)。并将所有头结点构成一个数组,下标为i的元素表示顶点为i的表头结点。

请添加图片描述

邻接表的特点如下:

邻接表表示不唯一;特别适合稀疏图的存储。

图的邻接表存储类型定义如下:

typedef struct ANode//声明边结点类型

{

​ int adjvex;//该边的终点编号

​ struct ANode *nextarc;//指向下一条边的指针

​ InfoType info;//该边的权值等信息

}ArcNode;

typedef struct Vnode//声明邻接表头结点类型

{

​ Vertex data;//顶点信息

​ ArcNode *firstarc;//指向第一条边

}VNode;

typedef struct//声明图邻接表类型

{

​ VNode adjlist[MAXV];//邻接表

​ int n, e;//图中顶点数n和边数e

}AdjGraph;

逆邻接表:就是在有向图的邻接表中,对每个顶点,链接的是指向该顶点的边。

深度优先搜索及广度优先搜索

基本思想:——仿照的先序遍历过程

请添加图片描述

【深度优先搜索的步骤】

简单归纳:

  • 访问起始点V;
  • 若V的第1个邻接点没访问过,深度遍历此邻接点;
  • 若当前邻接点已访问过,再找V的第2个邻接点重新遍历。

详细归纳:

  • 在访问图中某一个起始顶点V后,由V出发,访问它的任一邻接顶点w1;

  • 再从w1出发,访问与w1邻接但未被访问过的顶点w2;

  • 然后再从w2出发,进行类似的访问,...

  • 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点u为止。

  • 接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。

    如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;

    如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。

基本思想——仿树的层次遍历过程

请添加图片描述

【广度优先搜索的步骤】

简单归纳:

  • 在访问了起始点v之后,依次访问v的邻接点;
  • 然后再依次访问这些顶点中未被访问过的邻接点;
  • 直到所有顶点都被访问过为止。

广度优先搜索是一种分层的搜索过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有回退的情况

因此,广度优先搜索不是一个递归的过程,其算法也不是递归的。

【算法思想】

  1. 从图中某个顶点v出发,访问v,并置visited[v]的值为true,然后将v进队。
  2. 只要队列不空,则重复下述处理。
    1. 队头顶点u出队
    2. 依次检查u的所有邻接点w,如果visited[w]的值为false,则访问w,并置visited[w]的值为true,然后将w进队。

网络爬虫软件:1. 广度优先搜索算法 2. 深度优先搜索算法。

#include<iostream>

#include<stdio.h>
#include<stdlib.h>

#define ElemType char;

//初始化队头和队尾指针置为0
int front = 0, rear = 0;

typedef struct BitreeNode{
    ElemType data;//数据域
    struct BitreeNode *lchild, *rchild;//左右指针
}BitreeNode, *BiNode;

void CreateBinTreeFunc(BiNode *T)
{
    *T = (BitreeNode*)malloc(sizeof(BitreeNode));
    (*T)->data = 'A';
    
    (*T)->lchild = (BitreeNode*)malloc(sizeof(BitreeNode));
    (*T)->rchild = (BitreeNode*)malloc(sizeof(BitreeNode));
    (*T)->lchild->data = 'B';
    (*T)->rchild->data = 'C';
    
    (*T)->lchild->lchild = (BitreeNode*)malloc(sizeof(BitreeNode));
    (*T)->lchild->rchild = (BitreeNode*)malloc(sizeof(BitreeNode));
    (*T)->lchild->lchild->data = 'D';
    (*T)->lchild->lchild->lchild = NULL;
    (*T)->lchild->lchild->rchild = NULL;
    (*T)->lchild->rchild->data = 'E';
    (*T)->lchild->rchild->lchild = NULL;
    (*T)->lchild->rchild->rchild = NULL;
    
    (*T)->rchild->lchild = (BitreeNode*)malloc(sizeof(BitreeNode));
    (*T)->rchild->rchild = (BitreeNode*)malloc(sizeof(BitreeNode));
    (*T)->rchild->lchild->data = 'F';
    (*T)->rchild->lchild->lchild = NULL;
    (*T)->rchild->lchild->rchild = NULL;
    (*T)->rchild->rchild->data = 'G';
    (*T)->rchild->rchild->lchild = NULL;
    (*T)->rchild->rchild->rchild = NULL;
}


//入队操作
void EnterQueueFunc(BiNode *x, BiNode node)
{
    x[rear++] = node;
}
//出队操作
BitreeNode *DeleteQueueFunc(BitreeNode**x)
{
    return x[front++];
}
//输出数据元素
void printNodeData(BiNode node)
{
    printf("%c ",node->data);
}

int main()
{
    BiNode tree;
    
    //初始化树
    CreateBiTreeFunc(&tree);
    
    BitreeNode *p;
    BiNode x[50];
    
    EnterQueueFunc(x, tree);
    
    printf("广度优先搜索算法输出的结果");
    while(front < rear)
    {
        p = DeleteQueueFunc(x);
        PrintNodeData(p);
        
        if(p->lchild != NULL)
            EnterQueueFunc(x, p->lchild);
        if(p->rchild != NULL)
            EnterQueueFUnc(x, p->rchild);
    }
}

最小生成树及最短路径算法

最小生成树

一个连通图的生成树是一个极小连通图,它含有图中全部n个顶点和构成一棵树的(n-1)条边。

请添加图片描述

命题:如果在一棵生成树上添加一条边,必定构成一个环。

可以通过遍历方法产生生成树:

由深度优先算法遍历得到的生成树称为深度优先生成树。

请添加图片描述

由广度优先遍历得到的生成树称为广度优先生成树

请添加图片描述

【最小生成树的概念】

  • 对于带权连通图G(每条边上的权均大于零的实数),可能有多棵不同生成树
  • 每棵生成树的所有边的权值之和可能不同
  • 其中权值之和最小的生成树称为图的最小生成树。

【非连通图和生成树】

​ 连通图:仅需调用遍历过程(DFS或BFS)一次、从图中任一顶点出发,便可以遍历图中的各个顶点,产生相应的生成树。

​ 非连通图:需多次调用遍历过程。每个连通分量中的顶点集和遍历时走过的边一起构成一棵生成树。所有连通分量的生成树组成非连通图的生成森林。

重点:求带权连通图的最小生成树。

【普里姆算法】

普里姆(prim)算法是一种构造性算法,用于构造最小生成树。过程如下:

  1. 初始化U={v}。v到其他顶点的所有边为侯选边;
  2. 重复一下步骤n-1次,使得其他n-1个顶点被加入到U中:
    1. 从侯选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入到U中;
    2. 考察当前V-U中的所有顶点j,修改侯选边:若(j,k)的权值小于原来和顶点k关联的侯选边,则用(k,j)取代后者作为侯选边。

最短路径算法

用于计算一个结点到其他所有结点的最短路径。主要特点是以起始点为中心向外层扩展,直到扩展到终点为止。

dijkstra算法能得到最短路径的最优解,但由于它遍历计算的结点很多,所以效率很低。

  1. 在非网图中,最短路径是指两顶点之间经历的边数最少的路径。(非网图:没有权值)
  2. 在网图中,最短路径是指两顶点之间经历的边上权值之后最短的路径。

1.【单源点最短路径问题】

问题描述:给定带权有向图G=(V,E)和源点v∈V,求从v到G中其余各顶点的最短路径。

应用实例——计算机网络传输问题:怎样找到一种最经济的方式,从一台计算机向网上所有其它计算机发送一条消息。

2.【每一对顶点之间的最短路径】

问题描述:给定带权有向图G=(V,E),对任意顶点vi,vj∈V(i≠j),求顶点vi到顶点vj的最短路径。

解决方法1:每次以一个顶点为源点,调用Dijkstra算法n次。

3.【Dijkstra算法】

  1. 基本思想:设置一个集合S存放已经找到最短路径的顶点,S的初始状态只包含源点v,对vi∈V-S,假设从源点v到vi的有向边为最短路径。以后每求的一条最短路径v,...,vk,就将vk加入集合S中,并将路径v,...,vk,vi与原来的假设相比较,取路径长度较小者为最短路径。重复上述过程,直到集合V中全部顶点加入到集合S中。
  2. 设计数据结构:
    1. 图的存储结构:带权的邻接矩阵存储结构
    2. 数组dist[n]:每个分量dist[i]表示当前所找到的从初始点v到终点vi的最短路径的长度。初始态为:若从v到vi有弧,则dist[i]为弧上权值;否则置dist[i]为无穷。
    3. 数组path[n]:path[i]是一个字符串,表示当前所找到的从初始点v到终点vi的最短路径。初始态为:若从v到vi有弧,则path[i]为wi;否则置path[i]空串。
    4. 数组s[n]:存放源点和已经生成的终点,其初态为只有一个源点v。
#include<iostream>
#include<stdio.h>

#define MAX 50
#define MAXCOST 0xFFFFFFF

int graph[MAX][MAX];

void PrimFunc(int graph[][MAX], int n)
{
    int lowcost[MAX];
    int mst[MAX];
    int min, minid, sum = 0;
    
    for(i = 2; i <= n; i++)
    {
    	lowcost[i] = graph[1][i];//lowcast存放顶点1可以到达点的路径长度
        mst[i] = 1;//初始化以1位起始点
    }
    
    mst[i] = 0;
    for(int i = 2; i <= n; i++)
    {
        min = MAXCOST;
        minid = 0;
        for(int j = 2; j <= n; j++)
        {
            if(lowcost[j] < min && lowcost[j] != 0)
            {
                min = lowcost[j];//找出权值最短的路径长度
                minid = j;//找出最小的ID     
            }
        }
        printf("V&d-->V%d = %d\n", mst[minid], minid, min);
        sum += min;//求和
        lowcost[minid] = 0;//最短路径置为0
        
        for(j = 2; j <= n; j++)
        {
            if(grap[minid][j] < lowcost[j])//对这一点直达顶点进行路径更新
            {
                lowcost[j] = graph[minid][j];
                mst[j] = minid;
            }
        }
    }
    printf("最小权值之和为:%d\n", sum);
}

int main()
{
    int i, j, k, m, n;
    int x, y, cost;
    
    printf("请输入顶点和边数\n");
    scanf("%d%d",&m, &n);//m_顶点,n_边数
    
    for(i = 1; i <= n; i++)
    {
        for(j = 1; j <= n; j++)
        {
            graph[i][j] = MAXCOST;
        }
    }
    
    printf("请输入点到点之间名称及权值:\n");
    for(k = 1; k <= n; k++)
    {
        scanf("%d%d%d", &i, &j ,&cost);
        graph[i][j] = cost;
        graph[j][i] = cost;
    }
    
    PrimFunc(graph,m);
    
    return 0;
}

请添加图片描述

折半查找算法

折半查找

二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

【查找过程】

首先,假设表中的元素时按升序排雷,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

【算法要求】

  1. 必须采用顺序存储结构。
  2. 必须按关键字大小有序排列。
#include<iostream>

//定义折半查找算法
void BinSearchFunc(int a[], int key)
{
    int low, high, mid, flag = 0;
    
    low = 0;
    high = 9;
    
    while(low <= high)
    {
        mid = (low + high) / 2;
        printf("low = %d, mid = %d, high = %d \n",low, mid, high);
        
        if(a[mid] > key)
        {
            high = mid -1;
        }
        if(a[mid] < key)
        {
            low = mid + 1;
        }
        if(a[mid] = key)
        {
            flag = 1;
            break;
        }
    }
    
    if(flag)
    {
        printf("\n已经查找到该数字信息.\n\n");
    }
    else
    {
        printf("\n未查找到该数字信息.\n\n");
    }
}

int main()
{
    int a[10], num;
    printf("\n请初始化数字a[10]\n");
    for(int i = 0; i <= 9; i++)
    {
        scanf("%d", &a[i]);
    }
    printf("\n请输入要查找的数据:");
    scanf("%d", &num);
    
    BinSearchFunc(a, num);
    
    return 0;
}

散列表(Hash)算法

散列表(Hash)

​ 散列表(hash table,也叫哈希表),是根据关键码值(key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来记录访问记录,以加快查找的速度。这个映射函数也叫做叫做散列函数,存放记录的数组叫做散列表。

​ 给定表M,存在函数(key),对任意给定的关键值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash)函数。

​ 散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位。

实际工作中需要不同的情况采用不同的哈希函数,通常考虑的因素有:

  • 计算哈希函数所需时间
  • 关键字的长度
  • 哈希表的大小
  • 关键字的分布情况
  • 记录的查找频率
  1. 直接寻址法:取关键字或关键字的某个线性函数值为散列地址,即H(key)=key或H(key)=a.key + b,其中a和b为常数(这种散列函数叫做自身函数),若其中H(key)中已经有值了,就往下一个找,直到H(key)中没有值了,就放进去。
  2. 数字分析法:分析一组数据,比如一组员工的出生年月日,这是我们发现出生年月日的前几位数字大体相同,这样的话,出现冲突的几率就会很大,但是我们发现年月日的后几位表示月份和具体日期的数组差别很大,如果用后面的数字来构造散列地址,则冲突的几率会明显降低。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
  3. 平方取中法:当无法确定关键字中哪几位分布比较均匀时,可以先求出关键字的平方值,然后按需要取平均值的中间几位作为哈希地址。这是因为:平方后中间几位和关键字中每一位都相关,故不同关键字会以比较高的概率产生不同的哈希地址。
  4. 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加(去除进位)作为散列地址。数位叠可以有移位叠加和间界叠加两种方法。移位叠加是将分割后的每一部分的最低位对齐,然后相加;间界叠加是从一端向另一端沿分割界来回折叠,然后对齐相加。
  5. 随机数法:选择一随机数,取关键字的随机值作为散列地址,即H(key)=random(key)其中random为随机函数,通常用于关键字长度不等的场合。
  6. 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
#include<stdio.h>
#indlude<stdlib.h>

#define MAXSIZE 1000 //存储空间初始化分配量
#define TRUE 1
#define FALSE 0
#define HASHSIZE 12//定义哈希表长为数组的长度
#define NULLKEY -32768

//定义哈希表
typedef struct {
	int *elem;//数据元素存储基址,动态分配数组
    int count;//当前数据元素个数
}HashTable;

typedef int Status;//Status是函数的类型,其值是函数结果状态代码,TRUE,FALSE
int m = 0;//哈希表表长 全局变量

Status InitHashTable(HashTable *hash);//初始化哈希表
int HashFunc(int key);//构造哈希函数
void InsertHash(HashTable *hash, int key);//插入关键字进哈希表
Status FindHashFunc(HashTable hash, int key, int *addr);//哈希查找关键字

Status InitHahtable(HashTable *hash)
{
    int i;
    m = HASHSIZE;
    hash->count = m;
    hash->elem = (int *)malloc(m * sizeof(int));
    for(i = 0; i < m; i++)
    {
        hash->elem[i] = NULLKEY;
    }
    return TRUE;
}

int HashFunc(int key)
{
    return key % m;//构造方法为除留余数法
}

void InsertHash(HashTable *hash, int key)
{
    int addr = HashFunc(key);//求哈希地址
    while(hash->elem[addr] != NULLKEY)//如果不为空,则冲突
    {
        addr = (addr + 1) % m;//开放定制法线性探测
    }
    hash->elem[addr] = key;//直到有空位之后插入关键字
}
    
Status FindHashFunc(HashTable hash, int key, int *addr)
{
    *addr = HashFunc(key);//求哈希地址
    while(hash.elem[*addr] != key) //否则,使用开放定址法继续查找
    {
        *addr = (*addr + 1) % m;//开发定址法的线性探测
        if(hash.elem[*addr] == NULLKEY || *addr = HashFunc(key))
        {
            return FALSE;
        }
    }
    return TRUE;
}

int main()
{
    int a[HASHSIZE] = {12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 35};//插入的关键字
    int key = 37; //关键字
    int addr;//哈希地址
    HashTable hash;
    
    //初始化哈希表
    InitHashTable(&hash);
    
    //插入关键字到哈希表
    for(int i = 0; i < m; i++)
        InsertHash(&hash, a[i]);
    
    //查找key为37的关键字
    int rs = FindHashFunc(hash, key, &addr);
    if(rs)
        printf("\n查找%d的哈希地址为:%d\n\n",key , addr);
    else
        printf("\n查找%d失败,\n\n",key);
    
    //遍历查找关键字(都会成功)
    for(int i = 0; i < m; i++)
    {
        key = a[i];
        FindHashFunc(hash, key, &addr);
        printf("\n查找%d的哈希地址为:%d\n\n",key , addr);
    }
    printf("\n");
    
    return 0;
}

插入排序与快速排序

插入排序

​ 插入排序,一般也称为直接插入排序。对于少量元素的排序,它是一个有效的算法。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程中使用双层循环,外层循环除了第一个元素之外的所有元素,内层循环对当前元素前面的有序表进行待插入位置查找,并进行移动。

​ 应用分析:插入排序适用于已经有部分数据已经排好,并且排好的部分越大越好。一般在输入规模大于1000的场合下不建议使用插入排序。

#include<stdio.h>

void InsertSort(int a[], int n);
void SwapFunc(int *x, int *y);

void InsertSort(int a[], int n)
{
    for(int i = 1; i < n; i++)
    {
        for(int j = i; j > 0 && a[j] < a[j - 1]; j--)
        {
            SwapFunc(&a[j], &a[j -1]);
        }
    }
}

void SwapFunc(int *x, int *y)
{
    int t = *x;
    *x = *y;
    *y = t;
}

int main()
{
    int a[7] = {12, 23, 65, 81, 22, 28, 98};
    
    InsertSort(a, 7);
    for(int i = 0; i < 7; i++)
        printf("%d ", a[i]);
    printf("\n");
    
    return 0;
}

快速排序

​ 快速排序(Quicksort)是对冒泡排序的一种改进

​ 它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对着两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

​ 快速排序算法通过多次比较和交换来实现排序,其排序流程如下:

  1. 首先设定一个分界值,通过该分界值将数组分成左右两部分。
  2. 将大于或等于分界值的数据集中到数组右边,小于分界值的数据集中到数组的左边。此时,左边部分各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值。
  3. 然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以作类似处理。
  4. 重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了。
#include<stdio.h>

void KSSortFunc(int a[], int low, int high)
{
    if(high < low)
        return;
    int i = low;
    int j = high + 1;
    int key = a[low];
    
    while(true)
    {
        //从左向右比Key大的值
        while(a[++i] < key)
        {
            if(i == high)
                break;
        }
        //从右向左比Key小的值
        while(a[--j] > key)
        {
            if(j == low)
                break;
        }
        if(i >= j)
            break;
        
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
    //中界值与j对应值交换
    int temp = a[low];
    a[low] = a[j];
    a[j] = temp;
    
    KSSortFunc(a, low, j-1);
    KSSortFunc(a, j + 1, high);    
}

int main()
{
    int a[] = {57, 68, 59, 32, 73, 28, 97, 33, 25};
    
    KSSortFunc(a, 0, sizeof(a)/sizeof(a[o]) -1);//三个参数要赋值
    for(int i = 0; i < sizeof(a)/sizeof(a[0]); i++)
        printf("&d-->",a[i]);
    
    return 0;   
}

希尔排序与选择排序

希尔排序

​ 希尔排序(Shell`s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。

​ 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键字越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

希尔排序是基于插入排序的以下两点性质而提出改进方法的:

插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。

但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。

算法思想:

​ 希尔排序是特殊的插入排序,直接插入排序每次插入前的遍历步长为1,而希尔排序的将待排序列分为若干个子序列,对这些子序列分别进行直接插入排序,当每个子序列长度为1时,再进行一次直接插入排序时,结果一定是有序的。常见的划分子序列的方法有:初始步长(两个子序列相应元素相差的距离)为要排的数的一半,之后每执行一次步长折半。

#include<stdio.h>
#include<malloc.h>

void ShellSortFunc(int *p, int len);//函数声明

int _tmain(int argc, _TCHAR* argv[])
{
    int i. len, *p;
    printf("请输入要排序的数字个数:");
    scanf_s("%d", &len);
    p = (int*)malloc(len*sizeof(int));//动态定义数组
    printf("请输入要排的数字:\n");
    for(i = 0; i < len; i++)
        scanf_s("%d", &p[i]);
    
    ShellSortFunc(p ,len);
    printf("希尔升序排序之后输出的结果为:\n");
    for(i = 0; i < len; i++)
        printf("%d\t",p[i]);
    return 0;
}

void ShellSortFunc(int *p, int len)
{
    int temp, gap, i, j, k;//gap为步长
    for(gap = len / 2; gap > 0; gap /= 2)//步长初始化为数组长度的一半,每次遍历后步长减半
    {
        for(i = 0; i < gap; i++)//变量i为每次分组的下一个元素下标
        {
            for(j = i + gap; j < len; j += gap)//对步长为gap的元素进行直接排序,当gap为1时,就是直接排序
            {
                temp = p[j];//备份p[i]的值
            	k = j - gap;//j初始化为i的前一个元素(与i相差gap长度
            	while(k >= 0 && p[k] > temp)
            	{
                	p[k + gap] = p[i];//将在p[i]前且比temp的值大的元素向后移动一位
                	k -= gap;
            	}
            	p[k + gap] = temp;
            }
        }
    }
}

选择排序

​ 选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。

【算法思想】

​ 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

#include"stdafx.h"
#define ARRSIZE(arr) (sizeof(arr)/sizeof(arr[0]))
#define SWAP(X,Y) X=X+Y;Y=X-Y;X=X-Y;

void SelectSortFunc(int a[], int len);
void PrintFunc(int a[], int len);

int _tmain(int argc, _THCHAR* argv[])
{
    int a[] = {23, 4, 7, 67, 45, 3, 453, 47, 67, 4, 53, 43, 43};
    int len = ARRSIZE(a);
    printf("\n排序之前数据输出如下:\n");
    PrintFunc(a, len);
    
    SelectSortFunc(a, len);
    printf("\n排序之后数据输出如下:\n");
    PrintFunc(a, len);
    
    return 0;
}

void SelectSortFunc(int a[], int len)
{
    int min;
    for(int i = 0; i < len -1; i++)
    {
        min = i;
        for(int j = i + 1; j < len; j++)
        {
            if(a[j] < a[min])
            {
                min = j;
            }
        }
        if(min != i)
        {
            swap(a[i], a[min]);
        }    
    }
}

void PrintFunc(int a[], int len)
{
    for(int i = 0; i < len; i++)
    {
        printf("%5d", a[i]);
    }
    printf("\n");
}

冒泡排序与并归排序

冒泡排序

冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法。

​ 它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从Z到A)错误就把它们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说元素列已经排序完成。

​ 这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端(升序或降序排列),就如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名”冒泡排序“。

【算法原理】

冒泡排序算法的原理如下:

​ 比较相邻的元素。如果第一个比第二个大,就交换他们两个。

​ 对每一对相邻元素做相同的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。

​ 针对所有的元素重复以上的步骤,除了最后一个。

​ 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

#include"stdafx.h"

#include<stdio.h>

#define ARR_LEN 200 //数组长度上限
#define elemType int //元素类型

void BubbleSortFunc(elemType a[], int len)
{
    ElemType temp;
    for(int i = 0; i < len - 1; i++)//外循环是排序趟数,len个数进行len-1趟
    {
        for(int j = 0; j < len -i - 1; j++)//内循环是每趟比较的次数
        {
            if(a[j] > a[j + 1])//判断相信两个元素大小,决定是否交换它们的值
            {
                temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
            }
        }
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    elemType a[ARR_LEN] = {43, 56, 88, 98, 67, 73, 26, 76, 44, 90};
    
    printf("\n未排序之前输出的结果为:\n");
    for(int i = 0; i < 10; i++)
        printf("%d\t", a[i]);
    printf("\n");
    
    BubbleSortFunc(a, 10);
    
    printf("\n排序之后输出的结果为:\n");
    for(int i = 0; i < 10; i++)
        printf("%d\t", a[i]);
    
    return 0;
}

归并排序

​ 归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

【算法思想】

归并操作的工作原理如下:

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列。
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置。
  3. 比较两个指针所指的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置。
  4. 重复步骤3直到某一指针超出序列尾。
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。
#include "stdafx.h"

#include<stdio.h>
#define N 10

//递归方式实现归并排序
//实现归并,把结果存放到List1
void MerFunc(int *list1, int list1_size, int *list2, int list2_size)
{
    int i, j, k, m;
    int temp[N];
    
    i = j = k = 0;
    
    while(i < list1_size && j < list2_size)
    {
        if(list1[i] < list2[j])
        {
            temp[k] = list1[i];
            k++;
            i++;
        }
        else
        {
            temp[k++] = list2[j++]
        }
    }
    
    while(i < list1_size)
    {
        temp[k++] = list1[i++];
    }
    while(j < list2_size)
    {
        temp[k++] = list2[j++];
    }
    
    for(m = 0; m < (list1_size + list2_size); m++)
    {
        list1[m] = temp[m];
    }
}

void Msort(int k[], int n)
{
    if(n > 1)
    {
        //list1是左半部分,list2是右半部分
        int *list1 = k;
        int list1_size = n / 2;
        int *list2 = k + list1_size;
        int list2_size = n - list1_size;
        
        Msort(list1, list1_size);
        Msort(list2, list2_size);
        
        MerFunc(list1, list1_size, list2, list2_size);
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    int a[10] = {34,54,67,87,12,43,56,90,56,76};
    
    printf("\n未排序之前输出的结果为:\n");
    for(int i = 0; i < 10; i++)
        printf("%d\t", a[i]);
    printf("\n");
    
    Msort(a, 10);//调用归并算法
    
    printf("\n排序之后输出的结果为:\n");
    for(int i = 0; i < 10; i++)
        printf("%d\t", a[i]);
    
    return 0;
}

堆排序算法

堆排序

​ 堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。

【堆操作】

​ 在堆的数据结构中,堆中的最大值总是位于根结点(在优先队列中使用堆的话堆中的最小值位于根结点)。堆中定义了以下几种操作:

  • 最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
  • 创建最大堆(Build Max Heap):将堆中的所有数据重新排序
  • 堆排序(HeapSort):移位在第一个数据的根结点,并做最大堆调整的递归运算。
#include"stdfax.h"

#include<stdio.h>

void SwapFunc(int *x, int *y)
{
    int temp = *y;
    *x = *y;
    *x = temp;
}

void Max_heap(int a[], int start, int end)
{
    //建立父节点指标和子节点指标
    int dad = start;
    int son = dad * 2 + 1;
    
    //若子节点指标在范围内才做比较
    while(son <= end)
    {
        fi(son + 1 <= end && a[son] < a[son + 1])//先比较两个子节点大小,选择最大的
            son++;
        if(a[dad] > a[son])//如果父节点大于子节点代表调整完毕,直接跳出函数
            return;
        else//否则交换父子内容再继续子节点和孙结点比较
        {
            SwapFunc(&a[dad], &a[son]);
            dad = son;
            son = dad * 2 + 1;
        }
    }
}

void Head_Sort(int a[], int len)
{
    //初始化i从最后一个父节点开始调整
    for(int i = len / 2 - 1; i >= 0; i--)
    {
        Max_heap(a, i, len - 1);
    }
    //先将第一个元素和已排好元素前一位做交换,再重新调整,直到排序完毕
    for(int i = len - 1; i > 0; i--)
    {
        SwapFunc(&a[0], &a[i]);
        Max_heap(a, 0, i - 1);
    }
}

int _tmain(int argc, _THCHAR* argv[])
{
    int a[] = {-90, 23, 54, 65, 78, 96, 23, 45, 68, 13};
    int len = (int)sizeof(a) / sizeof(*a);
    
     printf("\n未排序之前输出的结果为:\n");
    for(int i = 0; i < len; i++)
        printf("%d\t", a[i]);
    printf("\n");
    
    Head_Sort(a, len);//调用归并算法
    
    printf("\n排序之后输出的结果为:\n");
    for(int i = 0; i < len; i++)
        printf("%d\t", a[i]);
    
    return 0;
}