数据结构和算法(C语言)

本文详细介绍了数据结构中的线性结构,包括数组和链表,以及它们在栈和队列中的应用。同时,讨论了排序算法如冒泡、选择、插入、快速、归并和希尔排序。此外,还提到了递归的概念和树这一非线性结构,包括二叉树的不同类型和遍历方法。文章以C语言实现了相关操作示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据结构和算法

1 概述

  • 定义

    数据结构 = 个体的存储 +个体关系的存储 (研究数据的存储)

    算法 = 对存储数据的操作 (对数据的操作)

  • 衡量算法的标准

    1. 时间复杂度

      大概程序要执行的次数,而非执行的时间

    2. 空间复杂度

      算法执行过程中大概所占用的最大内存

    3. 难易程度

    4. 健壮性

  • 预备知识

    • 指针

      指针就是地址 地址就是指针

      指针变量是存放内存单元地址的变量

      指针的本质是一个操作受限的非负整数

      一个字节就是一个地址,比方说一个double型的变量有8个字节,那么指针当中存了8个地址吗?不是,一般是存了这8个地址当中的首地址。

    • 地址

      地址就是内存单元的编号

      从0开始的非负整数

      范围:0——FFFFFFFF【0-4G-1】

    • 结构体

    • 跨函数使用内存——malloc

2 线性结构【把所有的节点用一根直线穿起来】

2.1 连续存储[数组]

  • 什么叫数组

    元素类型相同,大小相同

  • 优缺点

    • 优点

      • 存取速度很快
    • 缺点

      • 事先必须知道数组的长度
      • 插入删除元素很慢
      • 空间通常是有限制的
      • 需要大块连续的内存块

