前言
本篇文章中以C语言实现相关代码演示;
在数据结构的学习过程中,C语言的指针、结构体、动态内存管理对数据结构的学习较为重要。
第一版
一、数据结构介绍
1.1. 什么是数据结构
- 数据结构是计算机存储、组织数据的方式。 是指相互之间存在的一种特定关系的数据元素集合。在内存中管理和存储数据的一种方法
1.2. 什么是算法
- 算法是一类计算过程的统称。 它是一系列的计算步骤,用来将输入数据转化成输出结果。处理数据的一种方法
1.3. 数据结构与算法的的意义
- 通过对数据结构和算法的处理以优化编程中的代码。
二、算法的时间复杂度和空间复杂度
2.1. 算法效率
2.1.1. 算法复杂度的定义
- 算法在编写成可执行的程序后,在运行是会耗费时间和内存空间。因此判断一个算法的效率要从时间和空间两个角度来考虑。
2.1.2. 算法复杂度的种类
- 算法复杂度可分为时间复杂度和空间复杂度两类。
时间复杂度:主要衡量一个算法运行的快慢;
空间复杂度:主要衡量一个算法运行所需要的额外的空间。
2.2. 时间复杂度
2.2.1. 时间复杂度的概念
- 算法的时间复杂度是一个类似于数学表达式的函数,它定义了运行一个算法的时间。 但是具体所消耗的时间需要在计算机运行时测试出来。为了简化计算分析,我们可以通过计算算法中的基本操作次数来确定时间复杂度。
一个算法所花费的时间与其中语句的执行次数呈正比例关系。
2.2.2. 大O的渐进表示法
(1) 引例:计算Func1的时间复杂度
(2) 大O渐进表示法的定义
- 大O渐进表示法是一种对算法进行估算的方法。 用以显示相对的时间复杂度。
O
:用于描述渐进行为的数学符号。
(3) 大O渐进表示法的表示规则
- 常数用 1 替;
用常数1取代运行时间中的所有加法常数。 - 仅留最高阶;
在修改的运行次数的函数中,只保留最高阶项。 - 系数可去掉;
如果最高阶项存在且不是1,则去除与这个项相乘的参数。
那么在上例中,Func1的时间复杂度用大O表示法可表示为:O(N2)
- 上例 F(N)=N2+2N+10 那么我们可知道 N2 对结果的影响最大,那么在用大O渐进表示法是仅保留影响最大项即可。
大O渐进表示法去掉了那些对结果影响不大的项,简明的表示出了执行次数。
(4) 时间复杂第的基本情况
- 最好情况,任意输入规模的最小运行次数(下界);
- 平均情况,任意输入规模的期望运行次数;
- 最坏情况,任意输入规模的最大运行次数(上界)。
在实际中,一般重点关注算法的最坏运行情况!
关注最坏运行情况是通过降低预期以实现结果的稳定。(底线思维、保守思维)
2.2.3 时间复杂度计算的示例及讲解
在示例的讲解中,会讲解到大O渐进表示法表示规则的原理
(1) 计算Func2的时间复杂度
(2) 计算Func3的时间复杂度
(3) 计算Func4的时间复杂度
(4) 计算strchr的时间复杂度
(5) 计算BubbleSort的时间复杂度
快速排序的时间复杂度:O( N log2N ).
(6) 计算BinarySearch的时间复杂度
顺序查找的时间复杂度:O( N ).
时间复杂度并不是循环的次数,而是要关注程序的设计思路,经过分析后得出相应的时间复杂度。
(7) 计算阶乘递归Fac的时间复杂度
(8) 计算斐波那契递归Fib的时间复杂度
2.3 空间复杂度
2.3.1 相关概念
- 空间复杂度是一个数学表达式,是对一个算法在运行过程中临时所占用的额外的空间的大小数量。
- 空间复杂度和时间复杂度一样,也使用大O渐进表示法描述。
- 函数运行时所需要的栈空间在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时申请的额外空间来确定的。
2.3.2 空间复杂度计算的示例及讲解
(1) 计算BubbleSort的空间复杂度
(2) 计算Fibonacci的空间复杂度
(3) 计算阶乘递归Fac的空间复杂度
时间是一去不复返的,空间是可以重复利用的;
时间是累计计算的,空间则不会累计计算。
二、线性表
- 线性表是n个具有相同特性的数据元素的有限序列。线性表在逻辑上是线性结构,但物理存储时,通常以数组和链式结构进行存储。
- 常见的线性表有:顺序表、链表、栈、队列、字符串……
2.1 顺序表
2.1.1 相关概念
顺序表是一段物理地址连续的存储单元一次存储数据元素的线性结构,一般采用数组来进行存储。
顺序表一般分为静态顺序表和动态顺序表。
2.1.2 顺序表实现
相关头文件的包含:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
(1) 静态顺序表的创建
静态顺序表:空间固定,无法扩容的存储结构。
# define N 7 //顺序表的存储空间容量大小
typedef int SLDataType; //重定义数据类型的表示方式,便于以后更改
typedef struct SeqList{ //创建静态顺序表
SLDataType array[N]; //创建顺序表的存储空间大小
size_t size; //表示当前顺序表有效数据的个数
}SeqList; //结构重命名,方便后期使用
(2) 动态顺序表的创建
动态顺序表:空间可以不断扩容增加。
【提示】下文的操作全是对动态顺序表进行的。
typedef int SLDataType; //重定义数据类型的表示方式,便于又更改
typedef struct SeqList{ //创建动态顺序表
SLDataType* array; //创建一个动态开辟的数据作为顺序表的主题
size_t size; //表示当前顺序表有效数据的个数
size_t capicity; //表示当前顺序表的容量空间大小
}SeqList; //结构重命名,方便后期使用
(3) 顺序表的初始化
void SLInit(SL* psl) //通过转递指针,对顺序表的整体进行修改
{
assert(psl);
psl->a = (SLDatatype*)malloc(sizeof(SLDatatype) * 4); //为顺序表开辟初始空间
if (psl->a == NULL) //判断空间是否开辟成功
{
perror("malloc fail");
return;
}
psl->capacity = 4; //记录初始化后空间的大小为 4
psl->size = 0; //记录初始化后空间的有效数据为 0
}
(4) 顺序表的容量检查
void SLCheckCapacity(SL* psl) //通过转递指针,对顺序表的整体进行修改
{
assert(psl);
if (psl->size == psl->capacity) //判断当前的空间大小是否和有效数据的个数相等,若相等则开辟空间为原来的 2 倍
{
SLDatatype* tmp = (SLDatatype*)realloc(psl->a, sizeof(SLDatatype) * psl->capacity * 2); //新的空间大小为原有大小的2倍
if (tmp == NULL) //判断空间是否开辟成功
{
perror("realloc fail");
return;
}
psl->a = tmp; //将创建的空间交给顺序表
psl->capacity *= 2; //更新扩容后的空间大小
}
}
(5) 顺序表的尾插
void SLPushBack(SL* psl, SLDatatype x) //设置插入的顺序表和插入的数据
{
//基本实现逻辑
SLCheckCapacity(psl); //判断顺序表的空间是否足够
psl->a[psl->size++] = x //将数据存入顺序表a的size++位置处
//借助“顺序表在任意位置插入元素函数”的实现逻辑
assert(psl);
SLInsert(psl, psl->size, x);
}
(6) 顺序表的尾删
void SLPopBack(SL* psl)
{
//基本实现逻辑
assert(psl->size > 0);
psl->size--; //直接将记录的有效数据个数减一即可
//借助“顺序表在任意位置删除元素”的实现逻辑
SLErase(psl, psl->size - 1);
}
数据的删除本质上是更改数据所在空间的控制权。
(7) 顺序表的头插
void SLPushFront(SL* psl, SLDatatype x)
{
//基本实现逻辑
assert(psl);
SLCheckCapacity(psl); //判断顺序表空间是否足够
//挪动数据
int end = psl->size - 1; //记录顺序表最后一个元素的坐标
while (end >= 0) //通过循环“自右向左”使数据依次向前移动一个位置
{
psl->a[end + 1] = psl->a[end]; //将左边的数据交给右边的位置
--end; //更新还没有被移动元素的坐标
}
//头部插入数据
psl->a[0] = x; //在顺序表的头部进行数据插入
psl->size++; //更新顺序表有效数据的个数
//借助“顺序表在任意位置插入元素函数”的实现逻辑
assert(psl);
SLInsert(psl, 0, x);
}
(8) 顺序表的头删
void SLPopFront(SL* psl)
{
//基本实现逻辑
assert(psl->size > 0);
int start = 1; //记录顺序表第二个元素坐标
while (start < psl->size) //自左向右将后一个数据赋值给前一个元素
{
psl->a[start-1] = psl->a[start]; //将后一个元素坐标赋值给前一个元素坐标处
start++; //操作向右前进一个
}
psl->size--; //更新删除后的有效数据个数
//借助“顺序表在任意位置删除元素”的实现逻辑
assert(psl);
SLErase(psl, 0);
}
(9) 顺序表的查找
int SLFind(SL* psl, SLDatatype x) //查找的顺序表和所查找的数据信息
{
assert(psl);
for (int i = 0; i < psl->size; i++) //自左向右遍历顺序表
{
if (psl->a[i] == x) //判断顺序表的每个坐标出是否有与查找信息相同的数据,有则返回数据的坐标
{
return i;
}
}
//遍历完整个顺序表后,若无相应数据,则发返回-1
return -1;
}
只能查找相应元素首次出现的位置坐标
(10) 顺序表在任意位置插入元素
void SLInsert(SL* psl, int pos, SLDatatype x) //要操作的顺序表、插入的位置、插入的数据
{
assert(psl);
assert(0 <= pos && pos <= psl->size);
SLCheckCapacity(psl); //检查顺序表的容量
int end = psl->size - 1; //记录顺序表最后一个数据的坐标
while (end >= pos) //将pos坐标右边的所有数据自左向右进行移动
{
psl->a[end + 1] = psl->a[end]; //将左边的数据拷贝给右边的相邻位置
--end; //更新下一个要移动的数据的坐标
}
psl->a[pos] = x; //将数据插入至顺序表的pos坐标处
psl->size++; //更新顺序表的有效数据的个数
}
(11) 顺序表在任意位置删除元素
void SLErase(SL* psl, int pos) //要操作的顺序表,删除的坐标位置
{
assert(psl);
assert(0 <= pos && pos < psl->size);
int start = pos + 1; //记录要自右向左移动的起始坐标
while (start < psl->size) //将pos坐标右边的数据自右向左进行移动
{
psl->a[start - 1] = psl->a[start]; //将右边的数据拷贝给左边的相邻位置
++start; //更新下一个要移动的数据的坐标
}
psl->size--; //更新顺序表的有效数据的个数
}
(12) 顺序表的遍历(打印)
void SLPrint(SL* psl)
{
assert(psl);
for (int i = 0; i < psl->size; i++) //自左向右遍历顺序表
{
printf("%d ", psl->a[i]); //打印顺序表每个坐标处的数据
}
}
(13) 顺序表的修改
void SLModify(SL* psl, int pos, SLDatatype x) //要操作的顺序表、修改的位置、修改后的数据
{
assert(psl);
assert(0 <= pos && pos < psl->size);
psl->a[pos] = x; //直接对顺序表相应坐标的数据进行修改
}
(13) 顺序表的销毁
void SLDestroy(SL* psl)
{
assert(psl);
free(psl->a); //释放顺序表的内存空间
psl->a = NULL; //将顺序表的指针定义为空,避免野指针
psl->size = 0; //将有效个数归0
psl->capacity = 0; //将顺序表的空间大小归0
}
2.2 链表
2.2.1 相关概念
(1) 基本概念
链表是一种物理结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。
(2) 链表的种类
头: 哨兵位。
(2) 图示
2.2.2 单向无头非循环链表
单向无头非循环链表(下文简称“单链表”)是一种结构简单,一般不会单独用来存储数据的结构。在实际中更多作为其他结构的子结构,如哈希桶、图的连接表……
相关头文件的包含:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
(1) 单链表的创建
typedef int SLTDataType; //定义数据类型,方便以后全局的修改
typedef struct SListNode //创建一个单链表
{
SLTDataType data; //创建单链表中存储数据的变量
struct SListNode* next; //单链表中存放下一个节点地址的的变量
}SLTNode; //
(2) 单链表节点的动态申请与创建
SLTNode* BuyLTNode(SLTDataType x) //创建新节点时,同时插入需要存储的数据
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); //创建一个单链表的节点newnode
if (newnode == NULL) //判断单链表的节点是否创建成功
{
perror("malloc fail");
return NULL;
}
newnode->data = x; //为新建链表的数据部分插入数据
newnode->next = NULL; //为新建链表存储下一个节点地址出设置为NULL
return newnode; //返回新创建节点的地址
}
(3) 单链表的遍历(打印)
void SLTPrint(SLTNode* phead) //传入单链表收个节点的地址
{
SLTNode* cur = phead; //创建一个cur,用来记录单链表在遍历的时候指针的变换
while (cur != NULL) //遍历整个链表。当cur存储的不是最后一个节点所存储下一个节点的地址时,,则可以继续遍历整个链表
{
printf("%d->", cur->data); //打印当前地址所指向的节点的数据
cur = cur->next; //将当前节点内存储的下一个节点地址提取出来,并将其投入到循环变量中再次遍历链表
}
printf("NULL\n"); //打印NULL表示该单链表打印结束(该部分可以删去)
}
(4) 单链表的尾插
void SLPushBack(SLTNode** pphead, SLTDataType x) //传入链表的首节点地址,需要插入的数据
{
assert(pphead);
SLTNode* newnode = BuyLTNode(x);//创建一个新的插入节点,记录其相应的地址
//判断当前链表的空间属性
if (*pphead == NULL) //若该链表为空链表
{
*pphead = newnode; //若为空链表,则将创建的节点设置为首节点
}
else //若该链表为非空链表
{
SLTNode* tail = *pphead; //创建一个tail变量,首先记录链表首节点的地址,用来存储链表遍历时的地址。
while (tail->next != NULL) //寻找到链表最后一个节点。
{
tail = tail->next; //用当前tail中所存储下一个节点的地址找到下一个节点,并依次循环
}
tail->next = newnode; //将newnode的地址拷贝到原链表最后一个节点所存储下一个节点地址的变量中,完成链表与新节点的连接。
}
}
(5) 单链表的头插
void SLPushFront(SLTNode** pphead, SLTDataType x) //传入链表的首节点地址,需要插入的数据
{
assert(pphead); // 链表为空,pphead也不为空,因为他是头指针plist的地址
SLTNode* newnode = BuyLTNode(x); //创建一个新节点,并记录这个新节点的地址
newnode->next = *pphead; //将原链表的首节点地址拷贝到新节点存储下一个节点地址的地方
*pphead = newnode; //将链表首节点的地址更新为新创建的节点地址
}
(6) 单链表的尾删
void SLPopBack(SLTNode** pphead) //传入链表的首节点地址
{
assert(pphead);
assert(*pphead);
//判断链表的容量属性
if ((*pphead)->next == NULL) //判断链表是否只有一个节点
{
free(*pphead); //释放首个节点的内存空间
*pphead = NULL; //将原来存储首节点的指针定义为NULL,避免野指针
}
else //判断链表的节点数是否大于等于2个
{
SLTNode* tail = *pphead; //通过tail临时存储各节点的地址
while (tail->next->next != NULL) //借助tail,找到链表的倒数第二个节点。当tail->next->next == NULL时,tail->next为最后一个节点的地址,tail为倒数第二个节点的地址。
{
tail = tail->next; //用当前tail中所存储下一个节点的地址找到下一个节点,并依次循环
}
free(tail->next); //释放链表最后一个节点的内存空间
tail->next = NULL; //将指向链表最后一个节点的指针设置为NULL,避免野指针
}
}
(7) 单链表的头删
void SLPopFront(SLTNode** pphead) //传入链表首节点的地址
{
assert(pphead);
assert(*pphead);
SLTNode* del = *pphead; //设置del临时存储链表首个节点的地址
*pphead = (*pphead)->next; //将链表第二个节点的地址拷贝给存储链表首节点的变量处,完成对链表首节点信息的更新
free(del); //释放原链表首节点所占用的内存空间
}
(8) 单链表的查找
SLTNode* STFind(SLTNode* phead, SLTDataType x) //传入链表首节点的地址,需要查找的数据
{
SLTNode* cur = phead; //通过cur临时存储节点的地址
while (cur != NULL) //当cur记录的是节点地址时,持续遍历整个链表
{
if (cur->data == x) //若当前链表存储的数据与所查找的数据相同
{
return cur; //则返回当前链表的地址信息
}
cur = cur->next; //用当前cur中所存储下一个节点的地址找到下一个节点,并依次循环
}
return NULL; //若没有查找到相应的数据,则返回为NULL
}
(9) 单链表插入指定位置之前的值
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) //传入链表首节点的地址,要插入的位置,要插入的信息
{
assert(pphead);
assert(pos);
if (*pphead == pos) //判断是否是在链表头部插入
{
SLPushFront(pphead, x); //则使用头插函数即可完成
}
else
{
SLTNode* prev = *pphead; //创建prev变量,临时存储节点的地址
while (prev->next != pos) //通过prev->next找到pos位置的节点,则prev存储的是pos位置的前一个节点
{
prev = prev->next; //用当前prev中所存储下一个节点的地址找到下一个节点,并依次循环
}
SLTNode* newnode = BuyLTNode(x); //创建一个节点,并对节点数据进行设置
prev->next = newnode; //将prev出的下一个节点地址信息与newnode的地址信息相关联
newnode->next = pos; //将newnode的下一个节点信息与pos的地址信息相关联
}
}
(10) 单链表插入指定位置之后的值
void SLInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuyLTNode(x); //创建一个新节点,并传入相应的数据
newnode->next = pos->next; //将pos的下一个节点的地址信息拷贝给newnode所记录的下一个节点地址信息处
pos->next = newnode; //将newnode的地址信息拷贝至pos所记录下一个节点的地址信息处
}
(11) 单链表删除指定位置之前的值
void SLErase(SLTNode** pphead, SLTNode* pos) //传入要删除的链表首个节点,要删除节点的位置
{
assert(pphead);
assert(pos);
if (pos == *pphead) //判断要删除的位置是否是链表的第一个节点
{
SLPopFront(pphead); //是,则通过调用单链表的头删函数来实现删除
}
else //若不是,则进行下列操作
{
SLTNode* prev = *pphead; //将pphead的地址拷贝给prev
while (prev->next != pos) //找到pos节点前的一个节点
{
prev = prev->next; //用当前prev中所存储下一个节点的地址找到下一个节点,并依次循环
}
prev->next = pos->next; //将pos后一个节点的地址拷贝给prev节点中存放下一个节点地址的变量中
free(pos); //释放pos节点所占用的内存空间
}
}
(12) 单链表删除指定位置之后的值
void SLEraseAfter(SLTNode* pos) //输入要删除节点的位置
{
assert(pos);
assert(pos->next);
SLTNode* next = pos->next; //将pos节点的下一个节点的地址拷贝给next
pos->next = next->next; //将pos后的第二个节点的地址传递给pos节点记录下一个节点的地址处
free(next); //释放next节点所占用的内存空间
}
(13) 单链表的销毁
void SLDestory(SLTNode** pphead) //输入要删除链表的头结点指针地址
{
assert(pphead);
SLTNode* cur = *pphead; //找到第一个节点,并存储在cur中
while(cur) //只要cur不为空,循环持续进行
{
SLTNode* next = cur->next; //用next存储当前节点的下一个节点的地址
free(cur); //释放当前节点的内存信息
cur = next; //将先前存储的下一个节点的信息拷贝给cur,进行循环
}
*pphead = NULL; //循环结束后,将头指针设置为空,避免野指针
}
2.2.3 双向带头循环链表
双向带头循环链表(下文简称“双链表”)是一种结构最复杂,,一般用于单独存储数据。实际中使用的链表数据结构都是带头的双向循环链表。双链表的结构虽然复杂,,但是使用代码实现以后会发现其结构的诸多优势,实现双链表反而简单起来。
相关头文件的包含:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
(1) 双链表的创建
typedef int LTDataType; //定义数据类型,方便日后对数据类型的修改
typedef struct ListNode //创建双链表
{
struct ListNode* next; //双链表当前节点前一个节点的地址
struct ListNode* prev; //双链表当前节点后一个节点的地址
LTDataType data; //双链表节点存储的数据
}LTNode; //双链表的重命名
(2) 双链表节点的动态申请与创建
LTNode* BuyLTNode(LTDataType x) //双链表的创建
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode)); //动态申请一个节点,其大小为LTNode(一个节点的大小)
if (newnode == NULL) //判断节点是否创建成功
{
perror("malloc fail");
return NULL;
}
newnode->data = x; //为创建的节点输入相应的数据
newnode->next = NULL; //将该节点的下一个节点地址置为空
newnode->prev = NULL; //将该节点的上一个节点地址置为空
return newnode; //返回创建节点的地址信息
}
(3) 双链表头节点(哨兵位)的创建
LTNode* LTInit() //创建双链表的头节点(哨兵位)
{
LTNode* phead = BuyLTNode(-1); //创建一个节点,其赋值可以为任意值
phead->next = phead; //将该节点的下一个地址设置为当前节点的地址
phead->prev = phead; //将该节点的上一个地址设置为当前节点的地址
return phead; //返回头节点(哨兵位)的坐标
}
(4) 双链表的遍历(打印)
void LTPrint(LTNode* phead) //双链表的遍历(打印),传入哨兵位的地址
{
assert(phead); //判断哨兵位是否为空
printf("guard<==>"); //打印哨兵位的标记,便于遍历展示
LTNode* cur = phead->next; //创建一个cur用于存储哨兵位下一个节点的地址
while (cur != phead) //遍历循环整个链表,当cur的地址与哨兵位的地址不相同时,遍历持续;若地址相同,则说明一轮循环完成
{
printf("%d<==>", cur->data); //打印出cur内所存放的数据
cur = cur->next; //将cur的下一个节点的地址赋值给cur,促使循环继续
}
printf("\n"); //换行
}
(5) 双链表的判空
bool LTEmpty(LTNode* phead) //通过布尔值,判断双链表是否为空
{
assert(phead); //判断哨兵位是否为空
return phead->next == phead; //若哨兵位的下一个节点地址指向的为自身,则该双链表为空;反之,则不为空
}
(6) 双链表的尾插
void LTPushBack(LTNode* phead, LTDataType x) //双链表的尾插,传入哨兵位和数据的值
{
assert(phead); //判断哨兵位是否为空
//①
LTNode* tail = phead->prev; //创建tail,存储哨兵位的前一个节点的地址,即双链表未节点的地址
LTNode* newnode = BuyLTNode(x); //创建一个要插入双链表中的节点
//②
tail->next = newnode; //将tail(尾节点)的下一个节点指向为新建节点的地址
newnode->prev = tail; //将新建节点前一个节点指向为tail(原尾节点)的地址
newnode->next = phead; //将新建节点下一个节点指向为phead(哨兵位)的地址
phead->prev = newnode; //将哨兵位的前一个节点指向为新建节点的地址
//通过 双链表在指定位置的前面的插入 函数实现
LTInsert(phead, x);
}
(7) 双链表的头插
void LTPushFront(LTNode* phead, LTDataType x) //双链表的头插,传入哨兵位的地址和插入的数据
{
assert(phead); //判断哨兵位是否为空
//①
LTNode* newnode = BuyLTNode(x); //创建一个要插入双链表的节点
LTNode* first = phead->next; //通过first记录当前链表第一个节点(非哨兵位)的地址
//②
phead->next = newnode; //将哨兵位下一个节点的指向为新建节点的地址
newnode->prev = phead; //将新建节点的前一个节点指向为哨兵位的地址
newnode->next = first; //将新建节点的下一个节点指向为first(原链表第一个节点)的地址
first->prev = newnode; //将first(原链表第一个节点)的前一个节点指向为新建节点的地址
//通过 双链表在指定位置的前面的插入 函数实现
LTInsert(phead->next, x);
}
(8) 双链表的尾删
void LTPopBack(LTNode* phead) //双链表的尾删,传入哨兵位的地址
{
assert(phead); //判断哨兵位是否为空
assert(!LTEmpty(phead)); //判断链表是否为空
LTNode* tail = phead->prev; //创建tail,存储链表尾节点的地址
LTNode* tailPrev = tail->prev; //创建tailPrev,存储链表倒数第二个节点的地址
free(tail); //释放tail(原链表尾节点)的内存空间
tailPrev->next = phead; //将tailPrev(原链表倒数第二个节点)的下一个节点指向为哨兵位的地址
phead->prev = tailPrev; //将哨兵位的前一个节点指向为tailPrev(原链表倒数第二个节点)的地址
//通过 双链表删除指定位置的值 函数实现
LTErase(phead->prev);
}
(9) 双链表的头删
void LTPopFront(LTNode* phead) //双链表的头删,传入哨兵位的地址
{
assert(phead); //判断哨兵位是否为空
assert(!LTEmpty(phead)); //判断链表是否为空
//①
LTNode* first = phead->next; //创建first,存储链表第一个节点的地址
LTNode* second = first->next; //创建second,存储链表第二个节点的地址
//②
phead->next = second; //将哨兵位的下一个节点指向为second(原链表第二个节点)的地址
second->prev = phead; //将second(原链表第二个节点)的上一个节点指向为哨兵位的地址
free(first); //释放first(原链表第一个节点)的内存空间
//通过 双链表删除指定位置的值 函数实现
LTErase(phead->next);
}
(10) 双链表的查找
LTNode* LTFind(LTNode* phead, LTDataType x) //双链表的查找,输入哨兵位的地址和要查找的值
{
assert(phead); //判断哨兵位是否为空
LTNode* cur = phead->next; //创建cur,存储链表第一个节点的地址
while (cur != phead) //遍历循环整个链表,当cur的地址与哨兵位的地址不相同时,遍历持续;若地址相同,则说明一轮循环完成
{
if (cur->data == x) //判断cur当前指向节点的数据是否与所查找的数据相同
{
return cur; //相同则返回当前节点的地址
}
cur = cur->next; //不同,则cur指向当前节点的下一个节点,循环继续
}
return NULL; //若遍历循环完未找到要查找的值,则返回为空
}
(11) 双链表在指定位置的前面的插入
void LTInsert(LTNode* pos, LTDataType x) //传入指定的位置和相应的数据
{
assert(pos); //判断指定的位置是否存在
//①
LTNode* prev = pos->prev; //创建prev,存储指定位置前一个节点的地址
LTNode* newnode = BuyLTNode(x); //创建一个要插入双链表的节点
//②
prev->next = newnode; //将prev(指定位置的前一个节点)的下一个节点指向为新建节点的地址
newnode->prev = prev; //将新建节点的前一个节点指向为prev(原链表指定位置的前一个节点)
newnode->next = pos; //将新建节点的下一个节点指向为pos(原链表的指定位置节点)
pos->prev = newnode; //将pos(原链表的指定位置节点)的前一个节点指向为新建节点的地址
}
(12) 双链表删除指定位置的值
void LTErase(LTNode* pos) //传入指定位置的地址
{
assert(pos); //判断指定位置是否为空
//①
LTNode* posPrev = pos->prev; //创建posPrev,存储指定位置前一个节点的地址
LTNode* posNext = pos->next; //创建posNext,存储指定位置下一个节点的地址
//②
posPrev->next = posNext; //将posPrev(原链表指定位置前一个节点)的下一个节点指向为posNext(原链表指定位置下一个节点)的地址
posNext->prev = posPrev; //将posNext(原链表指定位置下一个节点)的前一个节点指向为posPrev(原链表指定位置前一个节点)的地址
free(pos); //释放指定位置节点的内存空间
}
(13) 双链表的销毁
void LTDestroy(LTNode* phead) //传入哨兵位的地址
{
assert(phead); //判断哨兵位是否为空
//①
LTNode* cur = phead->next; //创建cur,存储链表第一个节点的地址
while (cur != phead) //遍历循环整个链表,当cur的地址与哨兵位的地址不相同时,遍历持续;若地址相同,则说明一轮循环完成
{
LTNode* next = cur->next; //创建next,存储当前节点指向下一个节点的地址
free(cur); //释放当前节点的内存空间
cur = next; //将next所记录的下一个节点的信息传递给cur,进行新一轮循环
}
//②
free(phead); //释放哨兵位的内存空间
}
2.3 顺序表和链表的比较
2.3.1 比较
比较方面 | 顺序表 | 链表(带头双向循环链表) |
---|---|---|
存储空间 | 物理上是连续的 | 逻辑上是连续的,物理上不一定连续 |
是否支持随机访问 | 支持 | 不支持 |
任意位置插入或删除元素的方式 | 可能需要搬移元素,效率较低 | 只需要修改指针的指向 |
存储容量 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素的高效存储和频繁访问 | 频繁的任意位置插入和删除 |
缓存利用率 | 高 | 低 |
2.3.2 各自的特点
(1) 链表
优点:
- 任意位置插入,时间复杂度为 O(1);
- 可以按需申请释放空间。
缺点:
- 不支持下标的随机访问;
- 缓存命中率低。
(2) 顺序表
优点:
- 尾插、尾删的效率更高;
- 支持下标随机访问;
- CPU缓存命中率更高。
缺点:
- 前面部分插入、删除数据,效率是 O(N);
- 空间不够时,需要扩容。
· 扩容会有代价
· 扩容会伴随着空间浪费
三、栈
3.1 相关概念及结构
- 栈是一种特殊的线性表,只允许在固定的一端进行插入和删除元素 。进行数据插入和删除的一端称为栈顶,另一端称为栈底。
- 栈中的数据遵从LIFO原则(即 先进后出,后进先出 )。
- 压栈:栈的插入操作,数据加入在栈顶。(也称为进栈、入栈)
- 出栈:栈的删除操作,数据出去在栈顶。
3.2 栈的实现
相关头文件的包含:
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
(1) 静态栈的创建
typedef int STDataType; //定义数据类型,方便日后对数据类型的修改
#define N 10 //使用宏定义栈的空间大小
typedef struct Stack //创建静态栈
{
SLDataType a[N]; //创建栈的数据存储空间
int top; //定义栈顶
}Stack; //栈的重命名,简化书写
(2) 动态栈的创建
typedef int STDataType; //定义数据结构,方便日后对数据类型的修改
typedef struct Stack //创建动态栈
{
STDataType* a; //创建栈存储数据的空间
int top; //定义栈顶
int capacity; //记录栈的容量
}ST; //栈的重命名,简化书写
(3) 栈的初始化
void STInit(ST* pst) //栈的初始化,传入栈的指针
{
assert(pst); //判断栈是否存在
pst->a = NULL; //将栈的数据存储设置为空
pst->top = 0; //将栈顶的位置记录为0,top指向栈顶的下一个元素位置
pst->capacity = 0; //将栈的额容量记录为0
}
- 若
top
初始化为-1
,则top
指向栈顶的元素;- 若
top
初始化为0
,则top
指向栈顶的下一个元素。
(4) 数据入栈
void STPush(ST* pst, STDataType x) //入栈,传入栈的指针和数据
{
//①判断栈的空间是否足够
if (pst->top == pst->capacity) //判断栈顶的位置与栈的容量是否相等,相等则说明栈的空间已满,需要扩容
{
//对栈的空间进行判断
//1.1 若栈的容量为0,则将栈的容量设置为4;
//1.2 若栈的容量不为0,则将栈的容量扩展为原来的2倍。
//1.3 创建变量newCapacity,记录扩容后的栈的容量
int newCapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newCapacity * sizeof(STDataType)); //动态申请栈的空间,且用tmp进行记录空间
if (tmp == NULL) //判断动态申请的空间是否创建成功
{
perror("realloc fail");
return;
}
pst->a = tmp; //将动态申请的数据存储空间传递给栈记录数据的位置
pst->capacity = newCapacity; //将新创建的容量信息交给栈的空间容量信息处
}
//②数据的入栈
pst->a[pst->top] = x; //将数据传递给栈中记录数据的位置,且该位置是top所指向的位置
pst->top++; //对栈顶的信息进行处理,方便下次入栈操作
}
(5) 数据出栈
void STPop(ST* pst) //出栈,传入栈的指针
{
assert(pst); //判断栈是否存在
assert(!STEmpty(pst)); //判断栈的数据是否为空
pst->top--; //栈顶元素减1,取消对原栈顶元素的记录即可
}
(6) 获取栈顶的元素
STDataType STTop(ST* pst) //传入栈的指针
{
assert(pst); //判断栈是否存在
assert(!STEmpty(pst)); //判断栈的数据是否为空
return pst->a[pst->top - 1]; //返回当前栈顶所指向的元素
}
(7) 获取栈中有效元素的个数
int STSize(ST* pst) //传入栈的指针
{
assert(pst); //判断栈是否存在
return pst->top; //返回栈顶数据,即有效元素的个数
}
(8) 判断栈是否为空
bool STEmpty(ST* pst) //传入栈的指针
{
assert(pst); //判断栈是否存在
return pst->top == 0; //判断栈顶记录信息是否为0。是0,则返回True;不是0,则返回False。
}
(9) 栈的销毁
void STDestroy(ST* pst) //传入栈的指针
{
assert(pst); //判断栈是否存在
free(pst->a); //释放栈的数据存储空间
pst->a = NULL; //将记录数据空间的指针设置为空
pst->top = pst->capacity = 0; //将栈顶信息和栈的容量均设置为0
}
四、队列
4.1 相关概念及结构
- 队列是只允许一端进行数据插入操作,另一端进行删除数据操作的特殊线性表。进行数据插入的一端称为队尾,进行数据删除的一端称为对头。
- 队列具有FIFO的原则(即先进先出,后进后出)
- 入队列:插入数据;
- 出队列:删除数据。
4.2 队列的实现
相关头文件的包含:
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
(1) 队列的创建
typedef int QDataType; //定义数据类型,方便日后对类型的修改
typedef struct QueueNode //创建一个队列的节点
{
struct QueueNode* next; //存储当前队列节点下一个节点的地址
QDataType data; //队列节点所存储的数据
}QNode; //队列节点的重命名,简化书写
typedef struct Queue //创建一个队列
{
QNode* phead; //记录队列头节点的地址
QNode* ptail; //记录队列尾节点的地址
int size; //记录队列的长度
}Queue; //队列的重命名,简化书写
(2) 队列的初始化
void QueueInit(Queue* pq) //传入要操作队列的指针
{
assert(pq); //判断队列是否为空
pq->phead = NULL; //将记录头节点的指针设置为空
pq->ptail = NULL; //将记录尾节点的指针设置为空
pq->size = 0; //将队列的长度设置为0
}
(3) 队尾的入队列
void QueuePush(Queue* pq, QDataType x) //传入要操作的队列和插入的数据
{
assert(pq); //判断队列是否存在
//①创建一个要插入的节点
QNode* newnode = (QNode*)malloc(sizeof(QNode)); //创建一个节点
if (newnode == NULL) //判断节点是否创建成功
{
perror("malloc fail\n");
return;
}
newnode->data = x; //将数据存入到新创建的节点中
newnode->next = NULL; //将新建节点记录下一个节点的指针设置为空
//②将该节点插入到队列中
//判断队列的节点个数
if (pq->ptail == NULL) //如果当前队列为一个空队列
{
assert(pq->phead == NULL); //判断当前队列记录头节点的指针是否为空。若为空,则说明是空队列;非空,则不是空队列。
pq->phead = pq->ptail = newnode; //将队列中记录头节点和尾节点的指针均指向新建节点的地址
}
else //如果当前队列不为空
{
pq->ptail->next = newnode; //将当前尾节点存放下一个节点地址的指针指向新建节点
pq->ptail = newnode; //将队列的尾指针指向新建节点
}
pq->size++; //队列的长度加一
}
(4) 队头的出队列
void QueuePop(Queue* pq) //传入要操作的队列
{
assert(pq); //判断队列是否存在
assert(!QueueEmpty(pq)); //判断队列是否为空
//判断队列节点的个数
if (pq->phead->next == NULL) //如果队列只有一个节点
{
free(pq->phead); //释放头节点指针所指向的节点,即队列的唯一一个节点
pq->phead = pq->ptail = NULL; //将队列中记录头节点和尾节点的指针设置为空
}
else //如果队列有多个节点
{
QNode* next = pq->phead->next; //用next记录原队列的第二个节点地址
free(pq->phead); //释放原队列的头节点内存空间
pq->phead = next; //将队列记录头节点的指针指向next(原队列的第二个节点)
}
pq->size--; //节点的个数减一
}
(5) 获取队列队头的数据
QDataType QueueFront(Queue* pq) //传入要操作的队列
{
assert(pq); //判断队列是否存在
assert(!QueueEmpty(pq)); //判断队列是否为空
return pq->phead->data; //返回队列头指针指向的节点中记录数据的值
}
(6) 获取队列队尾的数据
QDataType QueueBack(Queue* pq) //传入要操作的队列
{
assert(pq); //判断队列是否存在
assert(!QueueEmpty(pq)); //判断队列是否为空
return pq->ptail->data; //返回队列尾指针指向的节点中记录的数据
}
(7) 获取队列中有效元素的个数
int QueueSize(Queue* pq) //传入要操作的队列
{
assert(pq); //判断队列是否存在
return pq->size; //返回队列中记录队列长度的值
}
(8) 判断队列是否为空
bool QueueEmpty(Queue* pq) //传入要操作的队列
{
assert(pq); //判断队列是否存在
return pq->size == 0; //判断队列的长度是否为0。若为0,则为空;反之,非空。
}
(9) 队列的销毁
void QueueDestroy(Queue* pq) //传入要操作的队列
{
assert(pq); //判断队列是否存在
QNode* cur = pq->phead; //创建cur,记录原链表的头节点
while (cur) //循环遍历整个队列
{
QNode* next = cur->next; //创建next,记录当前节点的下一个节点
free(cur); //释放当前节点的内存空间
cur = next; //将next(原当前节点的下一个节点)的地址传递给cur,继续遍历
}
pq->phead = pq->ptail = NULL; //将队列的头指针和尾指针均设置为空
pq->size = 0; //将队列的长度设置为0
}
4.3 环形队列
4.3.1 相关概念及结构
环形队列是一种数据大小和空间固定的队列。环形队列的空间满了便不能插入数据,只有等删除数据给出空间后才能继续插入数据。
4.3.2 实现代码
(1) 循环队列的创建
typedef struct MyCircularQueue{ //创建环形队列的基本结构
int front; //记录环形队列的头下标
int rear; //记录环形队列的尾尾下标
int k; //设置环形队列的空间大小
int* a; //通过创建一个数组来实现环形队列
} MyCircularQueue; //结构重命名,简化书写
MyCircularQueue* myCircularQueueCreate(int k) { //创建环形队列的数据结构,传入环形队列所需的节点个数
MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue)); //创建一个环形队列所需要的内存空间
obj->a = (int*)malloc(sizeof(int) * (k+1)); //创建k+1个节点,用以记录环形队列k个节点的数据
obj->front = obj->rear = 0; //将环形队列的头下标和尾下标均设置为0
obj->k = k; //将环形队列的空间大小设置为k
return obj; //返回创建的环形队列
}
(2) 循环队列的判空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) { //传入要操作的环形队列
return obj->front == obj->rear; //如果环形队列的头下标和尾下标相等,则说明环形队列为空;反之,非空。
}
(3) 循环队列的判满
bool myCircularQueueIsFull(MyCircularQueue* obj) { //传入要操作的环形队列
return (obj->rear + 1) % (obj->k + 1) == obj->front; //如果环形队列的 尾下标+1 与 头下标 的数值相同,则说明该队列是满的
}
(4) 循环队列的入队列并且判断是否入队成功
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) { //传入要操作的环形队列和要插入的值
//①判断环形队列是否是满的,满的则无法插入
if(myCircularQueueIsFull(obj))
return false;
//②若环形队列不是满的,则开始插入数据
obj->a[obj->rear] = value; //将要插入的数据插入至环形队列尾节点下标指向的节点
obj->rear++; //环形队列的尾下标加一
//③对尾下标的值做出调整,使其仍在环形队列的有效下标范围内
obj->rear %= (obj->k + 1);
//④插入成功,返回true
return true;
}
(5) 循环队列的出队列并且判断是否出队成功
bool myCircularQueueDeQueue(MyCircularQueue* obj) { //传入要操作的环形队列
//判断环形队列是否为空,空的则不用删除
if(myCircularQueueIsEmpty(obj))
return false;
//②对循环队列的数据进行删除
obj->front++; //循环队列的头节点下标向后加一(前进一个节点)
obj->front %= (obj->k + 1); //对队列头节点的下标进行调整,使其仍在环形队列的有效下标范围内
//④删除成功,返回true
return true;
}
(6) 获取循环队列的队头元素
int myCircularQueueFront(MyCircularQueue* obj) { //传入要操作的循环队列
if(myCircularQueueIsEmpty(obj)) //判断循环队列是否尾空
return -1;
return obj->a[obj->front]; //返回循环队列头节点下标所指向的节点
}
(7) 获取循环队列的队尾数据
int myCircularQueueRear(MyCircularQueue* obj) {
if(myCircularQueueIsEmpty(obj)) //判断循环队列是否为空
return -1;
return obj->a[(obj->rear + obj->k) % (obj->k + 1)]; //返回循环队列有效队尾下标所指向的节点
}
(8) 循环队列的销毁
void myCircularQueueFree(MyCircularQueue* obj) { //传入要操作的循环队列
free(obj->a); //释放循环队列存储数据的内存空间
free(obj); //释放循环队列结构的内存空间
}
五、树
5.1 树的概念及结构
5.1.1 树的概念
树是一种非线性的数据结构。树是由n( n>=0 )个有限节点组成的一个具有层次关系的结合。
之所以称该结构为树,是因为该结构看起来像一个倒挂的树。也就是说该结构的根是在上面,叶子在下面。
- 根节点: 没有前驱节点的节点,即无父节点。
- 除根节点外,其余节点被分为N( N>0 )个互不相交的集合(T1, T2, … , Tn),其中每个集合Ti( 1<=i<=m )又是一棵结构与树类似的子树。每个子树的根节点有且只有一个前驱节点,可以有0个或多个后继结点。
- 树是递归定义的。
- 树形结构中,子树之间不能有交集,否则就不是树形结构。
5.1.2 树的关系
树的关系是通过人类亲缘关系来描述的。
- 节点的度: 一个节点含有子树的个数称为该节点的度。上图中,A节点的度为6。
- 叶节点(终端节点): 度为0的节点称为叶节点。上图中,叶节点有B, C, H, I, P, Q, K, L, M, N。
- 分支节点(非终端节点): 度不为0的节点。上图中,分支节点有D, E, J, F,G。
- 双亲节点(父节点): 若一个节点含有子节点,则这个节点称为其子节点的额父节点。上图中,A是B的父节点。
- 孩子节点(子节点): 一个节点含有子树的根节点称为该节点的子节点。上图中,B是A的子节点。
- 兄弟节点: 具有相同父节点的节点互称为兄弟节点。上图中,B和C是兄弟节点。
- 树的度: 一棵树中,最大的节点的度称为树的度。上图中,树的度为4。
- 节点的层次: 从根开始定义,由1开始以此类推。上图中,A为第一层;B, C, … F, G 为第二层;H, I, … M, N为第三层;P, Q 为第四层。
- 树的高度(树的深度): 树中节点的最大层次。上图中,树的高度为4.
- 堂兄弟节点: 双亲在同一层的节点互为堂兄弟节点。上图中,H, I互为堂兄弟节点。
- 节点的祖先: 从根到该节点所经过的分支上的所有节点,上图中,A是所有节点的祖先。
- 子孙: 以某节点为根的子树中任意节点都称为该节点的子孙。上图中,所有节点都是A节点的子孙。
- 森林: 由m课互不相交的树的集合称为森林。
5.1.3 树的结构
树的几种表示方式:
- 如果明确了树的度,直接按度的数据进行定义即可。
- 通过顺序表来存储孩子。
- 双亲表示法,即每个位置只存双亲的指针或下标。
- 孩子兄弟表示法。
下面通过孩子兄弟表示法进行展示。
(1) 结构代码
typedef int DataType;
struct Node
{
struct Node* _firstChild; //节点的第一个孩子地址
struct Node* _pNextBrother; //节点的下一个兄弟地址
DataType _data; //节点所存储的数据
};
(2) 逻辑结构展示
5.1.4 树的应用
-
Linux的树状目录结构:
-
Windows文件系统结构为森林结构,每个树的根节点为其盘符。
5.2 二叉树的概念及结构
5.2.1 二叉树的概念
一棵二叉树的节点是一个有限集合,其满足:
1)集合为空;
2)由一个根节点加上两棵分别称为左子树和右子树的二叉树组成。
一个二叉树不存在度大于2的节点;
二叉树的子树由左右之分,次序不能颠倒。一次二叉树是有序树。
对于任意的二叉树都是由一下几种情况复合而成的。
二叉树的应用:
- 堆、选数、搜索树、AVL树、红黑树。
5.2.2 特殊的二叉树
(1) 满二叉树
- 一个二叉树,如果每一个层的节点数都达到最大值,则这个二叉树为满二叉树。
- 如果一个满二叉树有k层,则它的节点总数为2k-1个。
(2) 完全二叉树
- 完全二叉树是一个效率很高的数据结构,完全二叉树由满二叉树变形而来。
1)完全二叉树的非最后一层都是满二叉树;
2)最后一层可以是不满的,但叶节点的顺序必须是自左到右连续的。 - 满二叉树也是完全二叉树。
对于深度为k的由n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树编号从1至n一一对应时称为完全二叉树。 - 若完全二叉树由k层,则节点的个数为 [2n-1, 2n-1]。
5.2.3 二叉树的性质
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2i-1 个节点。
- 若规定根节点的层数为1,则深度为h的二叉树的最大节点数是2h-1。
- 对任何一个二叉树,如果度为0其叶节点的个数为n0,度为2的分支节点个数为n2,则有n0=n2+1。
- 若规定根节点的层数为1,具有n个节点的满二叉树深度为h=log2(n+1)
- 对于具有n个节点的完全二叉树,如果按照从上至下、从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的节点有:
1)若i>0,i位置节点的双亲序号为==(i-1)/2==;若i=0,i为根节点的编号,则其无双亲节点。
2)若2i+1<n,左孩子序号为2i+1;若2i+1>=n,则无左孩子。
3)若2i+2<n,右孩子序号为2i+2;若2i+2>=n,则无左孩子。
5.2.4 二叉树的顺序存储结构
- 二叉树的顺序存储即用数组进行设计。 顺序结构存储就是使用数组存储。
- 一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。
- 在实际编码中,只有堆才会使用数组来进行设计。
- 二叉树的顺序存储在物理上是一个数组,在逻辑上是一个二叉树。
父子节点下标之间的关系:
- 左子节点下标 = 父节点坐标*2 + 1
- 右子节点下标 = 父节点坐标*2 + 1
- 父节点坐标 = (子节点坐标 - 1) / 2
5.2.5 二叉树的链式存储结构
- 二叉树的链式存储结构是指用一个链表表示一个二叉树。即用链来表示元素间的逻辑关系。
- 通常该链表的每个节点由三个域组成,即数据域、、左指针域和右指针域。
1)数据域:存储二叉树各节点所记录的数据。
2)左右指针:存储当前节点左右子节点的地址。 - 链式结构常分为二叉链和三叉链。
结构代码:
二叉链表:
typedef int BTDataType;
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; //当前节点的左子节点
struct BinTreeNode* _pRight; //当前节点的右子节点
BTDataType _data; //当前节点存储的数据
}
三叉链表:
typedef int BTDataType;
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; //当前节点的父节点
struct BinTreeNode* _pLeft; //当前节点的左子节点
struct BinTreeNode* _pRight; //当前节点的右子节点
BTDataType _data; //当前节点存储的数据
}
5.3 二叉树的顺序结构及实现
普通的二叉树并不适合用数组进行来存储,以为其可能会造成大量的空间浪费。在实际编码中,通常把堆使用顺序结构的数组来进行存储。数据结构的堆和操作系统虚拟进程地址空间的堆是两回事,后者则是操作系统中管理内存的一块区域分段。
5.3.1 堆的概念
- 如果有一个关键码的集合k={k0, k1, k2, … , kn-1},把它的所有元素按完全二叉树的顺序存储方式进行存储。在这一个个一维数组中,若满足Ki <= K2i+2 且 Ki <= K2i+2 (或 Ki >= K2i+2 且 Ki >= K2i+2) 其中i=0,1,…,则称为大堆(或 小堆)。
- 将根节点最大的堆叫做最大堆或大根堆。
- 将根节点最小的堆叫做最小堆或小根堆。
堆的性质:
- 堆中某个节点的值总是不大于(或 不小于)其父节点的值。
- 堆是一个完全二叉树。
5.3.2 堆的结构
5.3.3 向上/向下调整算法
(1) 向下调整算法
向下调整算法的基本思想(以建小堆为例):
- 从根结点处开始,选出左右孩子中值较小的孩子。
- 让小的孩子与其父亲进行比较。
2.1. 若小的子节点比父父节点还小,则该子节点与其父节点的位置进行交换。并将原子节点的位置当成父节点继续向下进行调整,直到调整到叶子结点为止。
2.2. 若小的子节点比父节点大,则不需处理了,调整完成,整个树已经是小堆了。
(2) 向上调整算法
向上调整算法的 基本思想 (以建小堆为例):
- 将目标结点与其父结点比较。
- 若目标结点的值比其父节点的值小,则交换目标节点与其父节点的位置,并将原目标节点的父节点当作新的目标节点继续进行向上调整。
5.3.4 堆的实现代码
相关头文件的包含
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
(1) 堆的定义
typedef int HPDataType;
typedef struct Heap //创建堆的结构
{
HPDataType* _a; //通过数组来实现堆
int _size; //记录有效数据的个数
int _capacity; //记录当前堆的总容量
}HP; //结构重命名,便于后续使用
(2) 堆的初始化
void HeapInit(HP* php)
{
assert(php); //判断堆是否创建成功
php->a = NULL; //将堆的数据存储置空
php->size = 0; //将堆的有效数据置0
php->capacity = 0; //将堆的容量置0
}
(3) 堆的销毁
void HeapDestroy(HP* php)
{
assert(php); //判断堆是否创建成功
free(php->a); //释放堆记录数据所占的内存空间
php->a = NULL; //将记录数据的指针置为空
php->capacity = php->capacity = 0; //将堆的有效数据个数和堆的容量置为0
}
(4) 堆节点的交换
void Swap(HPDataType* p1, HPDataType* p2)
{
HPDataType tmp = *p1; //创建tmp临时记录p1的内存信息
*p1 = *p2; //将p2的内存信息赋值给p1
*p2 = tmp; //将tmp存储的内存信息赋值给p2
}
(5) 堆的向上调整
void AdjustUp(HPDataType* a, int child)
{
int parent = (child - 1) / 2; //找到当前子结点的父节点
//向上调整
while (child > 0) //只要子节点在堆数组的有效区域内,则持续循环进行调整
{
if (a[child] > a[parent]) //判断子节点是否大于父节点
//若创建大堆:a[child] > a[parent]
//若创建小队:a[child] < a[parent]
{
Swap(&a[child], &a[parent]); //若符合所需求的父子大小关系,则交换父子节点的内容
//对父子节点的下标信息进行更新
child = parent; //当前父节点作为新的子节点
parent = (child - 1) / 2; //通过子节点寻找到父节点的下标
}
else
{
break; //若不符合,则说明当前堆无需再调整;即退出循环
}
}
}
(6) 堆的向下调整
void AdjustDown(int* a, int n, int parent) //传入 堆数组、堆数组的长度、父节点
{
int child = parent * 2 + 1; //找到当前父节点的子节点
//向下调整
while (child < n) //只要子节点在堆数组的有效区域内,则持续循环进行调整
{
//选出左右子节点较小(或较大)的一个
if (child + 1 < n //判断右节点是否存在
&& a[child + 1] > a[child]) //判断两节点的关系
//找较大节点:a[child + 1] > a[child]
//找较小节点:a[child + 1] < a[child]
{
++child; //找到最大的节点,即右节点;若不成立,则最大的节点为左节点
}
if (a[child] > a[parent]) //判断子节点是否大于父节点
//若创建大堆:a[child] > a[parent]
//若创建小队:a[child] < a[parent]
{
Swap(&a[parent], &a[child]); //若符合所需求的父子大小关系,则交换父子节点的内容
//对父子节点的下标信息进行更新
parent = child; //将当前子节点作为新的父节点
child = parent * 2 + 1; //找到新的父节点的子节点
}
else
{
break; //若不符合,则说明当前堆无需再调整;即退出循环
}
}
}
(7) 堆的插入
void HeapPush(HP* php, HPDataType x)
{
assert(php); //判断堆是否存在
//堆的扩容
if (php->size == php->capacity) //若有效数据个数等于堆的总容量,则需要先扩容
{
int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2; //若堆的容量为0,则将其设置为4;反之,则将新的容量设置为原容量的2倍
HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType)); //将堆数组的内存空间扩容至与容量相对应的大小
if (tmp == NULL) //判断扩容是否成功
{
perror("realloc fail");
return;
}
php->a = tmp; //将扩容的空间赋予堆数组记录数据的内存空间
php->capacity = newCapacity; //关系堆记录总容量的信息
}
//数据的插入
php->a[php->size] = x; //将数据插入到堆的末尾
php->size++; //更新堆的有效数据个数
//数据的向上调整
//直接插入不一定会构成堆,只有进行相关的调整后,新插入的数据才能和原来的数据再次形成一个堆
AdjustUp(php->a, php->size - 1);
}
(8) 堆的删除
删除堆顶的数据
void HeapPop(HP* php)
{
assert(php); //判断堆是否存在
assert(!HeapEmpty(php)); //判断堆是否为空
//删除堆顶的节点
Swap(&php->a[0], &php->a[php->size - 1]); //交换堆顶和堆未的元素
php->size--; //删除交换后堆尾的节点
//数据的向下调整
//将堆顶的新数据进行向下调整,使得所有数据构成一个正确的堆
AdjustDown(php->a, php->size, 0);
}
(9) 堆的顶部数据取出
HPDataType HeapTop(HP* php)
{
assert(php); //判断堆是否存在
assert(!HeapEmpty(php)); //判断堆是否为空
return php->a[0]; //返回堆顶的元素数据
}
(10) 堆的数据个数判断
int HeapSize(HP* php)
{
assert(php); //判断堆是否存在
return php->size; //返回堆的有效数据个数
}
(11) 堆的判空
bool HeapEmpty(HP* php)
{
assert(php); //判断堆是否存在
return php->size == 0; //判断堆的有效数据个数是否为0
//若是,则返回True;反之,返回False。
}
5.3.5 堆的应用
(1) 堆排序
堆排序即用堆的思想来进行排序,大致步骤总结为:
-
建堆
升序:建大堆
降序:减小堆 -
排序
以升序为例。
1)建大堆。
2)根节点和最后一个节点(n)交换;忽略最后一个节点,其余节点自根节点向下调整建出新的大堆。
3)根节点与次后一个节点(n-1)交换;忽略次最后一个节点,其余节点自根节点向下调整建出新的大堆。
4)重复上述步骤,直到建立出数据的升序即可结束。
代码示例:
void HeapSort(int* a, int n)
{
//建堆
for (int i = (n-1-1)/2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
//排序
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
(2) Top-K问题
Top-K问题,即在N个数中找出最大/最小的前K个元素。
一般思路:
把N个数建成大堆,随后删除K次。便找出前k个元素。但是,当N特别大时,便无法实现。如在10亿个整数中寻找前k个数。10亿个整数约40G,一般内存中无法存储,便在硬盘中存储。但堆只能在内存中进行创建。
最佳思路:
- 用前K个元素来建堆。
求前k个最大元素,建大堆;
求前k个最小元素,减小堆。 - 用剩余的N-K个元素依次与堆顶的元素进行比较,不满足条件的元素则替换堆顶元素。
- 剩余的元素一次与堆顶元素比较完后,堆内剩余的元素便是所求的前k个元素。
代码示例:
void PrintTopK(int k, int *a, int n)
{
//取出所有的N个数据
for (int i = 0,j = 0; i < k; i++,j++)
{
kminheap[i] = a[j];
}
// 求最小,建小堆
for (int i = (k-1-1)/2; i >= 0; i--)
{
AdjustDown(kminheap, k, i);
}
//比较后N-K个数与堆顶数的大小
int val = 0;
m = k + 1;
while (m<=n)
{
val = a[m]
if (val > kminheap[0])
{
kminheap[0] = val;
AdjustDown(kminheap, k, 0);
}
m++;
}
//输出前K个数
for (int i = 0; i < k; i++)
{
printf("%d ", kminheap[i]);
}
printf("\n");
}
5.4 二叉树的链式结构及实现
5.4.1 二叉树链式结构的创建
typedef int BTDataType;
typedef struct BinaryTreeNode
{
BTDataType data; //数据
struct BinaryTreeNode* left; //左子树链接
struct BinaryTreeNode* right; //右子树链接
}BTNode;
5.4.2 二叉树的遍历
(1) 前序遍历
基本概念:
访问根节点的操作发生在遍历其左右子树之前。也称为“先根遍历”。
遍历图示:
代码示例:
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
printf("%d ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
(2) 中序遍历
基本概念:
访问根节点的操作发生在遍历其左右子树之中。也称为“中根遍历”。
遍历图示:
代码示例:
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ", root->data);
InOrder(root->right);
}
(3) 后序遍历
基本概念:
访问根节点的操作发生在遍历其左右子树之后。也称为“后根遍历”。
遍历图示:
代码示例:
void AfterOrder(BTNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
AfterOrder(root->left);
AfterOrder(root->right);
printf("%d ", root->data);
}
(4) 层序遍历
基本概念:
层序遍历从所在二叉树的根节点出发,首先访问第一层树根节点,然后从左到右在第二次访问节点,接着在第三层访问……这种自上而下、自左向右逐层访问树的节点的过程便是层序遍历。
遍历图示:
代码示例:
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->data);
if(front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
QueueDestroy(&q);
}
5.4.3 二叉树的节点以及高度应用
(1) 求二叉树的节点个数
遍历计数法:
int size = 0;
void BTreeSize(BTNode* root)
{
if (root == NULL)
return;
++size;
BTreeSize(root->left);
BTreeSize(root->right);
}
递归计数法:
int BTreeSize(BTNode* root)
{
if (root == NULL)
return 0;
return BTreeSize(root->left)
+ BTreeSize(root->right)
+ 1;
}
(2) 求二叉树叶子节点的个数
int BTreeLeafSize(BTNode* root)
{
if (root == NULL)
{
return 0;
}
if (root->left == NULL
&& root->right == NULL)
{
return 1;
}
return BTreeLeafSize(root->left)
+ BTreeLeafSize(root->right);
}
(3) 求二叉树的高度
int BTreeHeight(BTNode* root)
{
if (root == NULL)
return 0;
int leftHeight = BTreeHeight(root->left);
int rightHeight = BTreeHeight(root->right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
(4) 求二叉树第k层节点的个数
int BTreeLevelKSize(BTNode* root, int k)
{
assert(k > 0);
if (root == NULL)
return 0;
if (k == 1)
return 1;
return BTreeLevelKSize(root->left, k - 1)
+ BTreeLevelKSize(root->right, k - 1);
}
(5) 二叉树查找值为x的节点
BTNode* BTreeFind(BTNode* root, BTDataType x)
{
if (root == NULL)
return NULL;
if (root->data == x)
return root;
BTNode* ret1 = BTreeFind(root->left, x);
if (ret1)
return ret1;
BTNode* ret2 = BTreeFind(root->right, x);
if (ret2)
return ret2;
return NULL;
}
5.4.4 二叉树的创建和销毁
(1) 二叉树前序遍历数组构建二叉树
void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
printf("%d ", front->data);
if(front->left)
QueuePush(&q, front->left);
if (front->right)
QueuePush(&q, front->right);
}
printf("\n");
QueueDestroy(&q);
}
(2) 二叉树的销毁
void BTreeDestory(BTNode* root)
{
if (root == NULL)
return;
BTreeDestory(root->left);
BTreeDestory(root->right);
free(root);
}
(3) 判断二叉树是否为完全二叉树
bool BTreeComplete(BTNode* root)
{
Queue q;
QueueInit(&q);
if (root)
QueuePush(&q, root);
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
// 遇到空就跳出
if (front == NULL)
break;
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
// 检查后面的节点有没有非空
// 有非空,不是完全二叉树
while (!QueueEmpty(&q))
{
BTNode* front = QueueFront(&q);
QueuePop(&q);
if (front)
{
QueueDestroy(&q);
return false;
}
}
QueueDestroy(&q);
return true;
}
六、排序
6.1 相关概念
排序,即使一串记录按照其中的某个或某些关键字的大小,递增或递减排列起来的操作。
排序的稳定性,若在待排序的记录序列中,存在多个具有相同关键字的记录,经过排序后,这些记录的相对次序保持不变。
即在原序列中,
r[i]=r[j]
,且r[i]
在r[j]
之前,经过排序后,r[i]
仍然在r[j]
之前。则称这种排序算法是稳定的;反之,则不稳定。
内部排序,数据元素全部在内存中的排序。
外部排序,数据元素过多不能同时存放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
6.2 直接插入排序
6.2.1 排序思路
将待排序的记录按其相关键码值的大小逐个插入到一个已经排好序的序列中去,直到所有的记录插入完为止,随后便得到一个新的有序序列。
当插入
第i(i>=1)个
元素时,前面的array[0]、array[1]、……、array[i-1]
已经排好序,此时用array[i]
的排序码与array[i-1]、array[i-2]、……
的排序码顺序进行比较,找到插入位置(即将array[i]插入,原来位置上的元素顺序后移。)
6.2.2 排序特征
- 时间复杂度:
最好:O(N)
最坏:O(N2)
元素集合越接近有序,直接插入排序算法的时间效率越高
- 空间复杂度:O(1)
- 稳定性:稳定。
6.2.3 实现代码
void InsertSort(int* a, int n)
{
for (int i = 1; i < n; ++i)
{
// [0, end] 有序,插入tmp依旧有序
int end = i-1;
int tmp = a[i];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
6.3 希尔排序
6.3.1 排序思路
希尔排序法也称为缩小增量法
先选定一个整数(gap),把待排序文件中所有记录分成多个组。 所有距离为该整数(gap)的记录分在同一组内,并对每一组内的记录进行直接插入排序。 然后缩小该整数,重复上述分组和排序的工作。 当整数(gap)到达1时,所有记录在统一组内排好序。
希尔排序可简述为:
- 预排序;
- 直接插入排序。
6.3.2 排序特征
时间复杂度:O(N1.3)
希尔排序的时间复杂度较难去计算,一般在O(N1.25)到O(16*N1.25)之间,一般认为是O(N1.3)。
- gap越大,大的数可以更快的到序列后面,小的数可以更快的到前面。
- gap越小,序列中数据移动的更慢,但是序列越接近有序。
- gap等于1时,便是直接插入排序。
稳定性:不稳定。(相同的数据可能被分到不同组)
6.3.3 实现代码
void ShellSort(int* a, int n)
{
// 1、gap > 1 预排序
// 2、gap == 1 直接插入排序
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1; // +1可以保证最后一次一定是1
// gap = gap / 2;
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
6.4 直接选择排序
6.4.1 排序思路
- 每次从待排序的数据元素中选出最大(或最小)的一个元素,存在序列的起始位置,直到全部待排序的数据元素排序完。
- 在元素集合
array[i]
至array[N-1]
中选择关键码最大(或最小)的数据元素; - 若该元素不是这组元素中的最后一个(或第一个)元素,则将该元素与这组元素的最后一个(第一个)元素交换;
- 在剩余的
array[i]
至array[n-2]
(或array[i+1]
至array[n-1]
)集合中,重复上述步骤,直到集合剩余一个元素为止。
- 在元素集合
6.4.2 排序特征
时间复杂度:O(N2)
空间复杂度:O(1)
稳定性:不稳定。(选数据时稳定,但是交换时无法保证)
6.4.3 实现代码
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int maxi = begin, mini = begin;
for (int i = begin; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
// 如果maxi和begin重叠,修正一下即可
if (begin == maxi)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
6.5 堆排序
6.5.1 排序思路
利用堆这一数据结构来排序。
-
升序:建大堆
-
降序:建小堆
6.5.2 排序特征
时间复杂度:O(N*logN)
空间复杂度:O(1)
稳定性:不稳定。(要交换位置)
6.5.3 实现代码
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
// 找出小的那个孩子
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 排升序
void HeapSort(int* a, int n)
{
// 建大堆
for (int i = (n-1-1)/2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
6.6 冒泡排序
6.6.1 排序思路
冒泡排序指个元素重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
6.6.2 排序特征
时间复杂度为:O(n^2)
空间复杂度为:O(1)
稳定性:稳定。
6.6.3 实现代码
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; ++j)
{
bool exchange = false;
for (int i = 1; i < n-j; i++)
{
if (a[i - 1] > a[i])
{
int tmp = a[i];
a[i] = a[i - 1];
a[i - 1] = tmp;
exchange = true;
}
}
if (exchange == false)
{
break;
}
}
}
6.7 快速排序
6.7.1 排序思路
任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列。左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值。然后将最左、右子序列重复该过程,直到所有元素都排列在相应的位置上为止。
6.7.2 排序特征
时间复杂度:O(N*logN)
空间复杂度:O(logN)
稳定性:不稳定。(要交换位置)
6.7.3 实现代码
快速排序共有三个版本:horae版、挖坑版、前后指针版。
(1) horae 法
(1-1) 基本思路
- 左边(
L
)找比key
大的;右边(R
)找比key
小的; - 找到后交换左边和右边的数据
- 重复步骤1、步骤2;
L
和R
相遇后,交换L
和key
中的数据。
- **左边做
key
,右边先走:**保障了相遇位置比key
小;- **右边做
key
,左边先走:**保障了相遇位置比key
大;
左边做
key
,右边先走的两种情况:
- L遇到R,R是停下来的,L在走。R先走,R停下来的位置一定比key小;相遇的位置就是R停下来的位置,就一定会比key小。
- R遇到L,在相遇的这一轮,L没动,R在移动;跟L相遇,相遇的位置就是L的位置;L的位置就是key的位置;或者交换过一些轮次,相遇L位置一定比key小。
(1-2) 实现代码
int PartSort1(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);
int keyi = left;
while (left < right)
{
// 右边找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
// 左边找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[left]);
return left;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi+1, end);
}
(2) 挖坑 法
(2-1) 基本思路
- 将最左边的数据设置为坑;且最左边的数据作为key;
- 右边找比
key
小的,找到后用该处的数据去填入到坑中,当前右边的所在位置设置为新的坑; - 左边找比坑大的,找到后用该处的数据去填入到坑中,当前左边的所在位置为新的坑;
- 重复步骤二、步骤三,当L和R相遇时结束;
- 结束时将key存入的值填入到坑中。
(2-2) 实现代码
int PartSort2(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);
int key = a[left];
int hole = left;
while (left < right)
{
// 右边找小
while (left < right && a[right] >= key)
{
--right;
}
a[hole] = a[right];
hole = right;
// 左边找大
while (left < right && a[left] <= key)
{
++left;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi+1, end);
}
(3) 前后指针 法
翻跟头式的将大的往后推
(3-1) 基本思路
- prev和key在第一个,cur在第二个;
- cur先走,找比key小的;找到后prev前进一格,随后交换cur和prev的值;
- 重复步骤二,当cur走完整个排序序列时结束;
- 将key与当前prev的数据交换。
- 最开始cur和prev是相邻的;
- 当cur遇到比key大的值以后,cur与prev之间的都是比key大的值;
- cur找小的,找到小的以后,跟
++prev
位置交换。(相当于把大的数翻滚式往右推,同时把小的换到左边。)
(3-2) 实现代码
int PartSort3(int* a, int left, int right)
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);
int prev = left;
int cur = left+1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi+1, end);
}
(4) 快速排序的非递归实现
递归因为要开辟栈帧空间,但当数据量过大时会开辟大量的栈帧空间,因此存在着栈溢出的风险。故可以使用非递归的方法排除该风险。
**基本思路:**每次分割左右子区间,依次入栈。
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
STInit(&st);
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
//int keyi = PartSort3(a, left, right);
int keyi = PartSort1(a, left, right);
// [left, keyi-1] keyi [keyi+1, right]
if (keyi + 1 < right)
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi-1)
{
STPush(&st, keyi-1);
STPush(&st, left);
}
}
STDestroy(&st);
}
6.7.4 快速排序的优化
因为key的值过大(或过小)会影响到快速排序的效率,因此key应当选择一个值适中的数。因此采用如下的三数取中法来优化此算法。
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) / 2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
6.8 归并排序
6.8.1 排序思路
先使每个子序列有序,再使子序列间段有序。若将两个有序表合成为一个有序表,则完成归并。
归并排序的只要步骤为:分解,合并。
6.8.2 排序特征
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:稳定。
6.8.3 实现代码
(1) 递归实现
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin == end)
return;
// 小区间优化
/*if (end - begin + 1 < 10)
{
InsertSort(a+begin, end - begin + 1);
return;
}*/
int mid = (begin + end) / 2;
// [begin, mid] [mid+1, end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid+1, end, tmp);
int begin1 = begin, end1 = mid;
int begin2 = mid+1, end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a+begin, tmp+begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
(2) 非递归实现
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
// 1 2 4 ....
int gap = 1;
while (gap < n)
{
int j = 0;
for (int i = 0; i < n; i += 2 * gap)
{
// 每组的合并数据
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
if (end1 >= n || begin2 >= n)
{
break;
}
// 修正
if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
// 归并一组,拷贝一组
memcpy(a+i, tmp+i, sizeof(int)*(end2-i+1));
}
printf("\n");
//memcpy(a, tmp, sizeof(int) * n);
gap *= 2;
}
free(tmp);
}
6.9 计数排序
6.9.1 排序思路
- 先统计相同元素出现的次数;
- 根据统计结果将序列回收到原来的序列中。
6.9.2 排序特征
时间复杂度:O(N*logN)
空间复杂度:O(N)
稳定性:稳定。
缺陷1:依赖数据范围,适用于范围集中的数组
缺陷2:只能用于整形
6.9.3 实现代码
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] > max)
{
max = a[i];
}
}
int range = max - min + 1;
int* countA = (int*)malloc(sizeof(int) * range);
memset(countA, 0, sizeof(int) * range);
// 统计次数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
// 排序
int k = 0;
for (int j = 0; j < range; j++)
{
while (countA[j]--)
{
a[k++] = j + min;
}
}
}
6.10 外排序
在外存中进行排序(即在硬盘中进行排序)。
基本思路:在排序阶段,先读入能放在内存中的数据量,将其排序输出到一个临时文件,依此进行,将待排序数据组织为多个有序的临时文件。尔后在归并阶段将这些临时文件组合为一个大的有序文件,也即排序结果。
—— writing by Pan Qifan(潘琦藩) ——