数据结构

数据结构

线性表 SqList

广义表GList

定义

广义表是线性表的推广,也可以被称作列表(List)。最为常见的广义表的应用就是Lisp语言,其源程序本身就是一系列广义表。

广义表一般记作:\(LS=(a_1,a_2,…,a_n)\)

其中,LS为广义表的名称,n为其长度。与线性表不同的地方在于,\(a_i\)可以是单个元素也可以是广义表,它们分别成为广义表LS的原子子表。习惯上,大写字母用来表示广义表的名称,小写字母用来表示原子。

当广义表LS非空时,第一个元素\(a_1\)为LS的表头(Head),其余元素组成的表(a2,a3,…,an)是LS的表尾(Tail)。

广义表长度:所包含的元素(包括原子和子表)的个数

广义表的深度:括号的最大层数

  1. A=() , A是一个空表,它的长度为0。

  2. B=(e),B是一个只包含一个原子e的表,它的长度为1。

  3. C=(a,(b,c,d)),C的长度为2,元素分别为原子a和子表(b,c,d)。

  4. D=(A,B,C),D的长度是3,元素分别为A,B,C一共3个子表。

获取表头和表尾

head tail

注意tail操作得到的结果先写出一对括号,元素括号里

GetHead(D)=A
GetTail(D)=(B,C)

存储结构

头尾链表存储表示

由于数据元素既可能是原子又可能是广义表,所以需要两种结构的结点(可以使用联合类型实现):

  • 表结点,用于表示列表
  • 原子结点,用于表示原子。

由于上述定义,只要列表非空则可以确定为表头和表尾。

所以,一个表结点可以由三个域组成:标志域指示表头的指针域指示表尾的指针域

然而,一个原子结点只需要两个域组成:标志域值域

img

// 广义表的头尾链表存储表示
typedef enum {ATOM, LIST} ElemTag;	// ATOM 原子	LIST 子表
typedef struct GLNode{
    ElemTag tag;					// 标志域
    union{
        AtomType	atom;			// 值域
        struct{	struct GLNode * hp, *tp;}ptr;	// 表头指针域 表尾指针域
    }
}* GList;

基于头尾链表存储广义表示意图

  1. 空表的表头指针指向空。
  2. 非空列表的表头指针指向具体的表结点。(而不是说表头指针自身就是表结点)
  3. 可以直接的看出原子和子表所在的层次。
  4. 最高层表结点的个数就是列表的长度
扩展线性链表存储

这种表示方式和传统的线性表(链表)很相似,至少每种结点都存在一个Next指针,表结点还存在表头指针

img

// 广义表的扩展线性链表存储表示
typedef enum {ATOM, LIST} ElemTag;	// ATOM 原子	LIST 子表
typedef struct GLNode{
    ElemTag tag;					// 标志域
    union{
        AtomType	atom;			// 值域
        struct GLNode *hp;			// 表结点的表头指针
    };
    struct GLNode *tp;				// 相当于线性链表的next,指向下一个元素结点
}* GList;

img

数组

Stack

n个元素入栈的出栈序列

卡特兰数 :\(\frac{1}{n+1}C_{2n}^n\)

顺序栈SqStack

链栈LiStack

顺序队列SqQueue

front指向队首元素

循环队列

用模运算%将存储空间在逻辑上变为“环状”

  • 方案一

rear指向队尾元素后一个位置(牺牲一个存储单元)

image-20200928214925795

  • 方案二

rear指向队尾元素

image-20200928215737797

链式队列LinkQueue

String

存储结构

顺序存储SString

静态数组实现(定长顺序存储)
#define MAXLEN 255
typedef struct{
    char ch[MAXLEN];
    int length;
}SString;
动态数组实现(堆分配存储)
typedef struct{
    char *ch;
    int length;
}HString;
HString S;
S.ch = (char *)malloc(MAXLEN *sizeof(char));
S.length = 0;

链式存储

typedef struct StringNode{
	char ch[4];
	struct StringNode * next;
}StringNode, * String;

模式匹配算法

朴素模式匹配算法