2.2 离散存储[链表]

  • 定义

    n个节点离散分配

    彼此通过指针相连

    每个节点只有一个前驱节点,后续节点

    首节点没有前驱节点,尾节点没有后续节点

  • 专业术语:

    • 首节点:第一个有效节点

    • 尾节点:最后一个有效节点

    • 头节点:

      头节点的数据类型和首节点类型一样

      第一个有效节点之前的那个节点

      头节点并不存放有效数据

      加头节点的目的主要是为了方便对链表的操作

    • 头指针:指向头结点的指针变量

    • 尾指针:指向尾节点的指针变量

  • 如果希望通过一个函数来对链表进行处理,我们至少需要接收链表的哪些参数

    只需要一个参数:头指针,因为我们通过头指针可以推算出链表的其它所有参数

  • 分类

    • 单链表

    • 双链表:

      每一个节点有两个指针域

    • 循环链表:

      能通过任何一个节点找到其他所有的节点

    • 非循环链表

  • 算法

    • 遍历

    • 查找

    • 清空

    • 销毁

    • 求长度

    • 排序

    • 删除节点

    • 插入节点

      狭义的算法是与数据的存储方式密切相关

      广义的算法是与数据的存储方式无关

      泛型:

      ​ 利用某种技术达到的效果就是:不同的存数方式,执行的操作是一样的

  • 优缺点

    • 优点

      • 空间没有限制

      • 插入删除元素很快

    • 缺点

      • 存取速度很慢
  • 代码实现

    # include <stdio.h>
    # include <malloc.h>
    # include <stdlib.h>
    
    typedef struct Node
    {
    	int data; //数据域
    	struct Node * pNext; //指针域
    }NODE, *PNODE; //NODE等价于struct Node    PNODE等价于struct Node *
    
    //函数声明
    PNODE create_list(void);
    void traverse_list(PNODE pHead);
    bool is_empty(PNODE pHead);
    int length_list(PNODE);
    bool insert_list(PNODE, int, int);  //在pHead所指向链表的第pos个节点的前面插入一个新的结点,该节点的值是val, 并且pos的值是从1开始
    bool delete_list(PNODE, int, int *);
    void sort_list(PNODE);
    
    
    int main(void)
    {
    	PNODE pHead = NULL; //等价于 struct Node * pHead = NULL;
    	int val;
    
    	pHead = create_list();  //create_list()功能:创建一个非循环单链表,并将该链表的头结点的地址付给pHead
    	traverse_list(pHead);
    	
    	//insert_list(pHead, -4, 33);
    	if ( delete_list(pHead, 4, &val) )
    	{
    		printf("删除成功,您删除的元素是: %d\n", val);
    	}
    	else
    	{
    		printf("删除失败!您删除的元素不存在!\n");
    	}
    
    	traverse_list(pHead);
    	
    	//int len = length_list(pHead);
    	//printf("链表的长度是%d\n", len);
    
    	//sort_list(pHead);
    	//traverse_list(pHead);
    	
    /*	if ( is_empty(pHead) )
    		printf("链表为空!\n");
    	else
    		printf("链表不空!\n");
    */
    	return 0;
    }
    
    PNODE create_list(void)
    {
    	int len;  //用来存放有效节点的个数
    	int i;
    	int val; //用来临时存放用户输入的结点的值
    
    	//分配了一个不存放有效数据的头结点
    	PNODE pHead = (PNODE)malloc(sizeof(NODE));
    	if (NULL == pHead)
    	{
    		printf("分配失败, 程序终止!\n");
    		exit(-1);
    	}
    	PNODE pTail = pHead;
    	pTail->pNext = NULL;
    
    	printf("请输入您需要生成的链表节点的个数: len = ");
    	scanf("%d", &len);
    	
    	for (i=0; i<len; ++i)
    	{
    		printf("请输入第%d个节点的值: ", i+1);
    		scanf("%d", &val);
    		
    		PNODE pNew = (PNODE)malloc(sizeof(NODE));
    		if (NULL == pNew)
    		{
    			printf("分配失败, 程序终止!\n");
    			exit(-1);
    		}
    		pNew->data = val;
    		pTail->pNext = pNew;
    		pNew->pNext = NULL;
    		pTail = pNew;
    	}
    	
    	return pHead;
    }
    
    void traverse_list(PNODE pHead)
    {
    	PNODE p = pHead->pNext;
    
    	while (NULL != p)
    	{
    		printf("%d  ", p->data);
    		p = p->pNext;
    	}
    	printf("\n");
    	
    	return;
    }
    
    bool is_empty(PNODE pHead)
    {
    	if (NULL == pHead->pNext)
    		return true;
    	else
    		return false;
    }
    
    int length_list(PNODE pHead)
    {
    	PNODE p = pHead->pNext;
    	int len = 0;
    
    	while (NULL != p)
    	{
    		++len;
    		p = p->pNext;
    	}
    
    	return len;
    }
    
    void sort_list(PNODE pHead)
    {
    	int i, j, t;
    	int len = length_list(pHead);
    	PNODE p, q;
    	
    	for (i=0,p=pHead->pNext; i<len-1; ++i,p=p->pNext)
    	{
    		for (j=i+1,q=p->pNext; j<len; ++j,q=q->pNext)
    		{
    			if (p->data > q->data)  //类似于数组中的:  a[i] > a[j]
    			{
    				t = p->data;//类似于数组中的:  t = a[i];
    				p->data = q->data; //类似于数组中的:  a[i] = a[j];
    				q->data = t; //类似于数组中的:  a[j] = t;
    			}
    		}
    	}
    
    	return;
    }
    
    //在pHead所指向链表的第pos个节点的前面插入一个新的结点,该节点的值是val, 并且pos的值是从1开始
    bool insert_list(PNODE pHead, int pos, int val)
    {
    	int i = 0;
    	PNODE p = pHead;
    
    	while (NULL!=p && i<pos-1)
    	{
    		p = p->pNext;
    		++i;
    	}
    
    	if (i>pos-1 || NULL==p)
    		return false;
    
    	PNODE pNew = (PNODE)malloc(sizeof(NODE));
    	if (NULL == pNew)
    	{
    		printf("动态分配内存失败!\n");
    		exit(-1);
    	}
    	pNew->data = val;
    	PNODE q = p->pNext;
    	p->pNext = pNew;
    	pNew->pNext = q;
    
    	return true;
    }
    
    
    bool delete_list(PNODE pHead, int pos, int * pVal)
    {
    	int i = 0;
    	PNODE p = pHead;
    
    	while (NULL!=p->pNext && i<pos-1)
    	{
    		p = p->pNext;
    		++i;
    	}
    
    	if (i>pos-1 || NULL==p->pNext)
    		return false;
    	
    	PNODE q = p->pNext;
    	*pVal = q->data;
    
    	//删除p节点后面的结点
    	p->pNext = p->pNext->pNext;
    	free(q);
    	q = NULL;
    	
    	return true;
    
    }
    

