写在最开始的那段话:
整理完了对于C语言基础和一些提升部分的笔记内容,真的是感觉自己漏洞百出,不整理不知道,一整理吓一跳,对于以前很多的知识点都在忘记,所以也是赶快将这最重要的数据结构捡起来,通过以前的笔记和一些代码,将这些内容整理出来,方便自己,也方便初学者的朋友们,还是那句话,有则改之无则加勉!
序
1.什么是数据结构?
数据结构:是计算机存储,组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的结合。
2. 什么是算法?
算法:是定义良好的计算过程,他取一个或以组的值为输入,并产生出一个或一组的值为输出,简单的来说算法就是一系列的计算步骤,用来将输入数据转化成输出结构。
第一节:时间复杂度和空间复杂度
1. 算法效率
算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
2. 时间复杂度
算法中的基本操作的执行次数,为算法的时间复杂度
2.1 时间复杂度:
- 需要关注点:操作的数量级,基本操作的执行次数。其中基本操作包含了(一条指令,一组指令和函数调用)。
- 不关注的点:具体得执行时间:
1. 执行时间和硬件资源强相关,不同的硬件处理速度差异可能很大。
2. cpu每秒钟执行的操作在亿计以上,差异不大得操作次数,在直观感受上得区别微乎其微。
3. 大O渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数,因此这个结果就是O(N^2)
3.1 最高次项系数
例一:那么这个程序得时间复杂度是多少呢?
因此这个程序的时间复杂度就是 O(N)
。
练习题1
void Func(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
练习题1:基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)。
练习题2
void Func(int N, int M)
{
int count = 0;
for (int k = 0; k < M; ++ k)
{
++count;
}
for (int k = 0; k < N ; ++ k)
{
++count;
}
printf("%d\n", count);
}
练习2基本操作执行了M+N次,有两个未知数M和N,时间复杂度为 O(N+M)。
3.2 常数项
两者进行对比,什么时候所展示出来的是属于常数,而何时又不属于常数
练习题1
void Func(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
练习题1基本操作执行了100次,通过推导大O阶方法,时间复杂度为 O(1)。
练习题2
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
练习题2基本操作执行最好N次,最坏执行了(N*(N+1)/2次,通过推导大O阶方法+时间复杂度一般看最坏,时间复杂度为 O(N^2)。
3. 3 对数项时间复杂度
练习题1
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
while (begin <= end)
{
int mid = begin + ((end-begin)>>1);
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid;
else
return mid;
}
return -1;
}
练习题1基本操作执行最好1次,最坏O(logN)次,时间复杂度为 O(logN) ps:logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。(建议通过折纸查找的方式讲解logN是怎么计算出来的)因为每一次都是一半一半的进行查找,因此算下来也就是2的多少次方等于整体N。
3.4 函数调用N次
函数每调用一次,算作一次,因此在递归函数之中对于函数进行N次调用,则为O(N)
。
练习题1
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}
练习题1通过计算分析发现基本操作递归了N次,时间复杂度为O(N)。
练习题2
long long Fibonacci(size_t N)
{
return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}
练习题2通过计算分析发现基本操作递归了2N次,时间复杂度为O(2N)。(建议画图递归栈帧的二叉树讲解).
4. 空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
练习题1
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i-1] > a[i])
{
Swap(&a[i-1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
练习题1使用了常数个额外空间,所以空间复杂度为 O(1)。
练习题2
long long* Fibonacci(size_t n)
{
if(n==0)
return NULL;
long long * fibArray =
(long long *)malloc((n+1) * sizeof(long long));
fibArray[0] = 0;
fibArray[1] = 1;for (int i = 2; i <= n ; ++i)
{
fibArray[i ] = fibArray[ i - 1] + fibArray [i - 2];
}
return fibArray ;
}
练习题2动态开辟了N个空间,空间复杂度为 O(N)。
第二节:顺序表和链表
1. 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储.
1.1 顺序表
1.1.1 概念及结构
顺序表是用一段物理地址连续的存储单元以此存储数据元素的线性结构,一般情况下采用的是数组存储,之后再数组的基础上完成对于数据的增删改查。
顺序表一般可分为:
1. 静态顺序表:使用定长数组进行存储
#define N 100
typedef int SLDataType;
typedef struct SeqList
{
SLDataType array[N]; // 定长数组
size_t size; // 有效数据的个数
}SeqList;
2. 动态顺序表,使用动态开辟的数组进行存储
// 顺序表的动态存储
typedef struct SeqList
{
SLDataType* array; // 指向动态开辟的数组
size_t size ; // 有效数据个数
size_t capicity ; // 容量空间的大小
}SeqList;
1.1.2 动态顺序表接口的实现
typedef struct SeqList {
int* nums;
//数组
size_t size;
//元素个数
size_t capacity;
//容量
}SeqList;
- Init 初始化函数
void SeqListInit(SeqList* Sl) {
//初始化数组
Sl->nums = (int*)malloc(sizeof(int) * 4);
//申请数组的空间
Sl->capacity = 4;
//容量值初始化
Sl->size = 0;
//元素个数初始化
}
- CheckCapacity 检查容量函数
void CheckCapacity(SeqList* Sl) {
//第一种方式realloc
if (Sl->size == Sl->capacity) {
Sl->capacity *= 4;
//若元素个数和容量相等则进行扩容
Sl->nums = (int*)realloc(Sl->nums, sizeof(int) * Sl->capacity);
//使用realloc续接扩容,减少了释放和拷贝的过程
}
//第二种方式 malloc
int* newArray = (int*)malloc(sizeof(int) * Sl->capacity);
//拷贝
memcpy(newArray, Sl->nums, Sl->size * sizeof(int));
//释放空间
free(Sl->nums);
Sl->nums = newArray;
}
- PushBack 尾插函数
void SeqListPushBack(SeqList* Sl, int value) {
//第一种方式
CheckCapacity(Sl);
Sl->nums[Sl->size++] = value;
//将制定元素放置在最后位置
//第二种方式
SeqListInsert(Sl, Sl->size, value);
//直接善用插入函数,在最后的位置进行插入
}
- PopBack 尾删函数
void SeqListPopBack(SeqList* Sl) {
//第一种方式(不释放删除元素的空间)
if (Sl->size) {
Sl->size -= 1;
//直接将元素个数减1
}
//第一种方式
SeqListErase(Sl, Sl->size - 1);
//删除指定位置元素函数,此时位置为最后一个位置
}
- PushFront 头插函数:需要从后向前移动,避免元素覆盖
void SeqListPushFront(SeqList* Sl, int value) {
//第一种方式
CheckCapacity(Sl);
size_t end = Sl->size;
//定义最后一个位置
while (end > 0) {
Sl->nums[end] = Sl->nums[end - 1];
//从后往前逐个往后移动一位
end--;
}
Sl->nums[0] = value;
//将给定值赋予在初位置
Sl->size++;
//第二种方式
SeqListInsert(Sl, 0, value);
//直接使用插入函数,此时位置为0
}
- SeqListPopFront 头部删除函数 (元素从前向后移动,避免元素覆盖)
void SeqListPopFront(SeqList* Sl) {
//第一种方式
if (Sl->size) {
size_t start = 1;
//设定初始位置为头部之后一位
while (start < Sl->size) {
Sl->nums[start - 1] = Sl->nums[start];
//从前往后,逐个移动一位
start++;
}
}
Sl->size--;
//第二种方式
SeqListErase(Sl, 0);
//删除0位置的函数
}
- SeqListInsert 在指定Pos位置插入一个数据value
void SeqListInsert(SeqList* Sl, size_t Pos, int value) {
if (Pos < Sl->size) {
//判定所给定位置是否合法
CheckCapacity(Sl);
size_t end = Sl->size;
//设定末尾位置
while (end > Pos) {
//若给定位置小于最后位置
Sl->nums[end] = Sl->nums[end - 1];
//元素逐个后移一位
end--;
}
Sl->nums[Pos] = value;
//在指定位置赋予值
Sl->size++;
}
}
- SeqListErase 删除Pos位置的数据(从前向后移动避免元素覆盖)
void SeqListErase(SeqList* Sl, size_t Pos) {
if (Pos < Sl->size) {
//判断给定Pos是否合法
size_t start = Pos + 1;
//从给定位置后一个位置开始定位
while (start < Sl->size) {
Sl->nums[start - 1] = Sl->nums[start];
//逐个向前移动一位
start++;
}
Sl->size--;
}
}
- SeqListFind 查找指定值函数
int SeqListFind(SeqList* Sl, int value)
{
//遍历法查找元素
for (int i = 0; i < Sl->size; ++i)
{
if (Sl->nums[i] == value)
return i;
}
return -1;
}
- SeqListPrint 打印顺序表函数
void SeqListPrint(SeqList* Sl)
{
//遍历法打印元素
for (size_t i = 0; i < Sl->size; ++i)
{
printf("%d ", Sl->nums[i]);
}
printf("\n");
}
1.1.3 相关练习题
1.2 链表
1.2.1 链表的概念及结构
链表是一种物理存储结构上非连续,非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表的8种结构:
- 单向、双向
- 带头、不带头
- 循环、非循环
虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
- 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
- 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。
1.2.2单向 链表接口的实现
- 定义
typedef struct Node
{
int data;
struct Node* next;
}Node;
//实现不带头单向非循环链表
typedef struct SingList
{
Node* head; // head: 表示链表真正的头结点,即第一个有效的数据的位置
}SingList;
- SingListInit 初始化函数
void SingListInit(SingList* Sl) {
Sl->head = NULL;
//设置空链表
}
- CrearNode 创建节点
Node* CrearNode(int data) {
Node* node = (Node*)malloc(sizeof(Node));
//申请空间
node->data = data;
//将所给的data赋值到节点之中的数据上
node->next = NULL;
//将指向下一个节点的指针赋空
return node;
//返回所创建的node
}
- SingListPrint 打印链表之中的数组
void SingListPrint(SingList* Sl) {
Node* cur = Sl->head;
//将头链表的指针赋值给cur
while (cur) {
printf("%d ", cur->data);
//打印所指向的第一个节点之中的数据
cur = cur->next;
//将此节点之中指向下一个节点的指针重新赋值给cur
}
printf("\n");
}
- SingListPushFront 头插函数
void SingListPushFront(SingList* Sl, int data) {
Node* node = CrearNode(data);
//创建一个节点
if (Sl->head == NULL) {
//如果是空链表
Sl->head = node;
//直接将创建出来的node地址赋给头链表的指针位置
}
else {
node->next = Sl->head;
//将头链表之中的位置给所创建的节点的指针
Sl->head = node;
//将所创建的节点的地址给头指针
}
}
- SingListPopFront 头删函数
void SingListPopFront(SingList* Sl) {
if (Sl->head) {
//先进行判空
Node* cur = Sl->head;
//将头链表的位置给予Cur指针
Sl->head = cur->next;
//将头节点之中的指针所指向的地址给头节点
free(cur);
//释放头节点的空间
}
}
- SingListPushBack 尾部插入函数
void SingListPushBack(SingList* Sl, int data) {
Node* node = CrearNode(data);
//创建需要插入的节点
if (Sl->head = NULL) {
Sl->head = node;
//如果链表为空,则直接将头链表的指针指向所创建链表的位置
}
else {
Node* last = Sl->head;
//将链表首地址赋给所创建的新last指针
while (last->next) {
//循环找到next指针为空的那个
last = last->next;
}
last->next = node;
//将所创建的节点位置给予所查找到的那个next指针
}
}
- SingListPopBack 尾删函数
void SingListPopBack(SingList* Sl) {
//找到最后一个节点,并且修改删除节点之前的前驱节点的执行
if (Sl->head) {
//头链表指针若不为零
Node* prev = NULL;
//prev节点为空
Node* tail = Sl->head;
//将头指针的位置给tail
while (tail->next) {
//当指针不为NULL
prev = tail;
//将此节点赋给prev
tail = tail->next;
//将指向下一个结点的指针给tail
}
if (tail == Sl->head) {
//如果只有一个头链表
Sl->head = NULL;
//则头链表指向空
}
else {
prev->next = NULL;
//否则上一个前驱结点指向空
}
free(tail);
//释放最后一个节点的空间
}
}
- SingListInsertAfter任意位置插入
void SingListInsertAfter(Node* pos, int data) {
if (pos == NULL) {
return;
//判断所给的位置是否为空
}
Node* newNode = CrearNode(data);
//创建所需要插入的新节点
newNode->next = pos->next;
//新节点的指针指向所要插入位置后一个节点的位置
pos->next = newNode;
//将此节点的地址传给此节点的指针
}
- SingListEraseAfter 删除任意位置节点
void SingListEraseAfter(Node* pos){
Node * next;
//创建连接变量
if (pos == NULL) {
return;
}
next = pos->next;
//让所创建指针变量指向删除位置下一个节点的地址
if (next) {
pos->next = next->next;
//下一个节点的指针给予此位置的指针
free(next);
}
}
- SingListFind 查找函数
Node* SingListFind(SingList* Sl, int data) {
Node* cur = Sl->head;
//创建指针指向链表头
while (cur) {
if (cur->data == data) {
return cur;
//如果等于此data,返回其指针位置
}
cur = cur->next;
//否则往下遍历
}
return NULL;
}
- SingListDestory 删除函数:防止内存泄漏
void SingListDestory(SingList* Sl) {
Node* cur = Sl->head;
//创建指针指向链表头
while (cur) {
Node* next = cur->next;
//先保存其中指向下一个节点的地址
free(cur);
cur = next;
//将保存下来的节点位置再次赋给cur指针
}
}
其中对于双指针的概念理解:
void SingleListPop(Node** head)
{
if (*head)
{
Node* newH = (*head)->next;
free(*head);
*head = newH;
}
}
1.2.3 双向链表的接口实现
定义节点结构体
typedef struct Node
{
int data;
//存储的数据
struct Node* next;
//指向下一个节点
struct Node* prev;
//指向上一个节点
}Node;
定义链表
typedef struct List
{
Node* header;
//头节点
}List;
- printList 链表打印函数
void printList(List* lst) {
Node* cur;
//创建节点变量
cur = lst->header->next;
//使其指向header的next地址
while (cur != lst->header) {
printf("%d ", cur->data);
//打印其中的data
cur = cur->next;
//后移一位
}
printf("\n");
}
- listInit链表初始化函数
void listInit(List* lst) {
lst->header = creatNode(0);
//使得header指向新创建的节点
lst->header->next = lst->header;
//使得其next和prev都指向其本身
lst->header->prev = lst->header;
}
- creatNode创建新节点函数
Node* creatNode(int data) {
Node* node = (Node*)malloc(sizeof(Node));
//为node申请一个新的Node大小的空间
node->data = data;
//将所给予的data值赋值给node的data
node->next = node->prev = NULL;
//next和prev全部指向空
return node;
//返回node
}
- listPushBack尾插函数
void listPushBack(List* lst, int data) {
Node* last = lst->header->prev;
//创建last指针指向header的前一个节点
Node* node = creatNode(data);
//为data创建新的节点准备进行插入
last->next = node;
//last的next中存放node的地址
node->prev = last;
//node的prev中存放last的地址
node->next = lst->header;
//将lst的header的地址放置到node的next中
lst->header->prev = node;
//将node的地址放置到header的prev中
//第二种方法:使用随机插入函数
listInsert(lst->header, data);
}
5. listPopBack尾删函数
void listPopBack(List* lst) {
if (lst->header->next == lst->header) {
//不能删除header头,因此如果等于,则表示空链表
return;
}
Node* last, * prev;
//创建两个节点指针变量
last = lst->header->prev;
//last中存放header前一个节点的地址
prev = last->prev;
//prev中存放last前一个节点的地址
prev->next = lst->header;
//prev的next之中存放头节点的地址
lst->header->prev = prev;
//header的prev中存放prev的地址
free(last);
//释放last
//第二种方法:使用随机删除函数
listErase(lst->header->prev);
}
- listPushFront头插函数
void listPushFront(List* lst, int data) {
//
Node* node = creatNode(data);
//创建需要插入的节点
Node* next;
//创建新的next指针变量
next = lst->header->next;
//将header中的next地址存放在next之中
lst->header->next = node;
//将node存放在header的next之中
node->prev = lst->header;
//将header的地址存放在node的prev之中
node->next = next;
//将next存放在node的next之中
next->prev = node;
//将node存放在next的prev之中
//第二种方法:使用随机插入函数
listInsert(lst->header->next, data);
}
7. listPopFront头删函数
void listPopFront(List* lst) {
if (lst->header->next = lst->header) {
return;
//若仅仅只有header,则表示空链,不进行删除
}
Node* next,*cur;
//创建两个变量
cur = lst->header->next;
//将header的next地址保存在cur
next = cur->next;
//将cur的next给予到next之中
lst->header->next = next;
//将next的地址保存在header的next之中
next->prev = lst->header;
//将header存放在next的prev之中
free(cur);
//释放cur
//使用随机删除函数
listErase(lst->header->next);
}
- listInsert任意位置插入函数
void listInsert(Node* pos, int data) {
//在pos位置之前插入节点
Node* node = creatNode(data);
//创建插入节点
Node* front;
//创建指针
front = pos->prev;
//将要插入位置的prev存放在front之中
front->next = node;
//将node的地址给与到front的next之中
node->prev = front;
//将front的地址保存在node的prev之中
node->next = pos;
//将pos保存在node的next之中
pos->prev = node;
//将node存放在pos的prev之中
}
- listErase任意位置删除函数
void listErase(Node* pos) {
//删除pos位置的节点
Node* prev, * next;
//创建两个指针
if (pos->next == pos) {
return; //空表不能删除
}
prev = pos->prev;
//将prev的地址存放在prev之中
next = pos->next;
//将pos的next地址保存在next之中
prev->next = next;
//将next的地址保存在prev的next之中
next->prev = prev;
//将prev的地址保存在next的prev之中
free(pos);
//释放pos
}
- listDesroy 销毁函数
void listDesroy(List* lst) {
//释放
Node* cur = lst->header->next;
//让cur指向head的next所保存的地址
while (cur != lst->header) {
//若cur不等于header,则刚好循环一周
Node* next = cur->next;
//将cur的next地址存放在next之中
free(cur);
//释放cur
cur = next;
//重新让cur指向next
}
free(lst->header);
//释放头指针
lst->header = NULL;
//使其指向空
}
1.3 顺序表和链表的区别和联系
第三节:栈和队列
1. 栈
1.1 栈的概念
一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)(也简称后进先出)的原则。
- 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
- 出栈:栈的删除操作叫做出栈。出数据也在栈顶。
1.2 栈的实现
栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。
1.3 栈的接口实现
用顺序表来实现栈
//栈的定义
typedef struct Stack
{
int* array;
size_t size;
size_t capacity;
}Stack;
- stackInit栈的初始化
void stackInit(Stack* st, size_t n) {
st->array = (int*)malloc(sizeof(int) * n);
//为动态数组申请空间
st->capacity = n;
//将容量设置为n
st->size = 0;
//size设置为0
}
- stackPush进栈函数
void stackPush(Stack* st, int data) {
if (st->size == st->capacity) {
st->capacity *= 2;
st->array = (int*)realloc(st->array, st->capacity * sizeof(int));
}
st->array[st->size++] = data;
}
- stackPop出栈函数
void stackPop(Stack* st) {
if (st->size) {
//若size不等于0
st->size--;
//进行减减操作即可
}
}
- stackTop栈的最上层数据
int stackTop(Stack* st) {
return st->array[st->size - 1];
//直接返回size-1的数据
}
- stackSize栈中数据总量查询
size_t stackSize(Stack* st) {
return st->size;
//直接返回size的值
}
- stackEmpty判空函数
int stackEmpty(Stack* st) {
if (st->size == 0) {
//若等于空,则返回1
return 1;
}
return 0;
//否则返回0
}
- stackDestory销毁函数
void stackDestory(Stack* st) {
//释放
free(st->array);
//将数组array释放
st->array = NULL;
//让其指向kong
st->size = 0;
//size和capacity全部置零
st->capacity = 0;
}
2. 队列
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头。
2.1 队列的接口实现
- 定义函数
#include <stdio.h>
#include <stdlib.h>
//创建队列节点
typedef struct QNode {
struct QNode* next;
int data;
};
//创建队列表头表尾
typedef struct Queue {
QNode* front;
QNode* rear;
int size;
}Queue;
- 队列初始化函数
void queueInit(Queue* q) {
q->front = q->rear = NULL;
//使得其头和尾全部指向空
q->size = 0;
//其中的元素为0
}
- 创建队列节点函数
QNode* creatNode(int data) {
QNode* node = (QNode*)malloc(sizeof(QNode));
//申请新的节点空间
node->data = data;
//将所要赋值的数据data赋予给node的data
node->next = NULL;
//将node的next指向空
return node;
//返回所申请到的node指针
}
- 队尾入队函数
void queuePush(Queue* q, int data) {
QNode* node=creatNode(data);
//判空,若为空队列
if (q->front = NULL) {
q->front = q->rear = node;
//若为空队列,则直接让头front和尾rear指向node
}
else {
//否则
q->rear->next = node;
//将node链接到rear的next上
q->rear = node;
//使得rear重新指向node
//也就是在原本的尾后面续接上一个node,使得node成为新的rear
}
}
- 队头出队函数
void queuePop(Queue* q) {
if (q->front) {
//申请中间变量next,将q的front的next地址赋给next
QNode* next = q->front->next;
free(q->front);
//之后释放头部的位置
q->front = next;
//之后将next赋给q的front
//也就是使得front往后移动一位,将原本的front释放
if (q->front = NULL) {
//释放后再次判空,判断是否为空列表
q->rear = NULL;
//若为空列表,将尾也指向空
}
q->size--;
//头部出队后,数据量减少1
}
}
- 获取队头元素的函数
int queueFront(Queue* q) {
return q->front->data;
//直接返回q的队头的数据
}
- 获取队尾元素的函数
int queueBack(Queue* q) {
return q->rear->data;
//直接返回q的队尾的数据
}
- 返回队内元素个数函数
int queueSize(Queue* q) {
return q->size;
//直接返回数据size的大小
//若是没有size的话则进行遍历
int num = 0;
QNode* cur = q->front;
//创建中间变量指针cur 指向q的头地址
while (cur) {
//进行遍历,num++ 逐渐后移,直到cur指向空为止
num++;
cur = cur->next;
}
return num;
//最终返回num
}
- 队列判空函数
int queueEmpty(Queue* q) {
if (q->front == NULL) {
return 1;
///若指向空,则返回1
}
return 0;
//否则返回0
}
- 队列销毁函数
void queueDestory(Queue* q) {
QNode* cur = q->front;
//创建中间变量指针cur 指向q的头地址
while (cur) {
//进行遍历,逐渐后移,直到cur指向空为止
QNode* next = cur->next;
//创建中间变量next指向cur的next
free(cur);
//释放cur
cur = next;
//使得cur重新指向next
}
q->front = q->rear = NULL;
//头和尾全部指向null
q->size = 0;
//size归零
}
2.2 链表,栈,队列的对比
- 双向链表
- 栈
- 队列
3. 环形队列
我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列可以使用数组实现,也可以使用循环链表实现。
3.1 官方版环形队列接口实现
- 循环体构造
typedef struct {
int front;
int rear;
int k;
int* array;
} MyCircularQueue;
- 循环队列创建函数
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* mq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
//创建mq指针,指向新申请的队列空间
mq->array = (int*)malloc(sizeof(int) * (k + 1));
//为动态数组申请空间 (k+1) 最后留出一个空位置
mq->front = mq->rear = 0;
//头和尾全部赋值0
mq->k = k;
//容量尾k
return mq;
}
- 循环队列尾插函数
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if ((obj->rear + 1) % (obj->k + 1) == obj->front)
//若队列已满,则尾插失败
return false;
obj->array[obj->rear++] = value;
//进行尾插操作
if (obj->rear == obj->k + 1)
//保持循环结构,
obj->rear = 0;
- 循环队列头删函数
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (obj->front == obj->rear)
//若队列为空,则删除失败
return false;
++obj->front;
//将头指针向后移动一位
if (obj->front == obj->k + 1)
//若头指向最后的空位置,则归零保持循环
obj->front = 0;
return true;
}
- 循环队列队头元素
int myCircularQueueFront(MyCircularQueue* obj) {
if (obj->front == obj->rear)
//判空是否为空,若为空,则返回-1
return -1;
return obj->array[obj->front];
//否则则返回队头元素
}
- 循环队列队尾元素
int myCircularQueueRear(MyCircularQueue* obj) {
if (obj->front == obj->rear)
//若为空,同理
return -1;
if (obj->rear == 0)
//若尾指向头0,则返回第k个元素
return obj->array[obj->k];
return obj->array[obj->rear - 1];
//否则返回尾之前的那个元素
}
- 循环队列判空函数
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
if (obj->front == obj->rear)
return true;
//若为空返回true
return false;
}
- 循环队列判满函数
bool myCircularQueueIsFull(MyCircularQueue* obj) {
if ((obj->rear + 1) % (obj->k + 1) == obj->front)
//若尾+1后除以本身的长度后所余的数等于front则表示满溢状态
return true;
return false;
}
- 循环队列释放函数
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->array);
//释放数组
free(obj);
//释放队列
}
官方版本得循环队列实现,不同的地方在于它在创建队列的时候,会在最后空出来一个位置,存放rear
指针,这样更好的保证了循环的有效性。
3.2 自带size版环形队列接口实现
- 循环体构造
typedef struct {
int front;
int rear;
int size;
int k;
int* array;
} MyCircularQueue;
- 循环队列创建函数
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* mq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
//申请新的变量
mq->array = (int*)malloc(sizeof(int) * k);
//给队列之中的数组开辟k个大小
mq->front = mq->rear = 0;
//头和尾全部为0
mq->size=0;
//元素个数为0
mq->k = k;
//总长度为k
return mq;
}
- 循环队列尾插函数
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
if (obj->size == obj->k)
//判断是否满
return false;
//尾插
obj->array[obj->rear++] = value;
//进行尾插操作
//保证循环结构
if (obj->rear == obj->k)
//若插入之后满了,则让其移动到原点位置,保证循环结构
obj->rear = 0;
obj->size++;
//插入后元素个数加1
return true;
}
- 循环队列头删函数
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
if (obj->size == 0)
//若元素个数为零,则删除失败
return false;
//头删
++obj->front;
//头往后移动一位
if (obj->front == obj->k)
//若移动后为最后元素,则重新循环
obj->front = 0;
//将原有的size减1
--obj->size;
return true;
}
- 循环队列队头元素
int myCircularQueueFront(MyCircularQueue* obj) {
if (obj->size == 0)
//若元素个数不为零
return -1;
return obj->array[obj->front];
//则直接返回队头元素
}
- 循环队列队尾元素
int myCircularQueueRear(MyCircularQueue* obj) {
if (obj->size == 0)
//若为零,则返回-1
return -1;
if (obj->rear == 0)
//若队尾在零位置,则返回之前的最后元素
return obj->array[obj->k - 1];
return obj->array[obj->rear - 1];
//两者都不满足的话,则直接返回尾部之前的一个元素
}
- 循环队列判空函数
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
if (obj->size == 0)
//若size为零,则代表队列里无元素
return true;
return false;
}
- 循环队列判满函数
bool myCircularQueueIsFull(MyCircularQueue* obj) {
if (obj->size == obj->k)
//若size等于最大长度,则代表满溢
return true;
return false;
}
- 循环队列释放函数
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->array);
//释放数组
free(obj);
//释放队列
}
4. 栈和队列的对比
5. 栈和队列的习题
第四节:二叉树和堆
1. 二叉树
1.1 树得概念及结构
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
树的表示形式为:双亲,孩子,孩子兄弟
- 根没有父节点
- 子树之间互不相交
- 节点个数 = 边数 + 1
- 节点的度:子树的个数
- 树的度:最大的节点度
- 树的高度:最大的层次
1.2 满二叉树和完全二叉树
- 满二叉树:除过叶子,其他节点的度都为2,并且每一层都是满的。
- 完全二叉树:除过最后一层,剩余结构是一个满二叉树,并且最后一层的节点从左向右中间无间隔空隙!
满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树!
1.3 二叉树节点数
- n0 + n1 + n2 = 二叉树的节点总数
- n1 + 2* n2 = 二叉树的边总数
- 节点总数 - 1 = 边的总数
- 推论:n0 = 1 + n2
- 满二叉树的高度为:log (N+1) log以2为底的(N+1)
- 满二叉树的节点:奇数
1.4 二叉树的存储形式
// 二叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
struct BinTreeNode* _pParent; // 指向当前节点的双亲
struct BinTreeNode* _pLeft; // 指向当前节点左孩子
struct BinTreeNode* _pRight; // 指向当前节点右孩子
BTDataType _data; // 当前节点值域
}
2. 堆
每个节点都是以其为根的树中的最值,并且结构为完全二叉树
例子:
2.1 两种调整方式
建堆是一个从数组到堆的过程
2.1.1 向下调整
- 向下调整思想:小堆
- 向下调整思想:大堆
例题:
向下调整的过程:
- 代码实现:
void shiftDown(HPDataType* array, int size, int parent) {
int child = 2 * parent + 1;
//子节点等于二倍的父节点+1
while (child < size) {
if (child + 1 < size && array[child + 1] < array[child]) {
//若右边的子节点小于左边的子节点,则将child++
child++;
}
if (array[child] < array[parent]) {
//若子节点比父节点小
Swap(array, child, parent);
//交换子节点和父节点
parent = child;
//将子节点赋给父节点
child = 2 * parent + 1;
//重新计算子节点
}
else {
break;
}
}
}
2.1.2 向上调整
向上调整更多的适用于堆的插入(以尾插的形式插入到堆中)
- 代码实现:
void shiftUp(HPDataType* array, int child) {
int parent = (child - 1) / 2;
//父节点等于子节点减一除2
while (child > 0) {
if (array[child] < array[parent]){
//若子节点比父节点小,则交换两者的位置
Swap(array,child,parent);
child=parent;
//之后将父节点赋给子节点
parent=(child-1)/2;
//重新寻找下一个父节点
}
else {
break;
}
}
}
*小技巧:重点牢记
向下调整和向上调整的对比:
2.2 建堆
建堆的过程:用一个数组的数据创建一个堆,从最后一个非叶子节点开始,进行向下调整,直到调整到根节点的位置结束,最后一个非叶子节点:(数组大小 - 2 )/ 2.
2.3 堆的尾删
堆的尾删:将最顶端的极值和最后一个叶子节点进行交换,之后将最后一个节点删掉后,重新进行建堆。
2.4 topk 从所有数据中找到前K个值
//TopK问题: 找出N个数里面最大/最小的前K个问题。
//这里实现两个版本:
//1. 找最大的K个元素
//假设堆为小堆
void PrintTopK(int* a, int n, int k)
{
Heap hp;
//建立含有K个元素的堆
HeapInit(&hp, a, k);
for (size_t i = k; i < n; ++i) // N
{
//每次和堆顶元素比较,大于堆顶元素,则删除堆顶元素,插入新的元素
if (a[i] > HeapTop(&hp)) // LogK
{
HeapPop(&hp);
HeapPush(&hp, a[i]);
}
}
for (int i = 0; i < k; ++i) {
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
}
//2. 找最小的K个元素
//假设堆为大堆
void PrintTopK(int* a, int n, int k)
{
Heap hp;
//建立含有K个元素的堆
HeapInit(&hp, a, k);
for (size_t i = k; i < n; ++i) // N
{
//每次和堆顶元素比较,小于堆顶元素,则删除堆顶元素,插入新的元素
if (a[i] < HeapTop(&hp)) // LogK
{
HeapPop(&hp);
HeapPush(&hp, a[i]);
}
}
for (int i = 0; i < k; ++i) {
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
}
3. 二叉树的遍历
- 前中后遍历顺序:
4. 二叉树接口的实现
- 二叉树的创建函数
//二叉树的创建
BTNode* BinaryTreeCreate(BTdataTyde* a, int* pi) {
if (a[*pi] != '#') {
//若节点值不为空
BTNode* root = (BTNode*)malloc(sizeof(BTNode));
//申请根节点空间
root->data = a[*pi];
//将数组值赋值给根节点
(*pi)++;
//之后数组地址++
root->left = BinaryTreeCreate(a, pi);
//左子树进行递归赋值
(*pi)++;
//数组地址依然++
root->right = BinaryTreeCreate(a, pi);
//右子树进行递归赋值
//右子树完毕之后不用地址++,因为右子树完毕之后返回根节点
return root;
}
else {
return NULL;
//若数组值为空,则返回空
}
}
- 二叉树销毁函数
//二叉树销毁函数
void BinaryTreeDestory(BTNode** root) {
BTNode* cur = *root;
//传进一级指针
if (cur) {
BinaryTreeDestory(&cur->left);
//将左子树的地址递归传进销毁函数
BinaryTreeDestory(&cur->right);
//将右子树的地址递归传递进销毁函数
free(cur);
//释放cur指针
*root = NULL;
//将*root指针指向空
}
}
- 二叉树结点个数统计函数(实现方法1)
int BinaryTreeSize1(BTNode* root) {
if (root == NULL) {
return 0;
}
if (root->left == NULL && root->right == NULL) {
return 1;
}
return BinaryTreeSize1(root->left) +
BinaryTreeSize1(root->right) + 1;
}
- 二叉树结点个数统计函数(实现方法2)
//二叉树节点个数统计函数2
void BinaryTreeSize2(BTNode* root, int* num) {
if (root)
{
++(*num);
BinaryTreeSize2(root->left, num);
BinaryTreeSize2(root->right, num);
}
}
- 二叉树的叶子节点个数统计函数
//二叉树的叶子节点个数统计函数
int BinaryTreeLeafSize(BTNode* root) {
if (root == NULL) {
return 0;
}
if (root->left == NULL && root->right == NULL) {
return 1;
}
return BinaryTreeLeafSize(root->left) +
BinaryTreeLeafSize(root->right);
}
- 二叉树第K层节点个数统计函数
//二叉树第K层节点个数统计函数
int BinaryTreeLevelKSize(BTNode* root, int k) {
if (root == NULL) {
return 0;
//若二叉树为空,则节点个数为0
}
if (k == 1) {
return 1;
//若是第一层,则节点数为1
}
return BinaryTreeLevelKSize(root->left, k - 1)
+ BinaryTreeLevelKSize(root->right, k - 1);
//逐次往下进行,则k逐次减一进行递归计算
}
- 二叉树查找值为x的节点函数
//二叉树查找值为x的节点函数
BTNode* BinaryTreeFind(BTNode* root, BTdataTyde x) {
BTNode* ret;
if (root == NULL) {
//若二叉树为空,则位置为空
return NULL;
}
if (root->data == x) {
return root;
//若根节点等于x,则返回根节点
}
ret = BinaryTreeFind(root->left, x);
//否则进行左子树的遍历
if (ret) {
return ret;
//若左子树能够找到,则返回左子树
}
return BinaryTreeFind(root->right, x);
//否则直接返回右子树
}
- 二叉树的前序遍历函数(递归实现)
//二叉树的前序遍历函数(递归)
void BinaryTreePrevOrder(BTNode* root) {
if (root) {
printf("%c", root->data);
//先打印根节点
BinaryTreePrevOrder(root->left);
//左子树递归
BinaryTreePrevOrder(root->right);
//右子树递归
}
}
- 二叉树的前序遍历函数(非递归实现)
//二叉树的前序遍历函数(非递归)
void BinaryTreePrevOrderNoR(BTNode* root) {
//借助栈来进行模拟递归的过程实现前序遍历
Stack st;
//创建栈
StackInit(&st, 10);
//栈进行初始化
BTNode* cur = root;
//指针变量指向root
BTNode* top;
//顶部变量
while (cur || StackEmpty(&st) != 1) {
//若cur和栈都不为空
while (cur) {
printf("%c", cur->data);
//打印根节点的值
StackPush(&st, cur);
//将其入栈
cur = cur->left;
//进入到左子树之中
}
top = StackTop(&st);
//取出栈中元素
StackPop(&st);
//删除栈顶元素
cur = top->right;
//指向右子树
}
printf("\n");
}
- 二叉树的中序遍历函数(递归实现)
//二叉树的中序遍历函数(递归)
void BinaryTreeInOrder(BTNode* root) {
if (root) {
BinaryTreeInOrder(root->left);
//先行递归左子树
printf("%c", root->data);
//打印根节点
BinaryTreeInOrder(root->right);
//再递归右子树
}
}
- 二叉树的中序遍历函数(非递归实现)
//二叉树的中序遍历函数(非递归)
void BinaryTreeInOrderNoR(BTNode* root) {
//借助栈来进行模拟递归的过程实现中序遍历
Stack st;
//创建栈
StackInit(&st, 10);
//栈进行初始化
BTNode* cur = root;
//指针变量指向root
BTNode* top;
//顶部变量
while (cur || StackEmpty(&st) != 1) {
//若cur和栈都不为空
while (cur) {
StackPush(&st, cur);
//将其入栈
cur = cur->left;
//进入到左子树之中
}
top = StackTop(&st);
//取出栈中元素
StackPop(&st);
//删除栈顶元素
printf("%c", top->data);
//打印根节点的值
cur = top->right;
//指向右子树
}
printf("\n");
}
- 二叉树的后序遍历函数(递归实现)
//二叉树的后序遍历函数(递归)
void BinaryTreePostOrder(BTNode* root) {
if (root) {
BinaryTreePostOrder(root->left);
BinaryTreePostOrder(root->right);
printf("%c", root->data);
}
}
- 二叉树的后序遍历函数(非递归实现)
//二叉树的后序遍历函数(非递归)
void BinaryTreePostOrderNoR(BTNode* root) {
//借助栈来进行模拟递归的过程实现后序遍历
Stack st;
//创建栈
StackInit(&st, 10);
//栈进行初始化
BTNode* cur = root;
//指针变量指向root
BTNode* top;
//顶部变量
BTNode* prev = NULL;
//设置中间变量,以考察栈顶元素是第几次访问
while (cur || StackEmpty(&st) != 1) {
//若cur和栈都不为空
while (cur) {
StackPush(&st, cur);
//将其入栈
cur = cur->left;
//进入到左子树之中
}
top = StackTop(&st);
//取出栈中元素
if (top->right == NULL || top->right == prev) {
//若右子树为空,或者已经访问过一次右子树
printf("%c", top->data);
//打印当前节点
StackPop(&st);
//删除栈顶元素
prev = top;
//将prev的指针更新
}
else {
cur = top->right;
//指向右子树
}
}
printf("\n");
}
- 二叉树的层序遍历函数
//层序遍历
void BinaryTreeLevelOrder(BTNode* root) {
//一个从左向右,从上到下的队列,保护每一层的节点数
Queue q;
//创建队列
QueueInit(&q);
//初始化队列
if (root) {
QueuePush(&q, root);
//二叉树根节点入队
}
while (QueueEmpty(&q) != 1) {
//队列若不为空
BTNode* front = QueueFront(&q);
//设置指针变量指向队列队头元素
QueuePop(&q);
//队列出队操作
printf("%c", front->data);
//将队头节点元素打印出来
if (front->left) {
QueuePush(&q, front->left);
//队头左子树入栈
}
if (front->right) {
QueuePush(&q, front->right);
//对头右子树入栈
}
}
printf("\n");
}
- 判断二叉奥数是否为完全二叉树
//判断二叉树是否完全二叉树
int BinaryTreeComplete(BTNode* root) {
Queue q;
//创建队列
QueueInit(&q);
//初始化队列
if (root) {
QueuePush(&q, root);
//二叉树根节点入队
}
while (QueueEmpty(&q) != 1) {
BTNode* front = QueueFront(&q);
//指针指向队列队头元素
QueuePop(&q);
//队列出队操作
if (front) {
QueuePush(&q, front->left);
//左边入队
QueuePush(&q, front->right);
//右边入队
}
else {
break;
}
}
while (QueueEmpty(&q) != 1) {
BTNode* front = QueueFront(&q);
//获取队头元素
QueuePop(&q);
//出队操作
if (front) {
//剩余元素之中存在任何一个非空节点,则说明不是一颗完全二叉树
return 0;
//若指针为空,则为0
}
}
//剩余节点全部为空节点,则为完全二叉树
return 1;
}