int Index(SString S, SString T){
    int k = 1;
    int i = k, j = 1;
    while (i <= S.length && j <= T.length){
        if (S.ch[i] == T.ch[j]){
            ++i;
            ++j;
        }else{
            k++;
            i = k;
            j = 1;
        }
    }
    if (j > T.length)
        return k;
    else 
        return 0;
}
  • 最坏情况:部分匹配,经常回溯,O(mn)

KMP算法

朴素模式匹配算法的优化

  • 主串指针不回溯
  • 模式匹配指针回溯

二叉树BiTree

概念

image-20200830155709114

image-20201105100756147

性质

  • 结点数 = 总度数 + 1

    每个结点一个前驱,除根结点 就是n-1边

    孩子为1的结点 n1

    孩子为2的结点边为2n2

    n-1=n1+2n2

    总结点n=孩子为0的结点+孩子为1的结点+孩子为2的结点

    根结点没有前驱

    每个结点都有且只有一个前驱

    总边数为n-1

    完全二叉树 FBT

    对于完全二叉树,可以由的结点数 n 推出为0、1和2的结点个数为n0、n1 和n2(突破点:完全二叉树最多只会有一个度为1的结点)

    某完全二叉树的第六层有24个叶结点,则该完全二叉树的结点总数最大为____
    A. 78
    B. 79
    C. 80
    D.81

    最多:第六层用了还剩24,\(2^6\) = 32,

    则用了32-24=8,8发展下一层的叶子节点\(n_0\)有2x8=16个

    总叶子节点个数\(n_0\) = 16 + 24 = 40个

    根据\(n_0 = n_2 +1\)

    **可以得到完全二叉结点总数最大:80个,2 $n_0 $ **

遍历

  • 使用前序和后序不能唯一确定一棵树

  • 两个节点,在前序和后序中的前后关系不太,证明二者是父子关系;如果相同,二者是兄弟关系

    根🌱 左右🍂🍃

    左右🍂🍃 根🌱

  • 前序序列和中序序列的关系相当于以 前序序列 为入栈次序,以 中序序列 为出栈次序

  • 因此前序序列和中序序列可以唯一地确定一棵二叉树

    故前序序列为a,b c,d不同的二叉树的个数就可以转化为“以序列a,b,c,d为入栈次序,则出栈次序的个数为多少”

    对于n个不同元素进栈,出栈序列的个数为\(\frac{1}{n+1}C^{n}_{2n}\)

线索二叉树ThreadTree

BST二叉排序树BSTNode

又叫二叉搜索树或二叉查找树

时间复杂度:

查找长度——在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度

二叉查找树时间复杂度即树的深度

二分\(logn\) ( \(2^x = n (树的高度)\) )

排序: for ( ) 循环解决 O(n) (如:遍历从1开始遍历到100)

image-20200920141912376

AVL 平衡二叉树

  • 二叉排序树特性:左子树结点值 < 根结点值 < 右子树结点值
  • 左子树和右子树高度之差不超过1
  • 结点的 平衡因子 = 左子树高度 - 右子树高度

时间复杂度:ASL = \(log_2n\)

哈夫曼树/最优二叉树

  • 结点的: 数值

  • 带权路径长度WPL

  • 叶子结点

哈夫曼树的构造

  • 在叶子结点中选择最小的,合成一个新结点
  • n个权值/叶子结点/不同的码字/字符
  • 结点总数:2n -1
  • 不存在度为1的结点
  • 树不唯一,但WPL相同
  • 前缀编码 :若没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码

双亲表示法PTree

孩子兄弟表示法CSTree

Graph

图的概念

  • V vertex 顶点集
  • E edge 边集
  • G graph 图 = (V, E)
  • 连通分量= 极大连通子图
  • 生成树 = 极小连通子图

image-20201112091504943

image-20200907091600211

image-20200923115015872

存储结构

邻接矩阵MGraph

邻接表ALGraph

无向图
image-20200903165832614
有向图
image-20200903171450535

image-20200904171438174

图的遍历

BFS: 使用队列