2.3 线性结构的常见应用——栈

  • 定义

    • 一种可以实现“先进先出”的存储结构,类似于箱子
  • 分类

    • 静态栈
    • 动态栈
  • 算法

    • 出栈

    • 压栈

    • 遍历

      # include <stdio.h>
      # include <malloc.h>
      # include <stdlib.h>
      
      typedef struct Node
      {
      	int data;
      	struct Node * pNext;
      }NODE, * PNODE;
      
      typedef struct Stack
      {
      	PNODE pTop;
      	PNODE pBottom;
      }STACK, * PSTACK;  //PSTACK 等价于 struct STACK *
      
      void init(PSTACK);
      void push(PSTACK, int );
      void traverse(PSTACK);
      bool pop(PSTACK, int *);
      void clear(PSTACK pS);
      
      int main(void)
      {
      	STACK S;  //STACK 等价于 struct Stack
      	int val;
      
      	init(&S);  //目的是造出一个空栈
      	push(&S, 1); //压栈
      	push(&S, 2);
      	push(&S, 3);
      	push(&S, 4);
      	push(&S, 5);
      	push(&S, 6);
      	traverse(&S); //遍历输出
      	
      	clear(&S);
      	//traverse(&S); //遍历输出
      
      	if ( pop(&S, &val) )
      	{
      		printf("出栈成功,出栈的元素是%d\n", val);
      	}
      	else
      	{
      		printf("出栈失败!\n");
      	}
      
      	traverse(&S); //遍历输出
      
      	return 0;
      }
      
      void init(PSTACK pS)
      {
      	pS->pTop = (PNODE)malloc(sizeof(NODE));
      	if (NULL == pS->pTop)
      	{
      		printf("动态内存分配失败!\n");
      		exit(-1);
      	}
      	else
      	{
      		pS->pBottom = pS->pTop;
      		pS->pTop->pNext = NULL; //pS->Bottom->pNext = NULL;
      	}
      }
      
      void push(PSTACK pS, int val)
      {
      	PNODE pNew = (PNODE)malloc(sizeof(NODE));
      	
      	pNew->data = val;
      	pNew->pNext = pS->pTop; //pS->Top不能改成pS->Bottom
      	pS->pTop = pNew;
      
      	return;
      }
      
      void traverse(PSTACK pS)
      {
      	PNODE p = pS->pTop;
      
      	while (p != pS->pBottom)
      	{
      		printf("%d  ", p->data);
      		p = p->pNext;
      	}
      	printf("\n");
      
      	return;
      }
      
      bool empty(PSTACK pS)
      {
      	if (pS->pTop == pS->pBottom)
      		return true;
      	else
      		return false;
      }
      
      //把pS所指向的栈出栈一次,并把出栈的元素存入pVal形参所指向的变量中,如果出栈失败,返回false,否则返回true
      bool pop(PSTACK pS, int * pVal)
      {
      	if ( empty(pS) ) //pS本身存放的就是S的地址
      	{
      		return false;
      	}
      	else
      	{
      		PNODE r = pS->pTop;
      		*pVal = r->data;
      		pS->pTop = r->pNext;
      		free(r);
      		r = NULL;
      
      		return true;
      	}
      }
      
      //clear清空
      void clear(PSTACK pS)
      {
      	if (empty(pS))
      	{
      		return;
      	}
      	else
      	{
      		PNODE p = pS->pTop;
      		PNODE q = NULL;
      
      		while (p != pS->pBottom)
      		{
      			q = p->pNext;
      			free(p);
      			p = q;
      		}
      		pS->pTop = pS->pBottom;
      	}
      }
      
  • 应用

    • 函数调用
    • 中断
    • 表达式求值
    • 内存分配
    • 缓冲处理
    • 迷宫

2.3 线性结构的常见应用——队列

  • 定义:

    一种可以实现“先进先出”的存储结构

  • 分类

    • 链式队列——用链表实现

    • 静态队列——用数组实现

      静态队列通常都必须是循环队列

    • 循环队列的讲解:

      1. 静态队列为什么必须是循环队列

        空间利用率

      2. 循环队列需要几个参数来确定

        需要两个参数来确定

      3. 循环队列各个参数的含义

        • 队列初始化:

          font和rear的值都是0

        • 队列非空:

          font代表的是队列的第一个元素

          rear代表的是队列的最后一个有效元素的下一个元素

        • 队列空

          font和rear的值相等,但不一定是0

      4. 循环队列入队伪算法讲解

        两步完成:

        1. 将值存入r所代表的位置
        2. r = (r+1) % 数组的长度
      5. 循环队列出队伪算法讲解

        f = (f+1) % 数组的长度

      6. 如何判断循环队列是否为空

        如果front与rear的值相等,则该队列一定为空

      7. 如何判断循环队列是否已满

        少用一个,如果r和f的值紧挨着,则队列已满

        if ((r + 1) % 数组长度 == f)

  • 队列的具体应用:

    所有和时间有关的操作都与队列有关

  • 代码示例

    # include <stdio.h>
    # include <malloc.h>
    
    typedef struct Queue
    {
    	int * pBase;
    	int front;
    	int rear;
    }QUEUE;  
    
    void init(QUEUE *);
    bool en_queue(QUEUE *, int val);  //入队
    void traverse_queue(QUEUE *);
    bool full_queue(QUEUE *);
    bool out_queue(QUEUE *, int *);
    bool emput_queue(QUEUE *);
    
    int main(void)
    {
    	QUEUE Q;
    	int val;
    
    	init(&Q);
    	en_queue(&Q, 1);
    	en_queue(&Q, 2);
    	en_queue(&Q, 3);
    	en_queue(&Q, 4);
    	en_queue(&Q, 5);
    	en_queue(&Q, 6);
    	en_queue(&Q, 7);
    	en_queue(&Q, 8);
    
    	traverse_queue(&Q);
    
    	if ( out_queue(&Q, &val) )
    	{
    		printf("出队成功,队列出队的元素是: %d\n", val);
    	}
    	else
    	{
    		printf("出队失败!\n");
    	}
    	traverse_queue(&Q);
    
    	return 0;
    }
    
    void init(QUEUE *pQ)
    {
    	pQ->pBase = (int *)malloc(sizeof(int) * 6);
    	pQ->front = 0;
    	pQ->rear = 0;
    }
    
    bool full_queue(QUEUE * pQ)
    {
    	if ( (pQ->rear + 1) % 6 == pQ->front  )
    		return true;
    	else
    		return false;
    }
    
    bool en_queue(QUEUE * pQ, int val)
    {
    	if ( full_queue(pQ) )
    	{
    		return false;
    	}
    	else
    	{
    		pQ->pBase[pQ->rear] = val;
    		pQ->rear = (pQ->rear+1) % 6;
    
    		return true;
    	}
    }
    
    void traverse_queue(QUEUE * pQ)
    {
    	int i = pQ->front;
    
    	while (i != pQ->rear)
    	{
    		printf("%d  ", pQ->pBase[i]);
    		i = (i+1) % 6;
    	}
    	printf("\n");
    
    	return;
    }
    
    bool emput_queue(QUEUE * pQ)
    {
    	if ( pQ->front == pQ->rear )
    		return true;
    	else
    		return false;
    }
    
    bool out_queue(QUEUE * pQ, int * pVal)
    {
    	if ( emput_queue(pQ) )
    	{
    		return false;
    	}
    	else
    	{
    		*pVal = pQ->pBase[pQ->front];
    		pQ->front = (pQ->front+1) % 6;
    
    		return true;
    	}
    }
    