Breadth First Search

image-20200923104250476

DFS: 使用栈

Depth First Search

  • DFS一次可以遍历一个完整的连通图

image-20200923104135537

归纳

Search 复杂度 内容
BFS、DFS 空间复杂度都是O(v) 顶点的个数!!
对于邻接矩阵 时间复杂度都是O(\(v^2\)) 顶点的平方!!!
对于邻接表 时间复杂度都是O(v+e) 顶点+边集!!!
  • BFS、DFS算法复杂度与算法没多大关系,取决于存储结构
  • 邻接矩阵是点数平方
  • 邻接表是点加边
  • BFS:类似于二叉树层次遍历
  • DFS:类似于二叉树先序遍历

图的应用

生成树:n个顶点,n-1条边

最小生成树:无向图/网


Prim算法: 顶点🍪

稠密图 时间复杂度\(O(|V|^2)\)

从可选择的顶点域中选择最小权值的顶点相连

  • U集:已选的顶点,逐步增加最小权值的顶点
  • W集:待选的顶点,同步删除选过的顶点
  • 每次选择对象是W集中 与U集上的顶点之间存在路径且 权值最小的顶点

Kruskal算法:边🥖

稀疏图 时间复杂度 \(O( |E|log_2|E|)\)

  • 不断连边
  • 边最小就连谁
  • 不选会导致生成环的边

最短路径:有向网


Dijkstra算法:单源最短路径🍭

给定一个出发点(单源点)和一个有向网G(V,E)

算法思想:

  • 初始化:标出所有到单源点路径权值
  • 连上权最小的一个边
  • 观察连上边后其他没有连的点到单源点的路径权值(源点到该点所经过的路径)变小的更新,路径更新
  • 每增加一个顶点,新顶点的路径就可能被修正
  • 没有路是无穷大

image-20201009114743695


Floyd算法:所有顶点对最短路径🍡

有向无环图

DAG图

  • Directed Acyclic Graph

拓扑排序


AOV网

  • Actire On Vertices

  • 顶点表示活动的有向图网络

  • 不能存在环——死循环

拓扑排序——判断是否存在有向环

  • 拓扑排序不唯一

关键路径


AOE网

  • Activity On Edge Network
  • 带权有向图
  • 顶点表示事件或工程进展状态
  • 弧表示活动
  • 权值表示完成活动所需时间
  • 唯一入度为0的点 : 源点——开始

  • 唯一出度为0的点 : 汇点——结束

  • 关键路径:从源点到汇点最路径

  • 没有回路

image-20201009155528065

排序

  • 大根堆

    根 大于 左右

    i 左孩子 2i

    i 右孩子 2i+1

    i 的父节点[i/2]

  • 小根堆

    左右小于根

image-20200924144752699

image-20200924152506159

归并排序

image-20200924160411346

算法设计

阴间题