2.4 递归

  • 定义

    一个函数自己直接或间接调用自己

  • 前提条件

    1. 递归必须得有一个明确的终止条件
    2. 该函数所处理的数据规模必须在递减
    3. 这个转化必须是可解的
  • 循环和递归

    • 递归:
      • 易于理解
      • 速度慢
      • 存储空间大
    • 循环:
      • 不易理解
      • 速度快
      • 存储空间小
  • 举例

    1. 求阶乘

    2. 1+2+3+4+…100的和

    3. 汉诺塔

      # include <stdio.h>
      /*
       * n是盘子的数量
       * A代表盘子所在的柱子
       * B代表需要借助的柱子
       * C代表盘子所去的目的地
       */
      void hannuota(int n, char A, char B, char C)
      {
      /*
      	如果是1个盘子
      		直接将A柱子上的盘子从A移到C
      	否则
      		先将A柱子上的n-1个盘子借助C移到B
      		直接将A柱子上的盘子从A移到C
      		最后将B柱子上的n-1个盘子借助A移到C
      */
      	if (1 == n)
      	{
      		printf("将编号为%d的盘子直接从%c柱子移到%c柱子\n", n, A, C);
      	}
      	else
      	{
      		hannuota(n-1, A, C, B);
      		printf("将编号为%d的盘子直接从%c柱子移到%c柱子\n", n, A, C);
      		hannuota(n-1, B, A, C);
      	}
      }
      
      int main(void)
      {
      	char ch1 = 'A';
      	char ch2 = 'B';
      	char ch3 = 'C';
      	int n;
      
      	printf("请输入要移动盘子的个数: ");
      	scanf("%d", &n);
      
      	hannuota(n, 'A', 'B', 'C');
      
      
      	return 0;
      }
      
    4. 走迷宫

  • 递归的应用

    • 数和森林就是以递归的方式定义的
    • 数和图的很多算法都是以递归来实现的
    • 很多数学公式就是以递归的方式定义的

3 非线性结构

3.1 树

  • 定义

    • 专业定义
      1. 有且只有一个称为根的节点
      2. 有若干个互不相交的子树,这些子树本身也是一棵树
    • 通俗的定义
      1. 树是由节点和边组成
      2. 每个节点只有一个父节点但可以有多个子节点
      3. 但是有一个节点例外,该节点没有父节点,此节点称为根节点
    • 专业术语
      • 节点 父节点
      • 子孙
      • 深度:从根节点到最底层节点的层数
      • 叶子节点:没有子节点的节点
      • 非叶子节点
      • 度:子节点的个数称为度
  • 分类

    • 一般树
      • 任意一个节点的子节点的个数都不受限制
    • 二叉树
      • 任意一个节点的子节点个数最多两个,且子节点的位置不可更改
      • 二叉树的分类
        • 二叉搜索树:有序树
        • 满二叉树:在不增加树的层数的前提下,无法再多添加一个节点的二叉树就是满二叉树
        • 完全二叉树:如果只是删除了满二叉树最底层最右边的连续若干个节点,这样形成的二叉树就是完全二叉树
    • 森林
      • n个互不相交的树的集合
  • 二叉树的存储

    • 连续存储【需要转化为完全二叉树】
      • 优点:查找某个节点的父节点和子节点(也包括判断有没有子节点)
      • 缺点:耗用内存空间过大
    • 链式存储
  • 森林的存储

    先把森林转化为二叉树,再存储二叉树

  • 二叉树操作

    • 遍历

      • 先序遍历【先访问根节点】

        先访问根节点,再先序访问左子树,再先序访问右子树

      • 中序遍历【中间访问根节点】

        中序遍历左子树,再访问根节点,再中序遍历右子树

      • 后序遍历【最后访问根节点 】

        先后序遍历左子树,再后序遍历右子树,再访问根节点

  • 已知两种遍历序列求原始二叉树

    • 通过先序和中序

    • 通过中序和后序

  • 应用

    • 树是数据库中数据组织一种重要形式
    • 操作系统父子进程的关系本身就是一棵树
    • 面向对象语言中类的继承关系本身就是一棵树
    • 赫夫曼树
  • 插讲一个知识点

    struct BTnode* function()
    {
    	//肯定是动态创建的二叉树,静态分配会被释放掉
        int i;//静态分配
        char str[100];//静态分配
        struct BTnode *p = (struct BTnode *)malloc(sizeof(struct BTnode));//p是静态分配,malloc动态分配,静态分配的变量函数调用结束会被释放掉,所以在调用该函数时,应创建一个指针来指向动态分配好的空间
        return p;
    }
    
  • 代码示例

    # include <stdio.h>
    # include <malloc.h>
    
    struct BTNode
    {
    	char data;
    	struct BTNode * pLchild; //p是指针 L是左  child是孩子
    	struct BTNode * pRchild;
    };
    
    void PostTraverseBTree(struct BTNode * pT);
    struct BTNode * CreateBTree(void);
    void PreTraverseBTree(struct BTNode * pT);
    void InTraverseBTree(struct BTNode * pT);
    
    int main(void)
    {
    	struct BTNode * pT = CreateBTree();
    	
    //	PreTraverseBTree(pT);
    //	InTraverseBTree(pT);
    	PostTraverseBTree(pT);
    	
    	return 0;
    }
    
    void PostTraverseBTree(struct BTNode * pT)
    {
    	if (NULL != pT)
    	{
    		if (NULL != pT->pLchild)
    		{
    			PostTraverseBTree(pT->pLchild);
    		}	
    		if (NULL != pT->pRchild)
    		{
    				PostTraverseBTree(pT->pRchild);
    			//pT->pLchild可以代表整个左子树
    		}
    		printf("%c\n", pT->data);
    	}
    }
    
    void InTraverseBTree(struct BTNode * pT)
    {
    	if (NULL != pT)
    	{
    		if (NULL != pT->pLchild)
    		{
    			InTraverseBTree(pT->pLchild);
    		}
    		
    		printf("%c\n", pT->data);
    	
    		if (NULL != pT->pRchild)
    		{
    				InTraverseBTree(pT->pRchild);
    			//pT->pLchild可以代表整个左子树
    		}	
    	}
    }
    
    void PreTraverseBTree(struct BTNode * pT)
    {
    	if (NULL != pT)
    	{
    		printf("%c\n", pT->data);
    	
    		if (NULL != pT->pLchild)
    		{
    			PreTraverseBTree(pT->pLchild);
    		}
    		
    		if (NULL != pT->pRchild)
    		{
    				PreTraverseBTree(pT->pRchild);
    			//pT->pLchild可以代表整个左子树
    		}	
    	}	
    
    /*
    	伪算法
    	先访问根节点
    	再先序访问左子树
    	再先序访问右子树
    */
    }
    
    struct BTNode * CreateBTree(void)
    {
    	struct BTNode * pA = (struct BTNode *)malloc(sizeof(struct BTNode));
    	struct BTNode * pB = (struct BTNode *)malloc(sizeof(struct BTNode));
    	struct BTNode * pC = (struct BTNode *)malloc(sizeof(struct BTNode));
    	struct BTNode * pD = (struct BTNode *)malloc(sizeof(struct BTNode));
    	struct BTNode * pE = (struct BTNode *)malloc(sizeof(struct BTNode));
    
    	pA->data = 'A';
    	pB->data = 'B';
    	pC->data = 'C';
    	pD->data = 'D';
    	pE->data = 'E';
    
    	pA->pLchild = pB;
    	pA->pRchild = pC;
    	pB->pLchild = pB->pRchild = NULL;
    	pC->pLchild = pD;
    	pC->pRchild = NULL;
    	pD->pLchild = NULL;
    	pD->pRchild = pE;
    	pE->pLchild = pE->pRchild = NULL;
    
    	return pA;
    }
    