A 1 2 3 4 5 6 7 8
A 3 8 9 1 7 4 2 6
    
  while(i = 0 < [8+1/2] =4)
  {
      x = L[1] = 3;
      i = 1; j = 8;
      while ( 1 < 8)
      {
          while (1 < 8 && L[8] = 6 >= 3)
              j--;
          L[i] = L[j];   //3 < 8
          
          while (i = 1 < )
  }

程序设计基础

转义字符
Character Entity Name Entity Number(十进制)
&nbsp;
! &excl; !
" &quot; "
# &num; #
$ &dollar; $
% &percnt; %
& &amp; &
' &apos; '
( &lpar; (
) &rpar; )
* &ast; *
+ &plus; +
, &comma; ,
- &hyphen; -
. &period; .
/ &sol; /
: &colon; :
; &semi; ;
< &lt; <
= &equals; =
> &gt; >
? &quest; ?
@ &commat; @
[ &lsqb; [
\ &bsol; \
  • image-20201130170142882

逆序

逆序线性表

算法思想

第一个元素与最后一个元素对调,第二个元素与倒数第二个元素对调……以此类推。

void Reverse(int A[], int length)
{
    int i, temp;
    for ( i = 0; i < length / 2; i++)
    {
        temp = A[i];
        A[i] = A[n-i-1];
        A[n-i-1] = temp;
    }
}

逆置链表

直接逆置法

算法思想

将原链表结点的next指针逐个修改为指向新链表,即备份新链表,获取原链表最前面的一个结点,同时原链表指向第一个结点的指针向后移,再将新链表链接在获取到的结点后面,逐个操作。这样,处于前面的结点就接在的后方结点的后面,完成逆置。

void Reverse(LinkList &list)
{
    LinkList head, temp, reverse;
    head = list; //用来操作原链表的指针
    reverse = NULL; // 用来构建新逆序链表的指针
    while (head != NULL)
    {
        temp = reverse;//备份当前的reverse新链表
        reverse = head;//将reverse指向原链表头节点,即获取到最前面的一个结点。
        head = head->next;//原链表头指针向后移
        reverse->next = temp;//将备份的新链表reverse接在获取的结点后面
    }
    list = reverse;
}

删除指定数据

链表删除所有为item的数据

算法思想

先从链表的第2个结点开始,从前往后依次判断链表中的所有结点是否满足条件,若某个结点的数据域为item,则删除该结点。最后再回过头来判断链表中的第1个结点是否满足条件,若满足则将其删除。

void PurgeItem(LinkList &list, ElemType item)
{
    LinkList p,q = list;
    p = list->next;
    while (p != NULL)
    {
        if (p->data == item)
        {
            q->next = p->next;//将链表结点指向目标结点的下一个
            free(p);//释放找到的目标结点
            p = q->next;//操作指针重新链接下一个结点
        }else
        {
            q = p;//下一个结点不是目标item,就指向下一个结点
            p = p->next;
        }
    }
    if (list->data == item)
    {//处理第一个结点就匹配的情况
        q = list;
        list = list->next;
        free(q);
    }
}

广义表的递归算法

求广义表的深度

广义表的深度定义为广义表中括弧的重数,例如多元多项式广义表的深度就是该多项式中的变元个数。

分析

设非空广义表为

LS=(a1,a2,…,an)LS=(a1,a2,…,an)

其中ai(i=1,2,…,n)ai(i=1,2,…,n)或为原子或为子表,那么求LS的深度可以分解为n个问题。每个子问题为求aiai的深度,若aiai是原子,则深度为0。若aiai为广义表,则按照上述处理,而LSLS为上述n个深度的最大值加1。定义空表的深度为1。

img

int GListDepth(GList L){
    // 采用头尾链表存储结构
    if(!L) return 1; 					// 空表深度为1
    if(L->tag == ATOM) return 0; 		// 原子深度为0
    for(max = 0,pp=L; pp; pp = pp->ptr.tp){
        dep = GListDepth(pp->ptr.hp);	// pp->ptr.hp指向子表 或 原子
        if(dep > max)max = dep;
    }
    return max + 1;
}

广义表的复制

任何一个非空广义表都可以分解成表头和表尾,一对确定的表头和表尾也可以唯一确定一个广义表。所以,复制一个广义表只需要分别复制表头和表尾,然后合成即可。

Status CopyGList(GList &T, GList L){
    // 采用头尾链表存储结构,由广义表L复制得到广义表T。
    if(!L) T = null;
   	else{
        // 不是空表,就需要建立表结点
        if(!(T = (GList)malloc(sizeof(GLNode)))) exit(OVERFLOW);
        T->tag = L->tag;
		if(L->tag == ATOM) T->atom = L->atom; // 如果是原子直接复制
        
        else{
            // 复制表头
        	CopyGList(T->ptr.hp, L->ptr.hp);
            // 复制表尾
            CopyGList(T->ptr.tp, L->ptr.tp);
        }
    }
    return OK;
}

  • 树的递归

    树的递归套路解题模版

    • 模版一共三步,就是递归的三部曲:
    1. 找终止条件:什么时候递归到头了?此题自然是root为空的时候,空树当然是平衡的。
    2. 思考返回值,每一级递归应该向上返回什么信息?参考代码中的注释。
    3. 单步操作应该怎么写?因为递归就是大量的调用自身的重复操作,因此从宏观上考虑,只用想想单步怎么写就行了,左树和右树应该看成一个整体,即此时树一共三个节点:root,root.left,root.right。

遍历

先序遍历PreOrder

  • 递归算法

    void PreOrder(BiTree T)
    {
        if (T != NULL)
        {
            vist(T);
            PreOrder(T->leftchild);
            PreOrder(T->rightchild)
        }
    }
    
  • 非递归算法

    算法思想

    若p所指结点不为空,则访问该结点,然后将该结点的地址入栈,然后再将p指向其左孩子结点;若p所指向的结点为空,则从堆栈中退出栈顶元素(某个结点的地址),将p指向其右孩子结点。重复上述过程,直到p = NULL且堆栈为空,遍历结束。

    void PreOrder(BiTree T)
    {
        InitStack(S);BiTree p = T;//初始化栈,p是遍历指针
        while (p || !IsEmpty(S))
        {                         //栈不空且p不空时循环
            if (p)
            {                     //一路向左
                vist(p);Push(S,p);
                p = p->lchild;
            }
            else
            {                    //出栈,并转向出栈结点的右子树
                Pop(S,p);
                p = p->rchild;
            }
        }
    }
    

树的最大深度

int maxDepth(struct TreeNode *root) {
    if (root == NULL) return 0;
    return fmax(maxDepth(root->left), maxDepth(root->right)) + 1;
}

二叉树直径(递归求深度)

直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点

  • 二叉树的直径:二叉树中从一个结点到另一个节点最长的路径,叫做二叉树的直径
  • 采用分治和递归的思想:
    - 根节点为root的二叉树的直径 = max(root->left的直径,root->right的直径,root->left的最大深度+root->right的最大深度+1)
  • 求树的深度
int depth (TreeNode * rt){
    if (rt == NULL){
        return 0;  //递归结束的出口
    }
    int L,R;
    L = depth( rt -> left); //对左结点做同样的事情
    R = depth( rt -> right); //对右结点做同样的事情
    return max(L,R) + 1; 
}
  • 求两结点之间的路径
class Solution{
    private:
    int Max = 0;  //设定一个局部变量
    int depth (TreeNode * rt){
    
    if (rt == NULL){
        return 0;  //递归结束的出口
    }
    int L,R;
    L = depth( rt -> left); //对左结点做同样的事情
    R = depth( rt -> right); //对右结点做同样的事情
    if (L + R > Max){ //每次递归都计算一次
        Max = L + R;
    }
    return max(L,R) + 1; 
    
}
class Solution {
    int ans;
    int depth(TreeNode* rt){
        if (rt == NULL) {
            return 0; // 访问到空节点了,返回0
        }
        int L = depth(rt->left); // 左儿子为根的子树的深度
        int R = depth(rt->right); // 右儿子为根的子树的深度
        ans = max(ans, L + R + 1); // 计算d_node即L+R+1 并更新ans
        return max(L, R) + 1; // 返回该节点为根的子树的深度
    }
public:
    int diameterOfBinaryTree(TreeNode* root) {
        ans = 1;
        depth(root);
        return ans - 1;
    }
};
int calMaxRoot(struct TreeNode* root, int *maxRoot){
    if (NULL == root) return 0;
    int lLen = calMaxRoot(root->left, maxRoot);
    int rLen = calMaxRoot(root->right, maxRoot);
    if (lLen + rLen > *maxRoot) *maxRoot = lLen + rLen;
    return (lLen > rLen ? lLen : rLen) + 1;
}

int diameterOfBinaryTree(struct TreeNode* root){
    int maxRoot = 0;
    calMaxRoot(root, &maxRoot);
    return maxRoot;
}

判断平衡二叉树

分析:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

BFS,广度优先,层次遍历

队列

起点入队

访问节点,同时将该节点的子节点入队,

image-20201122174137270

DFS,深度优先,

放入起点,入栈

出栈,访问该节点,同时将该结点的子节点入栈

出一个,就访问一个,看他有什么子节点,放进去

image-20201122174338936

查找

顺序查找/线性查找

适用于顺序表、链表

typedef struct{
    ElemType *elem; //动态数组基址
    int TabbleLen; //表的长度
}SSTable;
//顺序查找
int Search_Seq(SSTable ST, ElemType key){
    int i;
    for (i = 0; i < ST.TableLen && ST.elem[i] != key; ++i) ;
    //查找成功,返回元素下标;查找失败,返回-1
    return i == ST.TableLen?-1:i;
}

//带哨兵的实现(不用一直判断下标越界)
int Search_Seq(SSTable ST, ElemType key){
    ST.elem[0] = key;
    int i;
    for (i = ST.TableLen; ST.elem[i] != key; --i) ;//从后往前找
    return i; //查找成功,返回元素下标;查找失败,返回-1
}
  • 查找效率

    \(ASL_{成功} = \frac{n+1}{2}\)

    $ASL_{失败} = n+1 $

  • 时间复杂度

    O(n)

  • 优化

    • 有序表查找
      • 查找失败ASL更少
      • 查找判断数
        • 成功关键字对比次数= 结点所在层数
        • 失败关键字对比次数= 父结点所在层数
    • 被查概率不等
      • 查找成功ASL更少

折半查找/二分查找

仅适用于有序顺序表

typedef struct{     //查找表的数据结构(升序表)
    ElemType *elem; //动态数组基址
    int TabbleLen; //表的长度
}SSTable;
//顺序查找
int Binary_Search(SSTable ST, ElemType key){
    int low = 0, high = L.TableLen-1, mid;
    while (low <= high){
        mid = (low + high) / 2; //取中间位置
        if (L.elem[mid] == key)
            return mid;  //查找成功则返回所在位置
        else if (L.elem[mid] > key)
            high = mid - 1;  //从前半部分继续查找
        else 
            low = mid + 1;  //从后半部分继续查找
    }
    return -1;  //查找失败,返回-1
}
  • 查找效率

    image-20201011140551844
    • 折半查找的判定树一定是平衡二叉树
    • 折半查找的判定树只有最下面一层是不满的
    • 元素个数为n时树高$h = \lceil log_2(n+1)\rceil $ (不包含失败结点)
  • 时间复杂度

    \(O(log_2n)\)

    但并不能说折半一定比顺序查找快

分块查找/索引顺序查找

  • 索引表 :保存每个分块的最大关键字和分块的存储空间
  • 特点:块内无序、块间有序
  • 算法思想
    1. 在索引表中确定待查记录所属分块(可顺序、可折半)
    2. 在块内顺序查找
//索引表 
typedef struct{     
    ElemType maxVal; 
    int low,high; //表的长度
}Index;

//顺序表存储实际元素
ElemType List[100];
  • 查找效率

    • 在长度为 n 的查找表,等分为 b 块, 每块 s 个元素时

    • ASL = 索引查找长度\(L_I\) + 块内查找长度\(L_S\)

    • 用顺序查找索引表

      \(L_I = \frac{b+1}{2}\)

      \(L_S = \frac{s + 1}{2}\)

      其中,当\(s = \sqrt n\) 时,\(ASL_{最小} = \sqrt n + 1\)

    • 用折半查找索引表

      \(L_I = \lceil log_2(b+1) \rceil\)

      \(L_S = \frac{s + 1}{2}\)

B树


  • 树中每个结点至多有m棵子树,至多含有m-1个关键字

  • 除根节点外,非叶子结点至少有\(\lceil m/2 \rceil\)棵子树,至少有\(\lceil m / 2 \rceil - 1\) 个关键字

  • n个关键字B树必有n+1个叶子结点(失败结点)

  • 含n个关键字的m阶B树的高度h

    $log_m(n+1) \leq h \leq log_{\lceil m/2 \rceil}\frac{n+1}{2}+1 $

  • 终端节点

  • 叶子节点

排序

内部排序

直接插入排序

算法思路:将待排序的关键字与已经排好的部分有序序列的中关键字从后往前进行比较,插入到合适位置,直至所有关键字都被插入到有序序列中

void insertSort(int R[],int n)//数组元素个数
{
    int i,j;
    int temp;
    for(i=1;i<n;i++)//直接从第二个元素开始,因为第一个元素组成的序列一定有序
    {
        temp=R[i];
        j=i-1;
        while(j>0&&temp<R[j])
        {
            R[j+1]=R[j];
            --j;
        }
        R[j+1]=temp;
    }
}

void insertSort(int R[],int n){
    int i,j;
    for(i=1;i<n;i++){
        if(R[i]<R[i-1]){
            int temp=R[i];
            for(j=i-1;j>=0&&R[j]>temp;j--){
                R[j+1]=R[j];
            }
            R[j+1]=temp;
        }
    }
}

冒泡排序

算法思路:每一趟排序从第一个关键字开始,与其后一个进行比较,如果前一个大于后一个则交换,否则不交换,这样第一趟就可以把序列中最大的关键字交换到最后,第二趟可以把第二大的关键字交换到倒数第二个位置上......排序结束的条件是在一趟排序过程中没有发生关键字交换

void bubbleSort(int R[],int n)
{
    int i,j,flag,temp;
    for(i=n-1;i>=1;--i)
    {
        flag=0;
        for(j=i;j<=i;j++)
        {
            if(R[j-1]>R[j])
            {
                temp=R[j];
                R[j]=R[j-1];
                R[j-1]=temp;
                flag=1;//发生了关键字交换
            }
        }
        if(flag==0)
            return;
    }
}

简单选择排序

算法思路:从无序序列中每趟挑出一个最小的关键字,与第i个关键字交换。

void selectSort(int R[],int n)
{
    int i,j,k,temp;
    for(i=0;i<n;i++)
    {
        k=i;
        //从无序序列中挑出最小的一个关键字
        for(j=k+1;j<n;j++)
            if(R[k]>R[j])
                k=j;
        //将上面挑出的最小关键字与i位置上的关键字交换
        temp=R[i];
        R[i]=R[k];
        R[k]=temp;
    }
}

希尔排序

算法思路:将序列按照规则划分为几个子序列,分别对这几个子序列进行直接插入排序。缩小增量,最后一个增量一定是1。如有10个关键字,第一个增量为5,则0-5,1-6,2-7,3-8,4-9;第二个增量为3,则0-3-6-9,1-4-7,2-5-8;第三个增量为1,直接插入排序。

//考研数据结构需重点掌握其执行流程
void shellSort(int R[],int n)
{
    int temp;
    for(int gap=n/2;gap>0;gap/=2)
    {
        for(int i=gap;i<n;i++)
        {
            temp=R[i];
            int j;
            for(j=1;j>=gap&&R[j-gap]>temp;j-=gap)
                R[j]=R[j-gap];
            R[j]=temp;
        }
    }
}

快速排序

算法思路:每一趟选择序列的第一个关键字作为枢轴,将序列中比枢轴小的放到枢轴前面,比枢轴大的放到枢轴后面。

void quickSort(int R[],int low,int high)
{
    int temp;
    int i=low,j=high;//指向头尾关键字
    if(low<high)
    {
        temp=R[low];//第一个数作为枢轴
        while(i<j)
        {
            //从后往前扫描,遇到比枢轴小的关键字,停到这里
            while(j>i&&R[j]>=temp)
                --j;
            //将j位置上这个比枢轴小的值放到前面i位置上
            if(i<j)
            {
                R[i]=R[j];
                ++i;
            }
            //从前往后扫描,遇到比枢轴大的关键字,停到这里
            while(i<j&&R[i]<temp)
                ++i;
            //将i位置上这个比枢轴大的值放到后面j位置上
            if(i<j)
            {
                R[j]=R[i];
                --j;
            }
        }
        //i和j相遇后,把枢轴放入这个i等于j的位置
        R[i]=temp;
        //分别对枢轴左边和右边的序列进行递归划分
        quickSort(R,low,i-1);
        quickSort(R,i+1,high);
    }
}

堆排序

算法思路:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列。

void sift(int R[],int low,int high)//节点调整
{
    int i=low,j=2*i;
    int temp=R[i];
    while(j<=high)
    {
        if(j<high&&R[j]<R[j+1])
            ++j;
        if(temp<R[j])
        {
            R[i]=R[j];
            i=j;
            j=2*i;
        }
        else
            break;
    }
    R[i]=temp;
}
void heapSort(int R[],int n)
{
    int i,temp;
    for(i=n/2;i>=1;--i)
        sift(R,i,n);
    for(i=n;i>=2;--i)
    {
        //换出根节点的关键字,将其放入最终位置
        temp=R[1];
        R[1]=R[i];
        R[i]=temp;
        sift(R,1,i-1);
    }
}

归并排序

算法思路:先将序列分为两半,对两个序列进行归并排序,得到两个有序序列,然后将这两个有序序列合并成为一个有序序列。

void merge(int arr[],int low,int mid,int high)
{
    int i,j,k;
    int n1=mid-low+1;
    int n2=high-mid;
    int L[n1],R[n2];
    for(i=0;i<n1;i++)
        L[i]=arr[low+i];
    for(j=0;j<n2;j++)
        R[j]=arr[mid+1+j];
    i=0;
    j=0;
    k=low;
    while(i<n1&&j<n2)
    {
        if(L[i]<=R[j])
            arr[k]=L[i++];
        else
            arr[k]=R[j++];
        k++;
    }
    while(i<n1)
        arr[k++]=L[i++];
    while(j<n2)
        arr[k++]=R[j++];
}
void mergeSort(int R[],int low,int high)
{
    if(low<high)
    {
        int mid=(low+high)/2;
        mergeSort(R,low,mid);
        mergeSort(R,mid+1,high);
        merge(R,low,mid,high);//把R数组中low到mid和mid+1到high范围内的两段有序序列归并成一段有序序列
    }
}

基数排序

算法思路:不用比较,有最低位优先和最高位优先两种。以最低位优先为例,第一趟按最低位放入对应的桶中,收集时从0号桶从下往上收集,依次排开,收集结果最低位有序。依次对中间位和高位进行分配和收集,最后整个序列有序。

内部排序算法比较

算法 最好情况 最坏情况 平均情况 空间复杂度 稳定性
直接插入排序 O(n) O(n2) O(n2) O(1) 稳定
冒泡排序 O(n) O(n2) O(n2) O(1) 稳定
简单选择排序 O(n2) O(n2) O(n2) O(1) 不稳定
希尔排序 O(1) 不稳定
快速排序 O(nlog2n) O(nlog2n) O(n2) O(log2n) 不稳定
堆排序 O(nlog2n) O(nlog2n) O(nlog2n) O(1) 不稳定
二路归并排序 O(nlog2n) O(nlog2n) O(nlog2n) O(n) 稳定
基数排序 O(d(n+r)) O(d(n+r)) O(d(n+r)) O(r) 稳定

image-2

image-sort

排序稳定性记忆口诀:

快些选对不稳定

  • 快——快速排序
  • 些——希尔排序
  • 选——选择排序
  • 对——堆排序

关键字位置问题

直接插入排序、折半插入排序:在最后一趟排序前,没有一个关键字到达其最终位置
快速排序:每一趟排序后有一个关键字到达最终位置
归并排序:在一次排序结束后不一定能选出一个关键字放到其最终位置上
希尔排序:不能保证每趟排序至少能将一个关键字放到其最终位置上

适用情况

直接插入排序、冒泡排序:适用于序列基本有序的情况
快速排序:待排序列越接近无序,算法效率越高
简单选择排序、归并排序:执行次数与初试序列无关
堆排序:适合关键字数很多的情况
基数排序:适合序列中关键字很多,但组成关键字的取值范围较小的情况

外部排序

posted @ 2022-08-26 09:44  Hecto  阅读(167)  评论(0)    收藏  举报