4 排序

4.1 冒泡排序

  • 原理:

    将一段序列的最大值(最小值)拿到最左边或者最右边的操作,使用循环重复操作,(每轮排序都会少一个最大值或最小值),当最后只剩下一个数据的时候整个序列就已经排好序了。

  • 代码实现

    /*
    直接使用两层循环去实现,外层循环主要作用是存放最大值或最小值的,内存循环的主要作用是找到发生冲突的元素,如果发生冲突就交换两个数据。当两层循环的结束的时候整个序列就自然排好序了。时间复杂度为O(n^2).
    */
    void bubSort(Ty* elems,int size) {
    	for(int i = 0; i < size -1; i++)
        {
    		for(int j = 0; j < size - i - 1; j++)
            {
                if(elems[j] > elems[j+1])
                {
                    int temp = elems[j];
                    elems[j] = elems[j+1];
                    elems[j+1] = temp;
                }
            }
        }
    }
    

4.2 选择排序

  • 原理

    基本思想和冒泡排序是一样的,选择排序相对于冒泡排序的优点就是减少交换次数。算法思想都是在序列中找到最大值(最小值),然后存放好下次进入循环就访问不到这个最大值(最小值)。当两层循环都结束的时候序列就自然排好了。

  • 代码实现

    void Swap(int *nums, int idx1, int idx2)
    {
        // 轮换赋值
        int tmp = nums[idx1];
        nums[idx1] = nums[idx2];
        nums[idx2] = tmp;
    }
    int* sortArray(int* nums, int numsSize, int* returnSize){
        *returnSize = numsSize;
        for (int i = 0; i < numsSize - 1; ++i) {    // 选择排序需要经过 n - 1 轮选择
            // 在 nums[i...len-1] 中选出最小的元素
            int midIdx = i; // 最小元素的索引值
            for (int j = i + 1; j < numsSize; ++j) {
                if (nums[j] < nums[midIdx]) {
                    midIdx = j;
                }
            }
            // 交换
            Swap(nums, i, midIdx);
        }
    
        return nums;
    }
    

4.3 插入排序

  • 原理

    基本思想还是冒泡排序,不过插入排序是两边相靠的冒泡,所以在序列部分有序的情况下,插入排序的效率要比冒泡排序效率高。从序列的尾部开始往前比较,如果当前的数据小于(大于)前一个的数据就进行交换,否则进入下一次循环,直到外层循环遍历完整个序列就自然排好序了。

  • 代码实现

    void Swap(int *nums, int idx1, int idx2)
    {
        // 轮换赋值
        int tmp = nums[idx1];
        nums[idx1] = nums[idx2];
        nums[idx2] = tmp;
    }
    int* sortArray(int* nums, int numsSize, int* returnSize){
        *returnSize = numsSize;
        // 把 nums[i] 插入有序数组 nums[0...i-1]
        for (int i = 1; i < numsSize; ++i) {    // 插入排序需要进行 n - 1 轮,
            for (int j = i; j > 0; --j) {
                if (nums[j - 1] > nums[j]) {
                    Swap(nums, j - 1, j);
                } else {    // 提前终止,退出内层循环
                    break;  
                }
            }
            // for (int j = i; j > 0 && nums[j - 1] > nums[j]; --j) {  // 等价写法
            //     Swap(nums, j - 1, j);
            // }
        }
    
        return nums;
    }
    

4.4 快速排序

  • 原理

    快速排序的核心思想是设立一个轴,然后其他数据都和这个轴作比较,最后把轴放在序列的中间,执行完一遍快速排序后左边的数据都比轴小,右边的数据都比轴大。然后递归下去,当递归结束的时候就拍好序了。快速排序的排序很快,但是当数据形成一边倒的情况的时候就发挥不出快速排序的优势。

  • 代码实现

    void Swap(int *nums, int idx1, int idx2)
    {
        int tmp = nums[idx1];
        nums[idx1] = nums[idx2];
        nums[idx2] = tmp;
    }
    
    int Partition(int *nums, int left, int right)
    {
        int pivot = nums[left];
        
        int j = left;
        // 循环不变量 nums[left + 1...j] <= pivot; nums(j, i) > pivot
        for (int i = left + 1; i <= right; ++i) {
            if (nums[i] <= pivot) {
                ++j;
                Swap(nums, j, i);
            }
        }
        Swap(nums, left, j);
    
        return j;
    }
    
    void QuickSort(int *nums, int left, int right)
    {
        if (left >= right) {
            return; 
        }
        int pivotIdx = Partition(nums, left, right);
        QuickSort(nums, left, pivotIdx - 1);
        QuickSort(nums, pivotIdx + 1, right);
    }
    
    int* sortArray(int* nums, int numsSize, int* returnSize){
        QuickSort(nums, 0, numsSize - 1);
        *returnSize = numsSize;
    
        return nums;
    }
    

4.5 归并排序

  • 原理

    把要排序的序列拆分成多个含有一个数据的序列,然后按照从小到大(从大到小)进行合并,这样就自然的将无序的序列排好序。

  • 代码实现

    /* 将两个有序区间nums[left...mid]和nums[mid+1...right]进行合并 */
    void MergeTwoSortedArray(int *nums, int left, int mid, int right, int *tmpNums)
    {
        for (int i = left; i <= right; ++i) {
            tmpNums[i] = nums[i];
        }
        int i = left, j = mid + 1;
        for (int k = left; k <= right; ++k) {
            if (i == mid + 1) {
                nums[k] = tmpNums[j];
                ++j;
            } else if (j == right + 1) {
                nums[k] = tmpNums[i];
                ++i;
            } else if (tmpNums[i] <= tmpNums[j]) {  // 注意是 ≤ 号,写成 < 则会不稳定
                nums[k] = tmpNums[i];
                ++i;
            } else {
                nums[k] = tmpNums[j];
                ++j;
            }
        }
    }
    
    /* 对nums[left...right]进行归并排序 */
    void MergeSort(int *nums, int left, int right, int *tmpNums)
    {
        if (left == right) {
            return;
        }
        int mid = (right + left) >> 1;
        MergeSort(nums, left, mid, tmpNums);
        MergeSort(nums, mid + 1, right, tmpNums);
        // 合并两个有序区间
        MergeTwoSortedArray(nums, left, mid, right, tmpNums);
    }
    
    int* sortArray(int* nums, int numsSize, int* returnSize){
        *returnSize = numsSize;
        int tmpNums[numsSize];  // 辅助数组
        MergeSort(nums, 0, numsSize - 1, tmpNums);
    
        return nums;
    }
    

4.6 希尔排序

  • 原理

    希尔排序是一种基于插入排序的算法,即分组插入排序 或 逐渐缩小间隔的插入排序。

  • 代码实现

    int* sortArray(int* nums, int numsSize, int* returnSize){
        *returnSize = numsSize;
        for (int delta = numsSize / 2; delta > 0; delta /= 2) { // 分组, 逐渐缩小间隔
            for (int start = 0; start < delta; ++start) {   // 对每一组进行处理
                // 分组插入排序
                for (int i = start + delta; i < numsSize; i += delta) {
                    int tmp = nums[i];
                    int j;
                    for (j = i; j - delta >= 0 && nums[j - delta] > tmp; j -= delta) {
                        nums[j] = nums[j - delta];
                    }
                    nums[j] = tmp;
                }
            }
        }
    
        return nums;
    }
    
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值