数据结构与算法C语言实现
一、线性结构与非线性结构
1线性数据结构
1)线性结构作 为最常用的数据结构,其特点是数据元素之间存在-对一的线性关系
2)线性结 构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的。(指的是存储地址连续)
3)链 式存储的线性表称为链表,链表中的存储元素不-定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。(存储地址不一定连续,用指针指向上下节点。充分利用碎片空间)
4)线性结构常见的有: 数组、队列、链表和栈
2非线性结构
非线性结构包括:二维数组,多维数组,广义表,树结构,图结构
二、稀疏数组
1例子
➢分析问题:
因为该二维数组的很多值是默认值0,因此记录了很多没有意义的数据->稀疏数组。
2基本介绍
当一个数组中大部分元素为0,或者为同一个值的数组时,可以使用稀疏数组来保存该数组。稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少个不同的值
2)把具有不同值的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
3应用实例
1)使用稀疏数组, 来保留类似前面的二维数组(棋盘、地图等等)
2)把稀疏数组存盘, 并且可以从新恢复原来的二维数组数
3)整体思路分析
二维数组转稀疏数组的思路
1.遍历原始的二维数组,得到有效数据的个数sum
2.根据sum就可以创建稀疏数组sparseArr int[sum+ 1][3]
3.将二维数组的有效数据数据存入到稀疏数组
稀疏数组转原始的二维数组的思路
1.先读取稀疏数组的第-行,根据第一行的数据,创建原始的二维数组,比如上面的chessArr2 =int[11][11]
2.在读取稀疏数组后几行的数据,并赋给原始的二维数组即可.
/**
* 稀疏数组实现 -- 二维数组转化为稀疏数组,稀疏数组转化为二维数组
*/
#include <stdio.h>
#include <string.h>
//尺寸
#define SIZE 11
int main() {
//1.创建原始二维数组11*11
//2.0:表示没有棋子,1表示黑子;2表示白子
int chessArr[SIZE][SIZE];
//记录非0值个数
int sum = 0;
//赋0值
memset(chessArr, 0, sizeof(chessArr));
chessArr[1][2] = 1;
chessArr[2][4] = 2;
chessArr[3][4] = 1;
//输出原始的二维数组
//行数
int row = sizeof(chessArr) / sizeof(chessArr[0]);
int rank = sizeof(chessArr[0]) / sizeof(int);
printf("行:%d,列:%d\n", row, rank);
printf("原始的二维数组:\n");
for (int i = 0; i < row; ++i) {
for (int j = 0; j < rank; ++j) {
printf("%d\t", chessArr[i][j]);
if (chessArr[i][j] != 0) {
sum++;
}
}
printf("\n");
}
printf("非0值个数sum=%d\n", sum);
//将二维数组转为稀疏数组
//1.sum已得到非0元素个数
//2.创建稀疏数组
int sparseArr[sum + 1][3];
//赋0值
memset(sparseArr, 0, sizeof(sparseArr));
//给稀疏数组赋值
sparseArr[0][0] = row;
sparseArr[0][1] = rank;
sparseArr[0][2] = sum;
//遍历二维数组将非0的值存入稀疏数组
int count = 0;
for (int i = 0; i < row; ++i) {
for (int j = 0; j < rank; ++j) {
if (chessArr[i][j] != 0) {
count++;
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = chessArr[i][j];
}
}
}
//输出稀疏数组
printf("得到的稀疏数组为:\n");
for (int i = 0; i < sum + 1; ++i) {
printf("%d\t%d\t%d\t\n", sparseArr[i][0], sparseArr[i][1], sparseArr[i][2]);
}
//将稀疏数组回复成原始的二维数组
//1.先读取稀疏数组
int chessArr1[sparseArr[0][0]][sparseArr[0][1]];
//赋0值
memset(chessArr1, 0, sizeof(chessArr1));
printf("恢复后的二维数组:\n");
int row1 = sizeof(chessArr1) / sizeof(chessArr1[0]);
int rank1 = sizeof(chessArr1[0]) / sizeof(int);
//2.遍历稀疏数组 赋值给原值
for (int i = 1; i < sum + 1; ++i) {
chessArr1[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
for (int i = 0; i < row1; ++i) {
for (int j = 0; j < rank1; ++j) {
printf("%d\t", chessArr1[i][j]);
}
printf("\n");
}
return 0;
}
三、环形队列(数组-线性)
1.有序列表,可以用数组或是链表来实现
2.遵循先入先出原则。
3.队列空间可以循环使用。
/**
* 使用数组实现顺序队列--队列空间循环使用
*/
#include <stdio.h>
//最大容量
#define MAXSIZE 5
//元素类型
typedef int elemType;
//模拟队列结构体
typedef struct {
//队列头 --指向队列头部(首元素索引)初始化位0
int front;
//队列尾 --指向队列尾部后的一个位置(即队列最后一个数据的后一个位置索引)初始化为0
int rear;
//存放数据的数组 最多存放MAXSIZE-1个元素
elemType arr[MAXSIZE];
} ArrayQueue;
//初始化方法
void init(ArrayQueue *arrayQueue);
//判断队列是否满的方法 0非满1满
int isFull(ArrayQueue *arrayQueue);
//判断队列为空的方法 0非空1空
int isEmpty(ArrayQueue *arrayQueue);
//添加数据到队列的方法 0失败1成功
int addQueue(ArrayQueue *arrayQueue, elemType data);
//获取队列数据0失败1成功 data为获取到的数据
int getQueue(ArrayQueue *arrayQueue, elemType *data);
//打印队列元素
void showQueue(ArrayQueue *arrayQueue);
//显示队列的头数据,非取出数据
int headQueue(ArrayQueue *arrayQueue, elemType *data);
//统计队列元素个数
int size(ArrayQueue *arrayQueue);
int main() {
//声明一个队列
ArrayQueue arrayQueue;
//用户输入指令
char key;
//用户输入
elemType data;
int loop = 1;
init(&arrayQueue);
while (loop) {
printf("\ns(show):显示队列");
printf("\ne(exit):退出程序");
printf("\na(add):添加数据到队列");
printf("\ng(get):从队列取出数据");
printf("\nh(head):查看队列头数据");
scanf_s("%c", &key);
getchar();
switch (key) {
case 's':
showQueue(&arrayQueue);
break;
case 'e':
loop = 0;
break;
case 'a':
printf("\n请输入一个数字:");
scanf_s("%d", &data);
getchar();
addQueue(&arrayQueue, data);
break;
case 'g':
if (getQueue(&arrayQueue, &data)) {
printf("\n取出的数据为:%d", data);
} else {
printf("未取到数据");
}
break;
case 'h':
if (headQueue(&arrayQueue, &data)) {
printf("\n队列头的数据为:%d", data);
} else {
printf("\n未取到队列头数据");
}
break;
default:
printf("\n输入的指令不正确");
break;
}
}
printf("\n程序退出");
return 0;
}
void init(ArrayQueue *arrayQueue) {
arrayQueue->front = arrayQueue->rear = 0;
}
int isFull(ArrayQueue *arrayQueue) {
if ((arrayQueue->rear + 1) % MAXSIZE == arrayQueue->front) {
return 1;
}
return 0;
}
int isEmpty(ArrayQueue *arrayQueue) {
if (arrayQueue->rear == arrayQueue->front) {
return 1;
}
return 0;
}
int addQueue(ArrayQueue *arrayQueue, elemType data) {
//判断队列是否满
if (isFull(arrayQueue)) {
//队列满
return 0;
}
//rear后移
arrayQueue->arr[arrayQueue->rear] = data;
//考虑循环 -- 重点理解公式
arrayQueue->rear = (arrayQueue->rear + 1) % MAXSIZE;
return 1;
}
int getQueue(ArrayQueue *arrayQueue, elemType *data) {
if (isEmpty(arrayQueue)) {
//为空
return 0;
}
//front指向队列的第一个元素
*data = arrayQueue->arr[arrayQueue->front];
arrayQueue->front = (arrayQueue->front + 1) % MAXSIZE;
return 1;
}
void showQueue(ArrayQueue *arrayQueue) {
if (isEmpty(arrayQueue)) {
return;
}
//从front开始遍历 遍历多少个元素
for (int i = arrayQueue->front; i < (arrayQueue->front + size(arrayQueue)); ++i) {
printf("\narr[%d]=%d;front=%d;rear=%d", i % MAXSIZE, arrayQueue->arr[i % MAXSIZE], arrayQueue->front,
arrayQueue->rear);
}
}
int headQueue(ArrayQueue *arrayQueue, elemType *data) {
if (isEmpty(arrayQueue)) {
return 0;
}
*data = arrayQueue->arr[arrayQueue->front];
return 1;
}
int size(ArrayQueue *arrayQueue) {
return (arrayQueue->rear + MAXSIZE - arrayQueue->front) % MAXSIZE;
}
四、单向链表(链表非线性)
1.链表是以节点的方式来存储的。
2.每个节点保罗data域存储数据,和next域指向下一个节点。
3.链表的各个节点不一定是连续存储。
4.链表分带头节点和没有头结点的链表,根据实际需求来确定。
头节点不存放具体数据,作用就是表示单链的表头
思路:
添加(创建):
1.先创建一个head头节点,作用就是表示单链表的头。
2.后面我们每添加一个节点,就直接加入到链表的最后。
/**
* 单链表
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Hero {
int no;
char name[16];
char nickName[16];
} Hero;
//元素类型
typedef Hero elemType;
//结点结构体
typedef struct LinkedNode {
//存放数据
elemType data;
//指向下一个节点地址
struct LinkedNode *next;
} LinkedNode;
//初始化方法
LinkedNode *init();
//第一种添加方式,顺序添加一个节点
//将链表最后一个节点的next指向新节点;linkedNode添加到的链表,addNode添加的新元素
void add(LinkedNode *linkedNode, LinkedNode *addNode);
//第二种添加方式,升序添加,存在相同的元素添加失败0, 1为成功
int addByAsc(LinkedNode *linkedNode, LinkedNode *addNode);
//显示链表
void printList(LinkedNode *linkedNode);
//创造一个节点
LinkedNode *createNode(int no, char name[], char nickName[]);
//根据编号修改节点信息0失败 1成功
int update(LinkedNode *linkedNode, elemType newHeroNode);
//删除
int delete(LinkedNode *linkedNode, int no);
//单链表的反转 难点
//1.定义一个新节点头
//2.从头到尾遍历原来的链表,每遍历一个节点,将其取出,放在新链表的最前端
int reverse(LinkedNode *linkedNode);
//倒序打印单链表
//1.方式一:先倒序排列,再打印;破坏了原来的链表 --- 不建议
//2.方式二:利用栈
//3.方式三:递归
void reversePoint(LinkedNode *linkedNode);
//查询、统计有效数据个数略
//整个链表不用了,释放全部内存;写来玩玩儿
void freeMemory(LinkedNode *linkedNode);
int main() {
LinkedNode *linkedNode = init();
//以下方法是错误的,重新分配了地址
//LinkedNode linkedNode;
//init对地址进行了重新分配,改变了地址,后续使用&linkedNode就不是初始化后的数据了
//init(&linkedNode);
LinkedNode *linkedNode1 = createNode(1, "宋江", "及时雨");
LinkedNode *linkedNode2 = createNode(2, "卢俊义", "玉麒麟");
LinkedNode *linkedNode3 = createNode(3, "吴用", "智多星");
//add(linkedNode, linkedNode1);
//add(linkedNode, linkedNode2);
//add(linkedNode, linkedNode3);
addByAsc(linkedNode, linkedNode2);
addByAsc(linkedNode, linkedNode1);
addByAsc(linkedNode, linkedNode3);
//更新元素
elemType hero;
hero.no = 2;
strcpy(hero.name, "小卢");
strcpy(hero.nickName, "玉麒麟4");
update(linkedNode, hero);
//delete(linkedNode, 1);
printf("反转前链表:\n");
printList(linkedNode);
reverse(linkedNode);
printf("反转后链表:\n");
//打印新的倒置链表
printList(linkedNode);
//仅倒置打印,而不改变原来数据顺序
printf("反转打印链表:\n");
reversePoint(linkedNode);
//free(linkedNode1);
//free(linkedNode2);
//free(linkedNode3);
//free(linkedNode);
freeMemory(linkedNode);
return 0;
}
LinkedNode *init() {
LinkedNode *linkedNode = malloc(sizeof(LinkedNode));
printf("分配的内存地址:%p\n", linkedNode);
//初始化头节点,申请空间,但不存储数据
linkedNode->next = NULL;
return linkedNode;
}
LinkedNode *createNode(int no, char name[], char nickName[]) {
LinkedNode *linkedNode = malloc(sizeof(LinkedNode));
linkedNode->data.no = no;
strcpy(linkedNode->data.name, name);
strcpy(linkedNode->data.nickName, nickName);
printf("分配的内存地址:%p\n", linkedNode);
return linkedNode;
}
void add(LinkedNode *linkedNode, LinkedNode *addNode) {
//遍历找到最后一个节点
LinkedNode *temp = linkedNode;
while (1) {
//如果next为null,则是最后一个
if (temp->next == NULL) {
break;
}
//找下一个节点
temp = temp->next;
}
//重新设置链表指向,加入元素
temp->next = addNode;
addNode->next = NULL;
}
int addByAsc(LinkedNode *linkedNode, LinkedNode *addNode) {
if (linkedNode == NULL || addNode == NULL) {
return 0;
}
LinkedNode *temp = linkedNode;
while (1) {
if (temp->next == NULL) {
//还没有元素,直接加入即可
//也可能是最后一个元素
temp->next = addNode;
addNode->next = NULL;
return 1;
}
if (temp->next->data.no > addNode->data.no) {
//加入的是最小的元素
//也可能是中间元素
addNode->next = temp->next;
temp->next = addNode;
return 1;
} else if (temp->next->data.no == addNode->data.no) {
//重复元素
printf("添加数据重复;重复数据为:%d\n", addNode->data.no);
return 0;
}
temp = temp->next;
}
}
void printList(LinkedNode *linkedNode) {
LinkedNode *s;
s = linkedNode->next;
while (s != NULL) {
printf("编号:%d;\t姓名:%s;\t昵称:%s", s->data.no, s->data.name, s->data.nickName);
s = s->next;
printf("\n");
}
printf("\n");
}
int update(LinkedNode *linkedNode, elemType newHeroNode) {
if (linkedNode == NULL) {
printf("链表为空!\n");
return 0;
}
LinkedNode *temp = linkedNode;
while (temp->next != NULL) {
temp = temp->next;
if (temp->data.no == newHeroNode.no) {
strcpy(temp->data.name, newHeroNode.name);
strcpy(temp->data.nickName, newHeroNode.nickName);
return 1;
}
}
return 0;
}
int delete(LinkedNode *linkedNode, int no) {
LinkedNode *temp = linkedNode;
LinkedNode *delete;
while (1) {
if (temp->next == NULL) {
//已经到最后了
printf("没有找到删除节点编号:%d\n", no);
return 0;
}
if (temp->next->data.no == no) {
delete = temp->next;
temp->next = temp->next->next;
free(delete);
return 1;
}
}
}
int reverse(LinkedNode *linkedNode) {
if (linkedNode == NULL) {
//传入的链表没有元素
return 0;
}
LinkedNode *newLinkNode = init();
LinkedNode *temp;
//逐渐把linkedNode排空,加到newLinkNode之中
while (linkedNode->next != NULL) {
//保存下一个节点
temp = linkedNode->next;
//断链,扔掉一个节点
linkedNode->next = linkedNode->next->next;
//连接到新链表
temp->next = newLinkNode->next;
newLinkNode->next = temp;
}
linkedNode->next = newLinkNode->next;
free(newLinkNode);
return 1;
}
void reversePoint(LinkedNode *linkedNode) {
if (linkedNode == NULL) {
return;
}
LinkedNode *temp = linkedNode;
if (temp->next != NULL) {
temp = temp->next;
reversePoint(temp);
printf("编号:%d;\t姓名:%s;\t昵称:%s\n", temp->data.no, temp->data.name, temp->data.nickName);
}
}
void freeMemory(LinkedNode *linkedNode) {
//临时记录释放的下一个节点地址
LinkedNode *temp;
//释放的节点地址
LinkedNode *temp2 = linkedNode;
while (temp2->next != NULL) {
temp = temp2->next;
printf("释放的内存地址:%p\n", temp2);
free(temp2);
temp2 = temp;
}
printf("释放的内存地址:%p\n", temp2);
free(temp2);
}
五、双向链表
完成:双向链表的创建以及增删查改。
1.双向链表的可以向前,也可以向后查找。
2.前后节点相互指向。
3.代码在单向链表基础上修改,主要是注意设置前后节点的关联关系。
/**
* 双向链表
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct Hero {
int no;
char name[16];
char nickName[16];
} Hero;
//元素类型
typedef Hero elemType;
//结点结构体 双向链表
typedef struct LinkedNode {
//存放数据
elemType data;
//指向下一个节点地址
struct LinkedNode *next;
//指向前一个节点地址
struct LinkedNode *pre;
} LinkedNode;
//初始化方法
LinkedNode *init();
//第一种添加方式,顺序添加一个节点
//将链表最后一个节点的next指向新节点;linkedNode添加到的链表,addNode添加的新元素
void add(LinkedNode *linkedNode, LinkedNode *addNode);
//第二种添加方式,升序添加,存在相同的元素添加失败0, 1为成功
int addByAsc(LinkedNode *linkedNode, LinkedNode *addNode);
//显示链表
void printList(LinkedNode *linkedNode);
//创造一个节点
LinkedNode *createNode(int no, char name[], char nickName[]);
//根据编号修改节点信息0失败 1成功
int update(LinkedNode *linkedNode, elemType newHeroNode);
//删除 双向链表可以直接找到要删除的节点,找到后自我删除即可
int delete(LinkedNode *linkedNode, int no);
//单链表的反转 难点
//1.定义一个新节点头
//2.从头到尾遍历原来的链表,每遍历一个节点,将其取出,放在新链表的最前端
int reverse(LinkedNode *linkedNode);
//倒序打印单链表
//1.方式一:先倒序排列,再打印;破坏了原来的链表 --- 不建议
//2.方式二:利用栈
//3.方式三:递归
void reversePoint(LinkedNode *linkedNode);
//查询、统计有效数据个数略
//整个链表不用了,释放全部内存;写来玩玩儿
void freeMemory(LinkedNode *linkedNode);
int main() {
LinkedNode *linkedNode = init();
//以下方法是错误的,重新分配了地址
//LinkedNode linkedNode;
//init对地址进行了重新分配,改变了地址,后续使用&linkedNode就不是初始化后的数据了
//init(&linkedNode);
LinkedNode *linkedNode1 = createNode(1, "宋江", "及时雨");
LinkedNode *linkedNode2 = createNode(2, "卢俊义", "玉麒麟");
LinkedNode *linkedNode3 = createNode(3, "吴用", "智多星");
//add(linkedNode, linkedNode1);
// add(linkedNode, linkedNode2);
//add(linkedNode, linkedNode3);
addByAsc(linkedNode, linkedNode2);
addByAsc(linkedNode, linkedNode1);
addByAsc(linkedNode, linkedNode3);
//更新元素
elemType hero;
hero.no = 2;
strcpy(hero.name, "小卢");
strcpy(hero.nickName, "玉麒麟4");
update(linkedNode, hero);
delete(linkedNode, 3);
printf("反转前链表:\n");
printList(linkedNode);
reverse(linkedNode);
printf("反转后链表:\n");
//打印新的倒置链表
printList(linkedNode);
//仅倒置打印,而不改变原来数据顺序
printf("反转打印链表:\n");
reversePoint(linkedNode);
//free(linkedNode1);
//free(linkedNode2);
//free(linkedNode3);
//free(linkedNode);
freeMemory(linkedNode);
return 0;
}
LinkedNode *init() {
LinkedNode *linkedNode = malloc(sizeof(LinkedNode));
printf("分配的内存地址:%p\n", linkedNode);
//初始化头节点,申请空间,但不存储数据
linkedNode->next = NULL;
return linkedNode;
}
LinkedNode *createNode(int no, char name[], char nickName[]) {
LinkedNode *linkedNode = malloc(sizeof(LinkedNode));
linkedNode->data.no = no;
strcpy(linkedNode->data.name, name);
strcpy(linkedNode->data.nickName, nickName);
printf("分配的内存地址:%p\n", linkedNode);
return linkedNode;
}
void add(LinkedNode *linkedNode, LinkedNode *addNode) {
//遍历找到最后一个节点
LinkedNode *temp = linkedNode;
while (1) {
//如果next为null,则是最后一个
if (temp->next == NULL) {
break;
}
//找下一个节点
temp = temp->next;
}
//重新设置链表指向,加入元素
temp->next = addNode;
addNode->pre = temp;
addNode->next = NULL;
}
int addByAsc(LinkedNode *linkedNode, LinkedNode *addNode) {
if (linkedNode == NULL || addNode == NULL) {
return 0;
}
LinkedNode *temp = linkedNode;
while (1) {
if (temp->next == NULL) {
//还没有元素,直接加入即可
//也可能是最后一个元素
temp->next = addNode;
addNode->pre = temp;
addNode->next = NULL;
return 1;
}
if (temp->next->data.no > addNode->data.no) {
//加入的是最小的元素
//也可能是中间元素
addNode->next = temp->next;
temp->next = addNode;
addNode->pre = temp;
return 1;
} else if (temp->next->data.no == addNode->data.no) {
//重复元素
printf("添加数据重复;重复数据为:%d\n", addNode->data.no);
return 0;
}
temp = temp->next;
}
}
void printList(LinkedNode *linkedNode) {
LinkedNode *s;
s = linkedNode->next;
while (s != NULL) {
printf("编号:%d;\t姓名:%s;\t昵称:%s", s->data.no, s->data.name, s->data.nickName);
s = s->next;
printf("\n");
}
printf("\n");
}
int update(LinkedNode *linkedNode, elemType newHeroNode) {
if (linkedNode == NULL) {
printf("链表为空!\n");
return 0;
}
LinkedNode *temp = linkedNode;
while (temp->next != NULL) {
temp = temp->next;
if (temp->data.no == newHeroNode.no) {
strcpy(temp->data.name, newHeroNode.name);
strcpy(temp->data.nickName, newHeroNode.nickName);
return 1;
}
}
return 0;
}
int delete(LinkedNode *linkedNode, int no) {
if (linkedNode == NULL) {
printf("链表为空\n");
return 0;
}
LinkedNode *temp = linkedNode->next;
while (1) {
if (temp == NULL) {
//已经到最后了
printf("没有找到删除节点编号:%d\n", no);
return 0;
}
if (temp->data.no == no) {
temp->pre->next = temp->next;
//为最后一个节点 防止空指针
if(temp->next != NULL){
temp->next->pre = temp->pre;
}
free(temp);
return 1;
}
temp = temp->next;
}
}
int reverse(LinkedNode *linkedNode) {
if (linkedNode == NULL) {
//传入的链表没有元素
return 0;
}
LinkedNode *newLinkNode = init();
LinkedNode *temp;
//逐渐把linkedNode排空,加到newLinkNode之中
while (linkedNode->next != NULL) {
//保存下一个节点
temp = linkedNode->next;
//断链,扔掉一个节点
linkedNode->next = linkedNode->next->next;
//连接到新链表
temp->next = newLinkNode->next;
newLinkNode->next = temp;
}
linkedNode->next = newLinkNode->next;
free(newLinkNode);
return 1;
}
void reversePoint(LinkedNode *linkedNode) {
if (linkedNode == NULL) {
return;
}
LinkedNode *temp = linkedNode;
if (temp->next != NULL) {
temp = temp->next;
reversePoint(temp);
printf("编号:%d;\t姓名:%s;\t昵称:%s\n", temp->data.no, temp->data.name, temp->data.nickName);
}
}
void freeMemory(LinkedNode *linkedNode) {
//临时记录释放的下一个节点地址
LinkedNode *temp;
//释放的节点地址
LinkedNode *temp2 = linkedNode;
while (temp2->next != NULL) {
temp = temp2->next;
printf("释放的内存地址:%p\n", temp2);
free(temp2);
temp2 = temp;
}
printf("释放的内存地址:%p\n", temp2);
free(temp2);
}
六、约瑟夫环(Josephu)(单向环形链表)
完成:1.创建环形链表;2.约瑟夫出圈顺序
约瑟夫环:设编号为1、2…n的n个人围坐一圈,约定编号为k(1<=k<=n)的人从1开始报数,数到m的人出列,他的下一位又从1开始报数,数到m的那个人又出列,以此类推,直到所有人出列为止,由此产生一个出队编号的序列。
分析:
n=5,即有5个人。
k=1,从第一个人开始报数。
m=2,数两下。
出队顺序:2-4-1-5-3
出圈:
1.需要一个辅助指针helper,指向出圈节点前一个节点。
报数前,初始化helper和frist位置。
2.报数时让helper指针和first指针向前移动m-1次。
3.将first指向的节点出圈。
#include <stdio.h>
#include <stdlib.h>
//创建一个Boy类,表示一个节点
typedef struct BoyNode {
//编号
int no;
//指向下一个节点
struct BoyNode *next;
} BoyNode;
//创造一个节点
BoyNode *createNode(int no);
//创建一个环形链表 num为个数
BoyNode *circleSingleLinked(int num);
//遍历环形链表
void showBoyNode(BoyNode *boyNode);
/**
* 计算出圈顺序
* @param startNo 开始报数的序号
* @param countNum 每次数多少个数
* @param nums 一共多少个节点
* @param boyNode
*/
void countNode(int startNo, int countNum, int nums, BoyNode *boyNode);
//释放内存
void freeMemory(BoyNode *boyNode);
int main() {
BoyNode *boyNode = circleSingleLinked(5);
showBoyNode(boyNode);
countNode(1, 2, 5, boyNode);
//freeMemory(boyNode);
return 0;
}
BoyNode *createNode(int no) {
BoyNode *boyNode = malloc(sizeof(BoyNode));
boyNode->no = no;
printf("分配的内存地址:%p\n", boyNode);
return boyNode;
}
BoyNode *circleSingleLinked(int num) {
if (num < 1) {
printf("传入个数不正确\n");
return NULL;
}
BoyNode *first = NULL;
//辅助指针 帮助构建环形链表
BoyNode *cur = NULL;
for (int i = 1; i <= num; ++i) {
BoyNode *boyNode = createNode(i);
if (i == 1) {
//如果是第一个节点
first = boyNode;
//成环
first->next = first;
//让cur指向当前节点
cur = first;
} else {
//非第一个节点
cur->next = boyNode;
//尾部指向头部
boyNode->next = first;
//当前节点后移
cur = boyNode;
}
}
return first;
}
void showBoyNode(BoyNode *boyNode) {
if (boyNode == NULL) {
printf("环形链表为NULL\n");
return;
}
//因为boyNode不能动,仍然使用一个辅助指针完成遍历
BoyNode *cur = boyNode;
while (cur->next->no != boyNode->no) {
printf("node的编号:%3d\n", cur->no);
//当前节点后移
cur = cur->next;
}
//最后一个节点会被跳过,在打印一次
printf("node的编号:%3d\n", cur->no);
}
void countNode(int startNo, int countNum, int nums, BoyNode *boyNode) {
//校验
if (boyNode == NULL || startNo < 1 || startNo > nums) {
printf("参数校验不合格\n");
return;
}
//辅助指针
BoyNode *helper = boyNode;
//辅助指针指向当前指针前一个节点 定位
while (helper->next->no != boyNode->no) {
helper = helper->next;
}
//报数前初始化起始位置
for (int i = 0; i < startNo - 1; ++i) {
boyNode = boyNode->next;
helper = helper->next;
}
//2.报数时让helper指针和boyNode指针向前移动countNum-1次。
//循环操作,直到圈中只有一个节点
while (1) {
if (helper->no == boyNode->no) {
break;
}
//让helper指针和boyNode指针向前移动countNum-1次
for (int i = 0; i < countNum - 1; ++i) {
boyNode = boyNode->next;
helper = helper->next;
}
//这时boyNode指向的节点 是要出圈的节点
printf("出圈节点的编号:%d\n", boyNode->no);
//出圈
BoyNode *temp = boyNode;
boyNode = boyNode->next;
helper->next = boyNode;
printf("释放的内存地址:%p\n", temp);
free(temp);
}
printf("最后出圈节点的编号:%d\n", boyNode->no);
printf("释放的内存地址:%p\n", boyNode);
free(boyNode);
}
void freeMemory(BoyNode *boyNode) {
if (boyNode == NULL) {
printf("释放内存为空\n");
return;
}
//临时记录释放的下一个节点地址
BoyNode *temp = boyNode;
//释放的节点地址
BoyNode *temp2 = boyNode->next;
//断开循环链表
temp->next = NULL;
while (temp2->next != NULL) {
temp = temp2->next;
printf("释放的内存地址:%p\n", temp2);
free(temp2);
temp2 = temp;
}
//最后一个地址不被释放,再释放一次
printf("释放的内存地址:%p\n", temp2);
free(temp2);
}
七、栈
实现:栈以及其增删查改、一个计算器例子。
1.栈是先入后出的有序列表。
2.栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端为变化的一端,称为栈顶,另一端为固定的一端,称为栈底。
应用场景:
1.子程序调用
2.处理递归调用
3.表达式的转换[中缀表达式转后缀表达式]与求值(实际问题)。
4.二叉树遍历。
5.图形的深度优先(depth-first)搜索法。
栈实现思路分析:
1.使用数组来模拟栈。
2.定义一个top表示栈顶,初始化-1。
3.入栈操作,当有数据加入到栈时,top++,stack[top] = data。
4.出栈操作,int value =stack[top–];return value;
实现综合计算器思路:
1.通过index值来遍历表达式。
2.如果是一个数字,则入数栈。
3.如果是一个符号,则:
如果发现符号栈为空,直接入栈。
如果不为空,则进行比较优先级。如果当前符号的优先级小于或者等于栈中的操作符,就需要从数栈中pop出两个数,从符号栈中pop出一个符号,进行运算,将得到的结果入数栈,把当前的符号入符号栈。
如果当前操作符大于栈中的操作符,就直接入栈不计算。
4.当表达式扫描完毕,就顺序的从数栈和符号栈中pop出响应和符号,并运行。
5.最后在数栈只有一个数据,就是表达式的结果。
/*
* 栈实现以及实现综合计算器
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//定义栈大小
#define MAXSIZE 50
//定义数据类型
typedef int dataType;
typedef struct ArrayStack {
//数组模拟栈,数据存数组
dataType stack[MAXSIZE];
//栈顶
int top;
} ArrayStack;
//初始化一个栈
void initStack(ArrayStack *s);
//栈满返回1 否则0
int isFull(ArrayStack *s);
//栈空返回1 否则0
int isEmpty(ArrayStack *s);
//入栈成功返回1
int push(ArrayStack *s, const dataType *data);
//出栈成功返回1 data为出栈数据
int pop(ArrayStack *s, dataType *data);
//遍历栈 从栈顶遍历
void showStack(ArrayStack *s);
//返回栈顶值
int peek(ArrayStack *s);
/**
* 综合计算器部分
*/
//返回运算符的优先级,由程序员定义,优先级使用数字表示,数字越大,优先级越高
//假定只有+ - * /
int priority(int operate);
//判断是否是一个运算符.是返回1
int isOperate(char val);
int cal(int a, int b, int operate);
int main() {
//栈实现部分
/* dataType temp;
ArrayStack s;
initStack(&s);
char key;
int loop = 1;
while (loop) {
printf("show(s):显示栈\n");
printf("exit(e):退出程序\n");
printf("push(p):添加数据\n");
printf("pop(g):取出数据\n");
printf("请输入你的选择:\n");
scanf_s("%c", &key);
getchar();
switch (key) {
case 's':
showStack(&s);
break;
case 'e':
loop = 0;
break;
case 'p':
printf("请输入一个数:\n");
scanf_s("%d", &temp);
getchar();
push(&s, &temp);
break;
case 'g':
pop(&s, &temp);
printf("出栈的数据为:%d\n", temp);
break;
default:
printf("输入指令有误!!!\n");
break;
}
}
printf("程序退出!!!\n");*/
//计算器实现部分
//定义表达式
char expression[] = "30+2*6-2";
//定义数栈
ArrayStack numStack;
initStack(&numStack);
//定义符号栈
ArrayStack operateStack;
initStack(&operateStack);
//定义相关变量
//用于扫描
int num1 = 0;
int num2 = 0;
int operate = 0;
int res = 0;
int temp = 0;
//每次扫描的char保存在这里
char ch = ' ';
char keepNum[10] = "\0";
for (int i = 0; i < strlen(expression); ++i) {
ch = expression[i];
if (isOperate(ch)) {
//是运算符
//判断符号栈是否为空
if (!isEmpty(&operateStack)) {
//不为空的处理
if (priority(ch) <= priority(peek(&operateStack))) {
//栈顶优先级高;弹出两个数一个符号进行运算
pop(&numStack, &num1);
pop(&numStack, &num2);
pop(&operateStack, &operate);
//计算结果重新入栈
res = cal(num1, num2, operate);
push(&numStack, &res);
//将新符号入符号栈
temp = ch - 0;
push(&operateStack, &temp);
} else {
//直接入符号栈
temp = ch - 0;
push(&operateStack, &temp);
}
} else {
//为空直接入栈
temp = ch - 0;
push(&operateStack, &temp);
}
} else {
//如果数数就直接入数栈
//多位数的处理
keepNum[strlen(keepNum)] = ch;
//判断下一个字符 是数字进行拼接,否则继续扫描
if (isOperate(expression[i + 1]) || expression[i + 1] == '\0') {
//后一位是运算符
temp = strtol(keepNum, NULL, 10);
push(&numStack, &temp);
printf("字符串:%s\n", keepNum);
//清空 -- 全部清空
memset(keepNum, '\0', sizeof(keepNum));
}
}
}
//表达式扫描完毕,依次扫描,进行最后计算
while (1) {
//符号栈为空,则得到计算结果
if (isEmpty(&operateStack)) {
break;
}
pop(&numStack, &num1);
pop(&numStack, &num2);
pop(&operateStack, &operate);
res = cal(num1, num2, operate);;
push(&numStack, &res);
}
printf("表达式%s = %d", expression, res);
return 0;
}
void initStack(ArrayStack *s) {
s->top = -1;
}
int isFull(ArrayStack *s) {
if (s == NULL) {
printf("栈为空\n");
return 1;
}
if (s->top == MAXSIZE - 1) {
return 1;
}
return 0;
}
int isEmpty(ArrayStack *s) {
if (s == NULL) {
printf("栈为空\n");
return 0;
}
if (s->top == -1) {
return 1;
}
return 0;
}
int push(ArrayStack *s, const dataType *data) {
if (isFull(s)) {
printf("栈满,无法放入\n");
return 0;
}
s->stack[++(s->top)] = *data;
return 1;
}
int pop(ArrayStack *s, dataType *data) {
if (isEmpty(s)) {
printf("栈空,无法取出\n");
return 0;
}
*data = s->stack[s->top--];
return 1;
}
void showStack(ArrayStack *s) {
if (isEmpty(s)) {
printf("栈空,无法遍历\n");
return;
}
for (int i = s->top; i >= 0; i--) {
printf("stack[%d]=%d\n", i, s->stack[i]);
}
}
int priority(int operate) {
if (operate == '*' || operate == '/') {
return 1;
} else if (operate == '+' || operate == '-') {
return 0;
} else {
return -1;
}
}
int isOperate(char val) {
return val == '+' || val == '-' || val == '*' || val == '/';
}
int cal(int a, int b, int operate) {
int res = 0;
switch (operate) {
case '+':
res = a + b;
break;
case '-':
res = b - a;
break;
case '*':
res = b * a;
break;
case '/':
res = b / a;
break;
default:
break;
}
return res;
}
int peek(ArrayStack *s) {
return s->stack[s->top];
}
八、前缀(波兰)、中缀、后缀(逆波兰)表达式(关于栈的三大表达式)
1.前缀表达式
1.前缀表达式又称为波兰表达式,前缀表达式的运算符位于操作数之前。
2.举例:(3+4)*5-6;对应的前缀表达式 - * + 3 4 5 6
前缀表达式计算机求值:
1.从右向左扫描,遇到数字时,将数字压栈,遇到运算符时,弹出栈顶两个数,用运算符对其做相应的运算,并将结果入栈。
2.例如(3+4)*5-6;对应的前缀表达式 - * + 3 4 5 6 。
1.从右至左将6、5、4、3入栈;
2.遇到+运算符,弹出3、4,计算得7入栈;
3.遇到*运算符,弹出7、5,计算得35入栈;
4.遇到-运算符,弹出35、6,计算得29入栈;为最终结果。
2.中缀表达式
1.中缀表达式就是普通的运算表达式(3+4)*5-6
2.中缀表达式方便人理解,但不方便计算机理解,一般转成后缀表达式。
3.逆波兰表达式
1.后缀表达式又称为逆波兰表达式与前缀表达式相似,只是运算符位于操作数之后。
2.(3+4)*5-6的后缀表达式:3 4 + 5 * 6 - 再比如:a+(b-c) : a b c - +
后缀表达式计算机求值:
1.遇到数字时,将数字压栈,遇到运算符时,弹出栈顶两个数,用运算符对其做相应的运算,并将结果入栈。
2.例如(3+4)*5-6;对应的后缀表达式 3 4 + 5 * 6 - 。
1.从左至由将3、4入栈;
2.遇到+运算符,弹出3、4,计算得7入栈;
3.将5入栈;
5.遇到*运算符,弹出7、5,计算得35入栈;
7.将6入栈;
8.遇到-运算符,弹出35、6,计算得29入栈;为最终结果。
4.完成逆波兰计算器
1.输入一个逆波兰表达式,使用栈,计算结果。
2.支持小括号和多位数整数。
main.c
/*
* 栈实现以及实现综合计算器
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "header/Stack.h"
//字符数组传递的地址值
void split(char src[], const char *separator, char **dest, int *num);
//计算逆波兰
int calculate(ArrayStack *s, char expression[]);
//判断是否为数字
int isNum(const char *expression);
int main() {
ArrayStack arrayStack;
initStack(&arrayStack);
//定义一个逆波兰表达式
//中缀:4*5-8+60+8/2 改中缀 4 5 * 8 - 60 + 8 2 / +
char expression[] = "4 5 * 8 - 60 + 8 2 / +";
calculate(&arrayStack, expression);
showStack(&arrayStack);
return 0;
}
int isNum(const char *expression) {
for (int i = 0; expression[i] != '\0'; ++i) {
if (!isdigit(expression[i])) {
printf("该字符串不全为数字:%s\n", expression);
return 0;
}
}
return 1;
}
int calculate(ArrayStack *s, char expression[]) {
//指针数组,存放分隔开的数据
char *p[10] = {0};
//数据数量
int num = 0;
//临时记录数字
int temp;
//attention!!!!! 这里的分隔符已定要写为字符串的形式。
split(expression, " ", p, &num);
for (int i = 0; i < num; i++) {
printf("分割出的字符串:%s \n", p[i]);
}
for (int i = 0; i < num; i++) {
if (isNum(p[i])) {
//是数字
temp = strtol(p[i], NULL, 10);
push(s, &temp);
} else {
//pop出两个数并运算 再入栈
int num1;
int num2;
int res;
pop(s, &num2);
pop(s, &num1);
if (strcmp(p[i], "+") == 0) {
res = num2 + num1;
} else if (strcmp(p[i], "-") == 0) {
res = num1 - num2;
} else if (strcmp(p[i], "*") == 0) {
res = num1 * num2;
} else if (strcmp(p[i], "/") == 0) {
res = num1 / num2;
} else {
printf("运算符错误:%s\n", p[i]);
return 0;
}
push(s, &res);
}
}
return 1;
}
void split(char src[], const char *separator, char **dest, int *num) {
char *pNext;
//记录分隔符数量
int count = 0;
//原字符串为空
if (src == NULL || strlen(src) == 0)
return;
//未输入分隔符
if (separator == NULL || strlen(separator) == 0)
return;
/*
c语言string库中函数,
声明:
char *strtok(char *str, const char *delim)
参数:
str -- 要被分解成一组小字符串的字符串。
delim -- 包含分隔符的 C 字符串。
返回值:
该函数返回被分解的第一个子字符串,如果没有可检索的字符串,则返回一个空指针。
*/
//获得第一个由分隔符分割的字符串
pNext = strtok(src, separator);
while (pNext != NULL) {
//存入到目的字符串数组中
*dest++ = pNext;
++count;
/*
strtok()用来将字符串分割成一个个片段。参数s指向欲分割的字符串,参数delim则为分割字符串中包含的所有字符。
当strtok()在参数s的字符串中发现参数delim中包涵的分割字符时,则会将该字符改为\0 字符。
在第一次调用时,strtok()必需给予参数s字符串,往后的调用则将参数s设置成NULL。
每次调用成功则返回指向被分割出片段的指针。
*/
pNext = strtok(NULL, separator);
}
*num = count;
}
Stack.h
//
// Created by 16665 on 2024/1/17.
//
#ifndef REVERSEPOLAND_STACK_H
#define REVERSEPOLAND_STACK_H
#endif //REVERSEPOLAND_STACK_H
#include <stdio.h>
//定义栈大小
#define MAXSIZE 50
//定义数据类型
typedef int dataType;
typedef struct ArrayStack {
//数组模拟栈,数据存数组
dataType stack[MAXSIZE];
//栈顶
int top;
} ArrayStack;
//初始化一个栈
void initStack(ArrayStack *s);
//栈满返回1 否则0
int isFull(ArrayStack *s);
//栈空返回1 否则0
int isEmpty(ArrayStack *s);
//入栈成功返回1
int push(ArrayStack *s, const dataType *data);
//出栈成功返回1 data为出栈数据
int pop(ArrayStack *s, dataType *data);
//遍历栈 从栈顶遍历
void showStack(ArrayStack *s);
//返回栈顶值
int peek(ArrayStack *s);
Stack.c
#include "../header/Stack.h"
void initStack(ArrayStack *s) {
s->top = -1;
}
int isFull(ArrayStack *s) {
if (s == NULL) {
printf("栈为空\n");
return 1;
}
if (s->top == MAXSIZE - 1) {
return 1;
}
return 0;
}
int isEmpty(ArrayStack *s) {
if (s == NULL) {
printf("栈为空\n");
return 0;
}
if (s->top == -1) {
return 1;
}
return 0;
}
int push(ArrayStack *s, const dataType *data) {
if (isFull(s)) {
printf("栈满,无法放入\n");
return 0;
}
s->stack[++(s->top)] = *data;
return 1;
}
int pop(ArrayStack *s, dataType *data) {
if (isEmpty(s)) {
printf("栈空,无法取出\n");
return 0;
}
*data = s->stack[s->top--];
return 1;
}
void showStack(ArrayStack *s) {
if (isEmpty(s)) {
printf("栈空,无法遍历\n");
return;
}
for (int i = s->top; i >= 0; i--) {
printf("stack[%d]=%d\n", i, s->stack[i]);
}
}
int peek(ArrayStack *s) {
return s->stack[s->top];
}
5.中缀表达式转后缀表达式
1.初始化两个栈:运算符s1和中间结果存储栈s2;
2.从左到右扫描中缀表达式。
3.遇到操作数压栈s2;
4.遇到运算符时,比较与s1栈顶运算符的优先级:
1.如果s1为空,或者栈顶运算符为左括号,直接入符号栈。
2.否则,若优先级比栈顶元素高,也将运算符压栈。
3.否则,将s1栈顶的运算符弹出并压入到s2中,再次转到4-1与s1中的新的栈顶运算符相比较。
5.遇到括号时:
1.如果是左括号,直接入符号栈s1;
2.如果是右括号,依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃。
6.重复2-5,知道表达式的最右边。
7.将s1中剩余的运算符依次弹出并压入s2;
8.依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式。
main.c
/**
* 中缀转后缀 -- 考虑多位数
*/
#define MAXSIZEARR 100
#include <stdio.h>
#include <string.h>
#include "header/Stack.h"
#include <ctype.h>
//将表达式转化为数字(多位数整合到一起)和符号,用空格隔开,方便后续表达式转换操作
//如"10+((2+3)*4)-5" -- 10 + ( ( 2 + 3 ) * 4 ) - 5
void toInfixExpArr(char expression[], char dest[]);
//将分割好的字符数组转化为后缀表达式
void parseSuffixExpressionArr(char src[], char dest[]);
//判断传入的字符串是不是数组
int isNum(const char *expression);
//返回优先级
int precedence(char symbol);
int main() {
char p[MAXSIZEARR] = "\0";
char dest[MAXSIZEARR] = "\0";
//完成中缀转后缀
//1+((2+3)*4)-5 -- 1 2 3 + 4 * + 5
char expression[] = "10+((2+3)*4)-5";
toInfixExpArr(expression, p);
printf("字符串:%s\n", p);
parseSuffixExpressionArr(p, dest);
printf("");
printf("后缀表达式:%s\n", dest);
return 0;
}
void toInfixExpArr(char expression[], char dest[]) {
//指针
int i = 0;
//遍历到的每个字符
char c;
do {
c = expression[i];
if (!isdigit(c)) {
//不是数字是符号 -- 只有一位
dest[strlen(dest)] = c;
dest[strlen(dest)] = ' ';
i++;
} else {
while (i < strlen(expression) && isdigit(c)) {
//是数字,考虑多位数问题
dest[strlen(dest)] = c;
i++;
c = expression[i];
}
dest[strlen(dest)] = ' ';
}
} while (i < strlen(expression));
}
//返回优先级
int precedence(char symbol) {
if (symbol == '*' || symbol == '/') {
return 2;
} else if (symbol == '+' || symbol == '-') {
return 1;
} else {
//printf("不存在该运算符\n");
return 0;
}
}
void parseSuffixExpressionArr(char src[], char dest[]) {
//需要两个栈
ArrayStack s1;//符号栈
initStack(&s1);
//存放src分割后的临时数据
char *pNext;
char temp = '\0';
int index = 0;
//获得第一个由分隔符分割的字符串
pNext = strtok(src, " ");
while (pNext != NULL) {
if (isNum(pNext)) {
//是数字
for (int i = 0; i < strlen(pNext); ++i) {
dest[index++] = pNext[i];
}
dest[index++] = ' ';
} else if ((strcmp(pNext, "(") == 0)) {
//左括号直接入栈
push(&s1, &pNext[0]);
} else if ((strcmp(pNext, ")") == 0)) {
//是右括号,依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃。
while (peek(&s1) != '(') {
pop(&s1, &temp);
dest[index++] = temp;
dest[index++] = ' ';
}
//消除(
pop(&s1, &temp);
} else {
//当pNext的优先级小于等于栈顶运算符优先级,将s1栈顶的运算符弹出并压入到s2中,再次转到4-1与s1中的新的栈顶运算符相比较
temp = peek(&s1);
while (!isEmpty(&s1) && precedence(temp) >= precedence(pNext[0])) {
//栈顶优先级高
pop(&s1, &temp);
dest[index++] = temp;
dest[index++] = ' ';
}
push(&s1, &pNext[0]);
}
pNext = strtok(NULL, " ");
}
while (!isEmpty(&s1)) {
pop(&s1, &temp);
dest[index++] = temp;
dest[index++] = ' ';
}
}
int isNum(const char *expression) {
for (int i = 0; expression[i] != '\0'; ++i) {
if (!isdigit(expression[i])) {
//printf("该字符串不全为数字:%s\n", expression);
return 0;
}
}
return 1;
}
Stack.h 修改了存放的数据类型
//
// Created by 16665 on 2024/1/17.
//
#ifndef REVERSEPOLAND_STACK_H
#define REVERSEPOLAND_STACK_H
#endif //REVERSEPOLAND_STACK_H
#include <stdio.h>
//定义栈大小
#define MAXSIZE 50
//定义数据类型
typedef char dataType;
typedef struct ArrayStack {
//数组模拟栈,数据存数组
dataType stack[MAXSIZE];
//栈顶
int top;
} ArrayStack;
//初始化一个栈
void initStack(ArrayStack *s);
//栈满返回1 否则0
int isFull(ArrayStack *s);
//栈空返回1 否则0
int isEmpty(ArrayStack *s);
//入栈成功返回1
int push(ArrayStack *s, const dataType *data);
//出栈成功返回1 data为出栈数据
int pop(ArrayStack *s, dataType *data);
//遍历栈 从栈顶遍历
void showStack(ArrayStack *s);
//返回栈顶值
dataType peek(ArrayStack *s);
Stack.c
#include "../header/Stack.h"
void initStack(ArrayStack *s) {
s->top = -1;
}
int isFull(ArrayStack *s) {
if (s == NULL) {
printf("栈为空\n");
return 1;
}
if (s->top == MAXSIZE - 1) {
return 1;
}
return 0;
}
int isEmpty(ArrayStack *s) {
if (s == NULL) {
printf("栈为空\n");
return 0;
}
if (s->top == -1) {
return 1;
}
return 0;
}
int push(ArrayStack *s, const dataType *data) {
if (isFull(s)) {
printf("栈满,无法放入\n");
return 0;
}
s->stack[++(s->top)] = *data;
return 1;
}
int pop(ArrayStack *s, dataType *data) {
if (isEmpty(s)) {
printf("栈空,无法取出\n");
return 0;
}
*data = s->stack[s->top--];
return 1;
}
void showStack(ArrayStack *s) {
if (isEmpty(s)) {
printf("栈空,无法遍历\n");
return;
}
for (int i = s->top; i >= 0; i--) {
printf("stack[%d]=%d\n", i, s->stack[i]);
}
}
dataType peek(ArrayStack *s) {
return s->stack[s->top];
}
九、递归
1.迷宫回溯问题
递归就是方法自己调用自己,每次调入传入不同的变量,递归有助于编程者解决复杂问题。
可以解决的问题:
1.各种数学问题:如八皇后、汉诺塔、阶乘问题、迷宫问题、球和篮子问题。
2.各种算法:快排、归并排序、二分查找、分治算法等;
3.用栈解决的问题。
递归需要遵守的重要原则:
1.执行一个方法时,就创建一个新的受保护的独立空间(栈空间)。
2.方法的局部变量是独立的,不会相互影响。
3.递归必须向退出递归的条件逼近。
4.当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。
迷宫问题实现:
/**
* 迷宫问题
*/
#include <stdio.h>
#include <string.h>
//定义行
#define ROW 8
//定义列
#define COL 7
//创建一个二维数组并初始化
void creatArr2(int map[ROW][COL]);
//打印二维数组
void showArr2(int map[ROW][COL]);
//使用递归给小球找路,从i j开始找 找到返回1
//出发点:(1,1)
//小球能到:(ROW-2,COL-2)时表示通路
//约定:0未走过;1表示墙;2表示通路可以走;3表示已经走过但不通
//在走迷宫时定义策略,下->右->上->左。如果走不通再回溯
int setWay(int map[ROW][COL], int i, int j);
int main() {
//创建一个二维数组,模拟迷宫
int map[ROW][COL] = {{0}};
creatArr2(map);
//设置挡板
map[3][1] = 1;
map[3][2] = 1;
map[3][3] = 1;
map[3][4] = 1;
//map[3][5] = 1;
setWay(map, 1, 1);
showArr2(map);
return 0;
}
void creatArr2(int map[ROW][COL]) {//使用1表示墙
for (int i = 0; i < COL; ++i) {
map[0][i] = 1;
map[ROW - 1][i] = 1;
}
for (int i = 0; i < ROW; ++i) {
map[i][0] = 1;
map[i][COL - 1] = 1;
}
}
void showArr2(int map[ROW][COL]) {
for (int i = 0; i < ROW; ++i) {
for (int j = 0; j < COL; ++j) {
printf("%d ", map[i][j]);
}
printf("\n");
}
}
int setWay(int map[ROW][COL], int i, int j) {
if (map[6][1] == 2) {
return 1;
}
if (map[i][j] == 0) {
//策略下->右->上->左
//假定该点是可以走通的
map[i][j] = 2;
if (setWay(map, i + 1, j)) {
//如果向下走可以通
return 1;
} else if (setWay(map, i, j + 1)) {
//向右走
return 1;
} else if (setWay(map, i - 1, j)) {
//向上
return 1;
} else if (setWay(map, i, j - 1)) {
//向左
return 1;
} else {
//走不通的点
map[i][j] = 3;
return 0;
}
} else {
//不为0 可能1 2 3
return 0;
}
}
1 1 1 1 1 1 1
1 2 0 0 0 0 1
1 2 2 2 2 2 1
1 1 1 1 1 2 1
1 2 2 2 2 2 1
1 2 2 2 2 2 1
1 2 2 2 2 2 1
1 1 1 1 1 1 1
2.八皇后问题
八皇后问题是回溯算法的典型案例,在8X8的国际象棋上摆放8个皇后,使其不能处于同行、同列、同斜线;问有多少种解法。
思路:
1.将第一个皇后先放到第一行第一列。
2.第二个皇后放到第二行第一列,判断是否合适,不合适继续放到第二列,第三列…直到找到一个合适的位置。
3.继续第三个皇后,从第三行第一列开始放,直到找到合适的位置。
4.得到一个正确的解时,在栈回退到上一个栈时,开始回溯,即将第一个皇后,放到第一列所有的正确解全部得到。
5.然后回头继续将第一个皇后放到第一行第二列,继续1234步骤。
/**
* 八皇后问题 回溯算法
*/
#include <stdio.h>
#include <math.h>
//共有8个皇后
#define MAX 8
//保存皇后位置的结果,位置表示第几个皇后,值表示放在第几列
int array[MAX] = {0};
//统计次数
int count = 0;
//将摆放位置输出
void show();
/**
* 放置第n个皇后时,检测和前面的是否冲突
* @param n 第n个皇后
* @return 0冲突 1不冲突
*/
int judge(int n);
//放置第n个皇后
//每一轮递归都有一套for循环,摆满每一列位置去判断
void check(int n);
int main() {
check(0);
printf("次数:%d\n", count);
return 0;
}
void show() {
count++;
for (int i = 0; i < MAX; ++i) {
printf("%d ", array[i]);
}
printf("\n");
}
int judge(int n) {
for (int i = 0; i < n; ++i) {
//array[i] == array[n] 判断是否与之前的同列
//abs(n - i) == abs(array[n] - array[i]) 判断是否与之前的同斜线(重点理解)
//不需要判断同一行
if (array[i] == array[n] || abs(n - i) == abs(array[n] - array[i])) {
return 0;
}
}
return 1;
}
void check(int n) {
if (n == MAX) {
//第八个皇后已经放好了
show();
return;
}
//依次放入皇后,并判断是否冲突
for (int i = 0; i < MAX; ++i) {
//先把这个皇后放到该行的第i列,从第一列开始
array[n] = i;
//判断是否冲突
if (judge(n)) {
//不冲突
check(n + 1);
}
//如果冲突,就继续执行array[n] = i;这时候i已经++,开始放到下一列判断
}
}
十、算法的时间、空间复杂度
1.度量一个程序执行时间的两种方法
1.事后统计法:需要控制变量统计执行时间。
2.事前估算方法:通过分析某个散发的时间复杂度来判断算法优势。
2.时间频度(一个算法执行语句的多少)
一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
时间频度忽略常数项、忽略低次项、忽略系数。
3.时间复杂度
1)一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n) ),称 O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
2)T(n)不同,但时间复杂度可能相同。如:T(n)=n2+7n+6与T(n)=3n2+2n+2它们的T(n)不同,但时间复杂度相同,都为0(n^2)。
3)计算时间复杂度的方法:
用常数1代替运行时间中的所有加法常数修改后的运行次数函数中,只保留最高阶项
去除最高阶项的系数
4.常见的时间复杂度
时间复杂度从小到大:
1.常数阶O(1)
无论代码执行了多少行,没有循环等复杂结构,那代码的时间复杂度就是O(1);时间消耗不随变量的增长而增长。
2.对数阶O(log2n)
int i = 1, n = 1000;
while (i < n) {
i = i * 2;
}
3.线性阶O(n)
单纯的for循环就是线性阶,代码执行n遍。
4.线性对数阶O(nlog2n)
int n = 1000;
for (int j = 0; j < n; ++j) {
int i = 1;
while (i < n) {
i = i * 2;
}
}
对数阶的代码执行n遍。
5.平方阶O(n^2)
双层for循环就是平方阶。
6.立方阶O(n^3)
三层for循环就是立方阶。
7.k次方阶O(n^k)
k层for循环就是k次方阶。
8.指数阶O(2^n)(尽量避免)
9.O(n!)
5.平均时间复杂度和最坏时间复杂度
1)平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
2)最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。
这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长。
3)平均时间复杂度和最坏时间复杂度是否一致,和算法有关。
排序法 | 平均时间 | 最差情形 | 稳定度 | 额外空间 | 备注 |
---|---|---|---|---|---|
冒泡 | O(n^2) | O(n^2) | 稳定 | O(1) | n小时较好 |
交换 | O(n^2) | O(n^2) | 不稳定 | O(1) | n小时较好 |
选择 | O(n^2) | O(n^2) | 不稳定 | O(1) | n小时较好 |
插入 | O(n^2) | O(n^2) | 稳定 | O(1) | 大部分已排序较好 |
基数 | O(logRB) | O(logRB) | 稳定 | O(n) | B是真数(1-9);R是基数(个十百) |
Shell | O(nlogn) | O(n^s) 1<s<2 | 不稳定 | O(1) | s是所选分组 |
快速 | O(nlogn) | O(n^2) | 不稳定 | O(nlogn) | n大时较好 |
归并 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时较好 |
堆 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时较好 |
6.算法空间复杂度基本介绍
1.类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
2.空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况。
3.在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis,memcache)和算法(基数排序)本质就是用空间换时间.
十一、排序算法
排序:
1.内部排序:在内存中完成。
1.插入排序(直接插入、希尔)
2.选择排序(简单排序、堆排序)
3.交换排序(冒泡排序、快速排序)
4.归并排序
5.基数排序
2.外部排序:数据量大时,无法全部加载到内存中,需要借助外部存储进行排序。
1.冒泡排序
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而减少不必要的比较。
属于内部排序法。
冒泡排序规则:
1.一共进行数组大小-1次的循环。
2.每一趟排序的次数逐渐减小。
3.某趟排序中,没有发生交换,可以提前结束排序。
/**
* 1.完成冒泡排序
* 2.完成中间结果输出
* 3.完成事后时间统计
*
* 事前时间复杂度:O(n^2)
* 事后80000个数据排序大致耗时:14s
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//输出中间的排序结果,便于观察排序过程
void showArr(int arr[], int j, int size);
//依次将最大的数往后挪
void bubbleSort(int *arr, int size);
int main() {
int arr[] = {3, 9, -1, 10, 20};
int size = sizeof(arr) / sizeof(int);
bubbleSort(arr, size);
//事后统计执行时间 -- 测试一下80000个随机数时间
srand(time(NULL));
int arrTestTime[80000];
for (int i = 0; i < 80000; ++i) {
arrTestTime[i] = rand();
}
time_t t_start, t_end;
t_start = time(NULL);
bubbleSort(arrTestTime, 80000);
t_end = time(NULL);
printf("\n耗费时间time: %.0f s\n", difftime(t_end, t_start));
//bubbleSort(arr, size);
return 0;
}
void bubbleSort(int *arr, int size) {
//交换用
int temp = 0;
//标记是否进行过交换
int flag = 0;
//时间复杂度O(n^2)
for (int j = 0; j < size - 1; ++j) {
for (int i = 0; i < size - 1 - j; ++i) {
//如果前面的数比后面的数大,则交换
if (arr[i] > arr[i + 1]) {
flag = 1;
temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
}
//showArr(arr, j, size);
if (!flag) {
//一次交换都没发生,直接退出,已经有序了
break;
}
//重置
flag = 0;
}
}
void showArr(int arr[], int j, int size) {
printf("\n第%d次排序的输出结果:", j + 1);
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
}
2.选择排序
选择式排序也属于内部排序算法,是从欲排序的数据中,按制定的规则选出来某一元素,再依规定交换位置后达到排序的目的。
排序思想:
选择排序(select sorting)也是一种简单的排序方法。它的基本思想是:
第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换,
第二从arr[1]~arr[n-1]中选取最小值,与arr[1]交换,
第三次从arr[2]~arr[n-1]中选取最小值,与arr[2]交换,
…
第i次从arr[i-1]~arr[n-1]中选取最小值,与arr[i-1]交换,
…
第n-1次从arr[n-2]arr[n-1]中选取最小值,与arr [n-2]交换,
总共通过n-1次,得到一个按排序码从小到大排列的有序序列。
/**
* 1.完成选择排序
* 2.完成中间结果输出
* 3.完成事后时间统计
*
* 事前时间复杂度:O(n^2)
* 事后80000个数据排序大致耗时:6s
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//选择排序
void selectSort(int *arr, int size);
//输出中间的排序结果,便于观察排序过程
void showArr(int arr[], int j, int size);
int main() {
int arr[] = {101, 34, 119, 1};
int size = sizeof(arr) / sizeof(int);
selectSort(arr, size);
//事后统计执行时间 -- 测试一下80000个随机数时间
srand(time(NULL));
int arrTestTime[80000];
for (int i = 0; i < 80000; ++i) {
arrTestTime[i] = rand();
}
time_t t_start, t_end;
t_start = time(NULL);
selectSort(arrTestTime, 80000);
t_end = time(NULL);
printf("\n耗费时间time: %.0f s\n", difftime(t_end, t_start));
return 0;
}
void selectSort(int *arr, int size) {
//先假定
for (int j = 0; j < size - 1; ++j) {
int minIndex = j;
int min = arr[j];
for (int i = j + 1; i < size; ++i) {
if (min > arr[i]) {
//说明之前的min非最小值
//重置最小值min
min = arr[i];
//重置最小值序号
minIndex = i;
}
}
//将最小值交换位置
arr[minIndex] = arr[j];
arr[j] = min;
//打印中间过程
//showArr(arr, j, size);
}
}
void showArr(int arr[], int j, int size) {
printf("\n第%d次排序的输出结果:", j + 1);
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
}
耗费时间time: 6 s
3.插入排序
插入式排序属于内部排序法,是对于欲排序的元素以插入的方式找寻该元素的适当位置,以达到排序的目的。
插入排序(Insertion Sorting)的基本思想是:
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含有n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
/**
* 1.完成插入排序
* 2.完成中间结果输出
* 3.完成事后时间统计
*
* 事前时间复杂度:O(n^2)
* 事后80000个数据排序大致耗时:3s
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//插入排序
void insertSort(int *arr, int size);
//输出中间的排序结果,便于观察排序过程
void showArr(int arr[], int j, int size);
int main() {
int arr[] = {101, 34, 119, 1};
int size = sizeof(arr) / sizeof(int);
insertSort(arr, size);
//事后统计执行时间 -- 测试一下80000个随机数时间
srand(time(NULL));
int arrTestTime[80000];
for (int i = 0; i < 80000; ++i) {
arrTestTime[i] = rand();
}
time_t t_start, t_end;
t_start = time(NULL);
insertSort(arrTestTime, 80000);
t_end = time(NULL);
printf("\n耗费时间time: %.0f s\n", difftime(t_end, t_start));
return 0;
}
void showArr(int arr[], int j, int size) {
printf("\n第%d次排序的输出结果:", j + 1);
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
}
void insertSort(int *arr, int size) {
for (int i = 1; i < size; ++i) {
//待插入的数
int insertVal = arr[i];
//比较数的索引
int insertIndex = i - 1;
//保障不越界
//待插入的数还没有找到适当的位置,需要将arr[insertIndex]后移
while (insertIndex >= 0 && insertVal < arr[insertIndex]) {
arr[insertIndex + 1] = arr[insertIndex];
insertIndex--;
}
//当退出循环时,说明找到了位置为insertIndex+1
arr[insertIndex + 1] = insertVal;
//showArr(arr, i - 1, size);
}
}
耗费时间time: 3 s
插入排序存在的问题:
当时从小到大排序时,插入的数较小时,数组中后移的元素明显增多,对效率有影响。
4.希尔排序(升级后的插入排序)
希尔排序是希尔(Donaldshell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
希尔排序法基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
比如十个数,按照间隔分为5组,每组两个数据,比较大小交换位置。
按索引位置分为10/2=5组:0/5,1/6,2/7,3/8,4/9五组。组内比较大小,交换位置。
再按索引位置分为5/2=2组:0/2/4/6/8,1/3/5/7/9两组,组内比较大小交换位置。
再按索引位置分为2/2=1组:0/1/2/3/4/5/6/7/8/9一组,比较大小交换位置。
每次分组都使得小数字更接近于位置。
希尔排序分为:
1.交换法
2.移动法(效率更高一些)
交换法实现:
/**
* 1.完成希尔排序
* 2.完成中间结果输出
* 3.完成事后时间统计
*
* 事前时间复杂度:O(nlogn)
* 事后80000个数据排序大致耗时11s
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//希尔排序(交换法)
void shellSort(int *arr, int size);
//输出中间的排序结果,便于观察排序过程
void showArr(int arr[], int j, int size);
int main() {
int arr[] = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};
int size = sizeof(arr) / sizeof(int);
shellSort(arr, size);
//事后统计执行时间 -- 测试一下80000个随机数时间
srand(time(NULL));
int arrTestTime[80000];
for (int i = 0; i < 80000; ++i) {
arrTestTime[i] = rand();
}
time_t t_start, t_end;
t_start = time(NULL);
shellSort(arrTestTime, 80000);
t_end = time(NULL);
printf("\n耗费时间time: %.0f s\n", difftime(t_end, t_start));
return 0;
}
void showArr(int arr[], int j, int size) {
printf("\n第%d次排序的输出结果:", j + 1);
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
}
void shellSort(int *arr, int size) {
//重点在于理解这个步进值
int temp = 0;
int count = 0;
for (int gap = size / 2; gap > 0; gap /= 2) {
for (int i = gap; i < size; ++i) {
//遍历各组中所有元素(共gap组,每组size/gap个元素),步长为gap
for (int j = i - gap; j >= 0; j -= gap) {
//如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
//showArr(arr, count, size);
count++;
}
}
耗费时间time: 11 s
移动法
/**
* 1.完成希尔排序
* 2.完成中间结果输出
* 3.完成事后时间统计
*理解:每次进行两个数据的数据交换是费时的操作;只是移动,找到合适位置再交换会节约很多时间
* 事前时间复杂度:O(nlogn)
* 事后80000个数据排序大致耗时21ms
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//希尔排序,移位法
void shellSort2(int *arr, int size);
//输出中间的排序结果,便于观察排序过程
void showArr(int arr[], int j, int size);
int main() {
int arr[] = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0, 19, 12};
int size = sizeof(arr) / sizeof(int);
shellSort2(arr, size);
//事后统计执行时间 -- 测试一下80000个随机数时间
srand(time(NULL));
int arrTestTime[80000];
for (int i = 0; i < 80000; ++i) {
arrTestTime[i] = rand();
}
// 获取起始时间
struct timespec start_time;
clock_gettime(CLOCK_MONOTONIC, &start_time);
shellSort2(arrTestTime, 80000);
// 获取结束时间
struct timespec end_time;
clock_gettime(CLOCK_MONOTONIC, &end_time);
// 计算时间差
long seconds = end_time.tv_sec - start_time.tv_sec;
long nanoseconds = end_time.tv_nsec - start_time.tv_nsec;
// 将纳秒转换为毫秒
long milliseconds = nanoseconds / 1000000;
printf("\n耗费时间time: %ld s;共计%ldms\n", seconds, milliseconds);
return 0;
}
void showArr(int arr[], int j, int size) {
printf("\n第%d次排序的输出结果:", j + 1);
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
}
void shellSort2(int *arr, int size) {
//重点在于理解这个步进值
int count = 0;
for (int gap = size / 2; gap > 0; gap /= 2) {
//从第gap个元素,逐个对其所造的组进行直接插入排序
//举例:第一轮gap =5 分为5组
//第二轮gap =2 分为2组
for (int i = gap; i < size; ++i) {
//从gap 开始,往后直到遍历完整个数组,将每组更小的值往Ian挪动,
//举例,第一次将该组内最小的值移到该组第一个位置
int j = i;
int tempVal = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && tempVal < arr[j - gap]) {
//移动 用插入排序一直到到同一组内合适的位置,采用的是移动法
//同一组内的移动,第一次完成同组内最小数的排序,第二次完成第二小数值的排序;依次下去
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = tempVal;
}
}
if (gap == 1) {
//showArr(arr, count, size);
}
count++;
}
}
耗费时间time: 0 s;共计21ms
5.快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
以中轴为界,不断分为两组数据递归,左边的都比中轴小,右边的都比中轴大。需要注意边界索引的处理。
-9, 78, 0, 23, -567, 70
一轮:-9 -567 0 23 78 70 left = 0;r = 1;l=3;right = 5
二轮(左递归):-567 -9 0 23 78 70 left = 0;r = 0;l=2;right = 1
二轮(右递归):-567 -9 0 23 70 78 left = 3;r = 4;l=6;right = 5
三轮(左递归):-567 -9 0 23 70 78 left = 3;r = 2;l=4;right = 4
/**
* 1.完成快速排序
* 2.完成中间结果输出
* 3.完成事后时间统计
*
* 理解:每次对数组进行分组,其中一个数组的所有元素均小于另一个数组,分到最后为有序数组,对于和对比值相当的值,放在左边右边都不影响
*
* 事前时间复杂度:O(nlogn)
* 事后80000个数据排序大致耗时8ms
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef int i;
//输出中间的排序结果,便于观察排序过程
void showArr(int arr[], int size);
//快速排序法
void quickSort(int *arr, int left, int right, int size);
int main() {
int arr[] = {-9, 78, 0, 23, -567, 70};
int size = sizeof(arr) / sizeof(int);
quickSort(arr, 0, size - 1, size);
//事后统计执行时间 -- 测试一下80000个随机数时间
srand(time(NULL));
int arrTestTime[80000];
for (int i = 0; i < 80000; ++i) {
arrTestTime[i] = rand();
}
// 获取起始时间
struct timespec start_time;
clock_gettime(CLOCK_MONOTONIC, &start_time);
quickSort(arrTestTime, 0, 80000 - 1, 80000);
// 获取结束时间
struct timespec end_time;
clock_gettime(CLOCK_MONOTONIC, &end_time);
// 计算时间差
long seconds = end_time.tv_sec - start_time.tv_sec;
long nanoseconds = end_time.tv_nsec - start_time.tv_nsec;
// 将纳秒转换为毫秒
long milliseconds = nanoseconds / 1000000;
printf("\n耗费时间time: %ld s;共计%ldms\n", seconds, milliseconds);
return 0;
}
void showArr(int arr[], int size) {
printf("\n");
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
}
void quickSort(int *arr, int left, int right, int size) {
//左下标
int l = left;
//右下标
int r = right;
//中轴
int pivot = arr[(left + right) / 2];
//作为交换时使用
int temp = 0;
//目的是比pivot小的值放到左边,大的值放到右边
while (l < r) {
//在pivot左边一直找到大于等于pivot,才退出
while (arr[l] < pivot) {
l++;
}
//在pivot左边一直找到小于等于pivot,才退出
while (arr[r] > pivot) {
r--;
}
//已经按照左边全部是小于等于pivot,右边大于等于pivot值
if (l >= r) {
break;
}
//找到了两个值,交换
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
//交换完后,发现arr[l] == pivot值,r前移,为了推进,从而退出while循环
if (arr[l] == pivot) {
r--;
}
//交换完后,发现arr[r] == pivot值,l后移,为了推进,退出while循环
if (arr[r] == pivot) {
l++;
}
}
//递归 如果l == r ,必须l++,r--,否则栈溢出
if (l == r) {
l++;
r--;
}
//showArr(arr, size);
//向左递归
if (left < r) {
quickSort(arr, left, r, size);
}
//向右递归
if (l < right) {
quickSort(arr, l, right, size);
}
}
耗费时间time: 0 s;共计8ms
6.归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
8、4、5、7、1、3、6、2
分:
1.8、4、5、7 和 1、3、6、2
2.8、4和5、7以及1、3和6、2
3.8和4以及5和7以及1和3以及6和2
治:
1.4、8和5、7以及1、3和2、6
2.4、5、7、8和1、2、3、6
3.1、2、3、4、5、6、7、8
先分再合,合并多次。
2到3的合并:指针初始指向4和1,比较大小加入新数组,加入新数组的指针后移再次比较,直至全部加入数组。
/**
* 1.完成归并排序
* 2.完成中间结果输出
* 3.完成事后时间统计
*
* 理解:分治算法 先分解直到不能分解,然后合并,在合并的过程中进行排序处理
*
* 事前时间复杂度:O(nlogn)
* 事后80000个数据排序大致耗时12ms
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//输出中间的排序结果,便于观察排序过程
void showArr(int arr[], int size);
/**
* 合并的方法
* @param arr 需要排序的数组
* @param left 左边有序序列的初始索引
* @param right 右边有序序列索引
* @param mid 中间索引
* @param temp 做中转的数组
*/
void merge(int arr[], int left, int right, int mid, int temp[]);
//分解以及开始合并的处理
void decompose(int arr[], int left, int right, int temp[]);
int main() {
int arr[] = {8, 4, 5, 7, 1, 3, 6, 2};
int size = sizeof(arr) / sizeof(int);
int temp[size];
decompose(arr, 0, size - 1, temp);
showArr(arr, 8);
// //事后统计执行时间 -- 测试一下80000个随机数时间
// srand(time(NULL));
// int arrTestTime[80000];
// for (int i = 0; i < 80000; ++i) {
// arrTestTime[i] = rand();
// }
//
// // 获取起始时间
// struct timespec start_time;
// clock_gettime(CLOCK_MONOTONIC, &start_time);
//
// int temp_k[80000];
// decompose(arrTestTime, 0, 80000 - 1, temp_k);
// // 获取结束时间
// struct timespec end_time;
// clock_gettime(CLOCK_MONOTONIC, &end_time);
// // 计算时间差
// long seconds = end_time.tv_sec - start_time.tv_sec;
// long nanoseconds = end_time.tv_nsec - start_time.tv_nsec;
//
// // 将纳秒转换为毫秒
// long milliseconds = nanoseconds / 1000000;
// printf("\n耗费时间time: %ld s;共计%ldms\n", seconds, milliseconds);
return 0;
}
void showArr(int arr[], int size) {
printf("\n");
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
}
void merge(int arr[], int left, int right, int mid, int temp[]) {
//初始化i 左边有序序列的初始索引
int i = left;
//初始化j 右边有序序列的初始索引
int j = mid + 1;
//指向temp数组的当前索引
int t = 0;
//1.先把左右两边(有序)的数据按照规则填充到temp,直到左右两边有序序列又一边处理完成为止
//2.将还有数据的一方,全部填充到temp
//3.将temp数组拷贝到arr
while (i <= mid && j <= right) {
//左边有序序列的元素小于等于右边有序序列的当前元素,将左边的当前元素拷贝到temp数组,后移指针
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t++;
i++;
} else {
//反之拷贝右边
temp[t] = arr[j];
t++;
j++;
}
}
//剩余元素的处理
while (i <= mid) {
temp[t] = arr[i];
t++;
i++;
}
while (j <= right) {
temp[t] = arr[j];
t++;
j++;
}
//不是拷贝所有数据,只拷贝当次处理的数据段儿
t = 0;
int tempLeft = left;
printf("left=%d;right=%d\n", tempLeft, right);
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
void decompose(int arr[], int left, int right, int temp[]) {
if (left < right) {
int mid = (left + right) / 2;
//向左递归进行分解
decompose(arr, left, mid, temp);
//向右递归分解
decompose(arr, mid + 1, right, temp);
//一直递归分解,到不能分解了就合并
merge(arr, left, right, mid, temp);
}
}
//9个数的分解和合并序号过程
left=0;right=1
left=2;right=3
left=0;right=3
left=4;right=5
left=6;right=7
left=4;right=7
left=0;right=7
1 2 3 4 5 6 7 8
7.基数排序(桶排序)
1)基数排序(radixsort)属于“分配式排序”(distributionsort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用。
2)基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法
3)基数排序(RadixSort)是桶排序的扩展
4)基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
基数排序基本思想
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
基数排序以空间换时间的经典算法。
思想解释:
1.数组的初始状态{53,3,542,748,14,214}。
2.每个桶是一个数组,共十个桶。
3.第一轮排序,将每个元素的个位数取出,然后看这个数应该放在哪个对应的桶。比如53,个位数为3,则53放在下标为3的桶,524放在下标为2的桶。
4.按照这个桶的顺序(一维数组的下标)依次取出数据,放入原来的数组。数据变为{542,53,3,14,214,748}
5.第二轮排序,和第一轮(3)思想相同,但是是按照数字的十位数放入桶中。比如53,十位数为3,则53放在下标为5的桶;3十位数没有,补0,放入0号桶。
6.按照这个桶的顺序(一维数组的下标)依次取出数据,放入原来的数组。数据变为{3,14,214,542,748,53}。
7.第三轮排序,和第一轮(3)思想相同,但是是按照数字的百位数放入桶中。比如53,百位数为0,则53放在下标为0的桶;748百位数为7,放在下标为7的桶。
8.按照这个桶的顺序(一维数组的下标)依次取出数据,放入原来的数组。数据变为{3,14,53,214,542,748}。
9.一共排序几轮取决于最大数的位数。
/**
* 1.完成基数排序
* 2.完成中间结果输出
* 3.完成事后时间统计
*
* 理解:基数排序以空间换时间的经典算法
1.数组的初始状态{53,3,542,748,14,214}。
2.每个桶是一个数组,共十个桶。
3.第一轮排序,将每个元素的个位数取出,然后看这个数应该放在哪个对应的桶。比如53,个位数为3,则53放在下标为3的桶,524放在下标为2的桶。
4.按照这个桶的顺序(一维数组的下标)依次取出数据,放入原来的数组。数据变为{542,53,3,14,214,748}
5.第二轮排序,和第一轮(3)思想相同,但是是按照数字的十位数放入桶中。比如53,十位数为3,则53放在下标为5的桶;3十位数没有,补0,放入0号桶。
6.按照这个桶的顺序(一维数组的下标)依次取出数据,放入原来的数组。数据变为{3,14,214,542,748,53}。
7.第三轮排序,和第一轮(3)思想相同,但是是按照数字的百位数放入桶中。比如53,百位数为0,则53放在下标为0的桶;748百位数为7,放在下标为7的桶。
8.按照这个桶的顺序(一维数组的下标)依次取出数据,放入原来的数组。数据变为{3,14,53,214,542,748}。
9.一共排序几轮取决于最大数的位数。
*
* 事前时间复杂度:O(nlogn)
* 事后80000个数据排序大致耗时3ms
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>
#include <string.h>
#define SIZE 80000
//局部变量不能申请这么多空间,改为全局变量,入参数组大小决定这个SIZE值,记得修改
int bucket[10][SIZE];
//输出中间的排序结果,便于观察排序过程
void showArr(int arr[], int size);
void radixSort(int arr[], int size);
int main() {
int arr[] = {53, 3, 542, 748, 14, 214};
int size = sizeof(arr) / sizeof(int);
radixSort(arr, size);
//事后统计执行时间 -- 测试一下80000个随机数时间
srand(time(NULL));
int arrTestTime[SIZE];
for (int i = 0; i < SIZE; ++i) {
arrTestTime[i] = rand();
}
// 获取起始时间
struct timespec start_time;
clock_gettime(CLOCK_MONOTONIC, &start_time);
radixSort(arrTestTime, SIZE);
// 获取结束时间
struct timespec end_time;
clock_gettime(CLOCK_MONOTONIC, &end_time);
// 计算时间差
long seconds = end_time.tv_sec - start_time.tv_sec;
long nanoseconds = end_time.tv_nsec - start_time.tv_nsec;
// 将纳秒转换为毫秒
long milliseconds = nanoseconds / 1000000;
printf("\n耗费时间time: %ld s;共计%ldms\n", seconds, milliseconds);
return 0;
}
void showArr(int arr[], int size) {
printf("\n");
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
}
void radixSort(int arr[], int size) {
//需要得到最大数的位数
int max = arr[0];
for (int i = 1; i < size; ++i) {
if (arr[i] > max) {
max = arr[i];
}
}
//得到最大数的位数
int maxLength = (int) log10(max) + 1;
//第一轮 -- 对个位数排序 第二位--十位 maxLength最大位数,决定轮次
//定义一个二维数组表示十个桶,每个桶是一个一维数组
//桶排序用空间换时间
//记录每个桶实际存放了多少个数据,定义一个一维数组来记录各个桶的每次放入的数据个数。
//bucketEleCounts[0]记录的是0号桶数据的个数
int bucketEleCounts[10];
memset(bucketEleCounts, 0, sizeof(bucketEleCounts));
for (int k = 0, n = 1; k < maxLength; ++k, n *= 10) {
for (int i = 0; i < size; ++i) {
//取出每个元素的个/十/百。。。。位数
int digitOfElement = arr[i] / n % 10;
//放入桶
bucket[digitOfElement][bucketEleCounts[digitOfElement]] = arr[i];
bucketEleCounts[digitOfElement]++;
}
//按照桶顺序,取出数据放入原来的一维数组
int index = 0;
//遍历每一个桶
for (int i = 0; i < 10; ++i) {
//如果桶中有数据才放入原数组
if (bucketEleCounts[i] != 0) {
//循环该桶
for (int j = 0; j < bucketEleCounts[i]; ++j) {
//取出二维数组元素,放入arr
arr[index++] = bucket[i][j];
}
}
//bucketEleCounts对应位置置0
bucketEleCounts[i] = 0;
}
//showArr(arr, size);
}
}
耗费时间time: 0 s;共计3ms
{53, 3, 542, 748, 14, 214}过程:
第一轮:542 53 3 14 214 748
第二轮:3 14 214 542 748 53
第三轮:3 14 53 214 542 748
8.常用排序算法总结和对比
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 排序方式 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | In-place | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | In-place | 不稳定 |
插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | In-place | 稳定 |
希尔排序 | O(nlogn) | O(nlog^2n) | O(nlog^2n) | O(1) | In-place | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | Out-place | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2 | O(logn) | In-place | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | In-place | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | Out-place | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(nlogn) | O(n+k) | Out-place | 稳定 |
基数排序 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | Out-place | 稳定 |
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b前面。
不稳定:如果a原本在b前面,而a=b,排序之后a可能在b后面。
内排序:所有操作都是在内存中完成。
外排序:由于数据过大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行。
时间复杂度:一个算法执行所耗费的时间。
空间复杂度:运行完一个程序所需要的内存大小。
n:数据规模。
k:桶的个数。
In-place:不占用额外内存。
Out-place:占用额外内存。
十二、查找算法
1.线性查找
依次遍历数组,找到就返回下边。
/**
* 线性查找 --找到一个就返回
*/
#include <stdio.h>
int seqSearch(const int arr[], int value, int size);
int main() {
int arr[] = {53, 3, 542, 748, 14, 214};
int size = sizeof(arr) / sizeof(int);
int value = 3;
int index = seqSearch(arr, value, size);
if (index == -1) {
printf("没有查找到value:%d", value);
} else {
printf("找到index:%d", index);
}
return 0;
}
int seqSearch(const int arr[], int value, int size) {
//逐一比对,返回下标
for (int i = 0; i < size; ++i) {
if (arr[i] == value) {
return i;
}
}
return -1;
}
2.二分查找
1.先将数组有序。
2.二分查找。
思路:
1.确定该数组的中间下标。mid = (left + right) /2
2.需要查找的数findValue和arr[mid]比较。
2.1.findValue>arr[mid],要查找的数在mid右边,向右递归查找。
2.2.findValue<arr[mid],要查找的数在mid左边,向左递归查找。
2.3.findValue=arr[mid],要查找的数在mid,返回。
3.找到数退出递归
4.递归完整个数组,仍然没有找到findValue,退出递归。当left > right需要退出。
/**
* 二分查找 -- 基础写法
* 要求数组必须是有序的
* 逐一没有找到数据的退出方法
*/
#include <stdio.h>
/**
*
* @param arr 数组
* @param value 要查找的值
* @param left 左边的值
* @param right 右边的值
* @return 返回下标或-1
*/
int binarySearch(const int arr[], int value, int left, int right);
int main() {
int arr[] = {1, 8, 10, 89, 222, 321};
int size = sizeof(arr) / sizeof(int);
int value = 9;
int index = binarySearch(arr, value, 0, 5);
if (index == -1) {
printf("没有查找到value:%d\n", value);
} else {
printf("找到index:%d\n", index);
}
return 0;
}
int binarySearch(const int arr[], int value, int left, int right) {
//当left > right 时,没有找到,返回-1
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
int midValue = arr[mid];
if (value > midValue) {
//向右递归
return binarySearch(arr, value, mid + 1, right);
} else if (value < midValue) {
//向左递归
return binarySearch(arr, value, left, mid - 1);
} else {
//找到了
return mid;
}
}
升级:当数组中有相同的数值,找到所有的下标。
/**
* 二分查找
* 要求数组必须是有序的
* z可以找出所有相同的数值下标
*/
#include <stdio.h>
#include <string.h>
/**
*
* @param arr 数组
* @param value 要查找的值
* @param left 左边的值
* @param right 右边的值
* @return 返回下标或-1
*/
int binarySearch(const int arr[], int value, int left, int right);
/**
*找到所有目标值的下标,而不是一个。在找到一个时并不返回而是继续向左向右递归。直到找完所有的数,数组返回
* @param arr 数组
* @param value 要查找的值
* @param left 左边的值
* @param right 右边的值
* @param result 返回的结果数组
*/
void binarySearchAll(const int arr[], int value, int left, int right, int result[], int size);
int main() {
int arr[] = {1, 8, 8, 8, 10, 89, 222, 321};
int size = sizeof(arr) / sizeof(int);
int value = 8;
int index = binarySearch(arr, value, 0, 7);
if (index == -1) {
printf("没有查找到value:%d\n", value);
} else {
printf("找到index:%d\n", index);
}
int result[size];
memset(result, -1, sizeof(result));
binarySearchAll(arr, value, 0, 7, result, size);
int resultSize = sizeof(result) / sizeof(int);
for (int i = 0; i < resultSize; ++i) {
if (result[i] != -1) {
printf("%3d", result[i]);
}
}
return 0;
}
int binarySearch(const int arr[], int value, int left, int right) {
//当left > right 时,没有找到,返回-1
if (left > right) {
return -1;
}
int mid = (left + right) / 2;
int midValue = arr[mid];
if (value > midValue) {
//向右递归
return binarySearch(arr, value, mid + 1, right);
} else if (value < midValue) {
//向左递归
return binarySearch(arr, value, left, mid - 1);
} else {
//找到了
return mid;
}
}
void binarySearchAll(const int arr[], int value, int left, int right, int result[], int size) {
//当left > right 时,没有找到,返回
if (left > right) {
return;
}
int mid = (left + right) / 2;
int midValue = arr[mid];
if (value > midValue) {
//向右递归
return;
} else if (value < midValue) {
//向左递归
return;
} else {
//找到了
int temp = mid - 1;
int index = 0;
while (1) {
if (temp < 0 || arr[temp] != value) {
break;
}
//否则放入集合
result[index++] = temp--;
}
result[index++] = mid;
temp = mid + 1;
while (1) {
if (temp > size - 1 || arr[temp] != value) {
break;
}
//否则放入集合
result[index++] = temp++;
}
return;
}
}
3.插值查找
1.插值查找类似于二分查找,不同的是插值查找每次从自适应mid处开始查找。
2.将二分查找中的求mid索引的公式改成
m
i
d
=
(
l
e
f
t
+
r
i
g
h
t
)
/
2
=
l
e
f
t
+
(
r
i
g
h
t
−
l
e
f
t
)
/
2
mid = (left +right)/2 = left +(right -left)/2
mid=(left+right)/2=left+(right−left)/2
改成:
m
i
d
=
l
e
f
t
+
(
v
a
l
u
e
−
a
r
r
[
l
e
f
t
]
)
∗
(
r
i
g
h
t
−
l
e
f
t
)
/
(
a
r
r
[
r
i
g
h
t
]
−
a
r
r
[
l
e
f
t
]
)
mid = left +(value-arr[left])*(right-left)/(arr[right]-arr[left])
mid=left+(value−arr[left])∗(right−left)/(arr[right]−arr[left])
注意事项:
1.对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找速度较快。
2.关键字分布不均匀的情况下,该方法不一定不二分查找好。
/**
* 插值查找是对二分查找的优化,改变mid的值
* 插值算法也要求数组有序
* 关键是mid索引的求解
*/
#include <stdio.h>
/**
* 插值查找算法
* @param arr 传入数组
* @param left 左边索引
* @param right 右边索引
* @param value 查找的值
* @param size 数组大小
* @return 下标,未找到返回-1
*/
int insertValueSearch(int arr[], int left, int right, int value, int size);
int main() {
int arr[100];
for (int i = 0; i < 100; ++i) {
arr[i] = i + 1;
}
int index = insertValueSearch(arr, 0, 100 - 1, 100, 100);
if (index == -1) {
printf("没有查找到value");
} else {
printf("找到index:%d", index);
}
return 0;
}
int insertValueSearch(int arr[], int left, int right, int value, int size) {
printf("查找次数计数~");
//当left > right 时,没有找到,返回-1
if (left > right || value < arr[0] || value > arr[size - 1]) {
return -1;
}
//自适应的一种写法
int mid = left + (value - arr[left]) * (right - left) / (arr[right] - arr[left]);
int midValue = arr[mid];
if (value > midValue) {
//向右递归
return insertValueSearch(arr, value, mid + 1, right, size);
} else if (value < midValue) {
//向左递归
return insertValueSearch(arr, value, left, mid - 1, size);
} else {
//找到了
return mid;
}
}
4.斐波那契(黄金分割法)查找算法
关键点在于借助斐波那契数列找到黄金分割点。
1.黄金分割点是指把一条线段分割为两部分,使其中一部分与全长之比等于另一部分与这部分之比。取其前三位数字的近似值是0.618。由于按此比例设计的造型十分美丽,因此称为黄金分割,也称为中外比。这是一个神奇的数字,会带来意向不大的效果。
2.斐波那契数列{1,1,2,3,5,8,13,21,34,55}发现斐波那契数列的两个相邻数的比例,无限接近 黄金分割值0.618。
斐波那契(黄金分割法)原理:
斐波那契查找原理与前两种相似,仅仅改变了中间结点(mid)的位置,mid不再是中间或插值得到,而是位于黄金分割点附近,即
m
i
d
=
l
e
f
t
+
f
i
b
A
r
r
[
k
−
1
]
−
1
(
f
i
b
A
r
r
代表斐波那契数列
)
mid=left+fibArr[k-1]-1 (fibArr代表斐波那契数列)
mid=left+fibArr[k−1]−1(fibArr代表斐波那契数列)
对F(k-1)-1的理解:
1)由婓波那契数列 fibArr[k]=fibArr[k-1]+fibArr[k-2]的性质,可以得到(fibArr[k-1)=(fibArr[k-1]-1)+(fibArr[k-2]-1)+1。该式说明:只要顺序表的长度为fibArr[K]-1,则可以将该表分成长度为fibArr[k-1]-1和fibArr[k-2]-1的两段,即如从而中间位置为mid=left+fibArr(k-1)-1上图所示。
2)类似的,每一子段也可以用相同的方式分割
3)但顺序表长度n不一定刚好等于fibArr[K]-1,所以需要将原来的顺序表长度n增加至fibArr[K]-1。这里的k值只要能使得fibArr[K-1]恰好大于或等于n即可,由以下代码得到,顺序表长度增加后,新增的位置(从n+1到F[k]-1位置),都赋为n位置的值即可
/**
* 1.根据目标数组创建一个斐波那契数列
* 2.根据有序数组右下边获取斐波那契数列下标值k
* 3.斐波那契数列下标为k的元素决定了真正进行查找数组的大小
* 4.原数组的数据拷贝进新数组,不足新数组的补位原数组的最后一位元素
* 5.mid = left + fibArr[k - 1] - 1;根据公式取到黄金分割点,进行类似二分查找的流程
* 6.根据逻辑向左向右进行黄金分割点查找
*/
#include <stdio.h>
#define MAXSIZE 20
//获取一个斐波那契数列
void fib(int fibArr[]);
/**
* 斐波那契查找算法
* @param arr 查找的数组
* @param key 查找的关键字
* @param size 数组长度
* @return 对应下标 未找到-1
*/
int fibSearch(const int arr[], int key, int size);
int main() {
int arr[] = {1, 8, 10, 89, 1000, 1234};
int size = sizeof(arr) / sizeof(int);
int index = fibSearch(arr, 89, size);
if (index == -1) {
printf("没有查找到value");
} else {
printf("找到index:%d", index);
}
return 0;
}
void fib(int fibArr[]) {
fibArr[0] = 1;
fibArr[1] = 1;
for (int i = 2; i < MAXSIZE; ++i) {
fibArr[i] = fibArr[i - 1] + fibArr[i - 2];
}
}
int fibSearch(const int arr[], int key, int size) {
int left = 0;
int right = size - 1;
//表示斐波那契分割数值的下标
int k = 0;
//存放mid值
int mid = 0;
int fibArr[MAXSIZE];
//获取斐波那契数列
fib(fibArr);
//获取斐波那契分割值的下标值
while (right > fibArr[k] - 1) {
k++;
}
//因为fibArr[k]的值可能大于arr长度,因此对数组扩容
int temp[fibArr[k]];
for (int i = 0; i < size; ++i) {
//拷贝数据
temp[i] = arr[i];
}
//用最后一位元素填充多余的位置
for (int i = size; i < fibArr[k]; ++i) {
temp[i] = arr[size - 1];
}
//使用循环处理查找数
while (left <= right) {
//每次找到剩余数组黄金分割点的位置
mid = left + fibArr[k - 1] - 1;
if (key < temp[mid]) {
//key小于分割点索引的值,向左边查找
right = mid - 1;
//重点理解
//1.全部元素 = 前面元素+后面元素
//2.fibArr[k] = fibArr[k-1] + fibArr[k-2]
//3.因为前面有fibArr[k-1]个元素,所以可以继续拆分fibArr[k-1] = fibArr[k-2] + fibArr[k-3]
//4.即在fibArr[k-1]的前面继续查找k--;
//5.即下次循环mid = left + fibArr[k - 1 - 1] - 1;
k--;
} else if (key > temp[mid]) {
//向数组的右边查找
left = mid + 1;
//重点理解
//1.全部元素 = 前面元素+后面元素
//2.fibArr[k] = fibArr[k-1] + fibArr[k-2]
//3.因为后面有fibArr[k-2]个元素,所以可以继续拆分fibArr[k-2] = fibArr[k-3] + fibArr[k-4]
//4.即在fibArr[k-2]的前面继续查找k-=2;
//5.即下次循环 mid = left + fibArr[k - 1 - 2] - 1;
k -= 2;
} else {
//找到
//需要确定返回哪个下标,因为扩容了数组
if (mid <= right) {
return mid;
} else {
return right;
}
}
}
return -1;
}
十三、哈希表
散列表(Hash table,也叫哈希表)是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表
实现思路:数组+链表/二叉树,数组里面存放链表
/**
* 用链表加数组实现哈希表
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NAME_SIZE 16
#define LINKED_SIZE 7
/**
* 雇员结构体
*/
typedef struct Emp {
int id;
char name[NAME_SIZE];
struct Emp *next;
} Emp;
/**
* 链表结构体
*/
typedef struct EmpLinkedList {
//头指针,指向第一个Emp
struct Emp *head;
} EmpLinkedList;
/**
* Hash表结构体,管理多条链表
*/
typedef struct HashTable {
struct EmpLinkedList *empLinkedList[LINKED_SIZE];
} HashTable;
/**
* 添加雇员到链表 假定id自增,所以直接加到链表的最后一个
* @param emp 雇员
* @param empLinkedList 链表
*/
void addEmpLinked(Emp *emp, EmpLinkedList *empLinkedList);
/**
* 哈希表添加雇员
* @param emp 雇员信息
* @param hashTable 哈希表
*/
void add(Emp *emp, HashTable *hashTable);
/**
* 遍历链表
* @param empLinkedList 需要遍历的链表
*/
void listEmpLinkedList(EmpLinkedList *empLinkedList, int i);
/**
* 散列函数,根据id决定加入到哪条链表
* @param id id
* @return 链表索引
*/
int hashFun(int id);
/**
* 遍历哈希表
* @param hashTable 哈希表的引用
*/
void list(HashTable *hashTable);
/**
* 初始化一个雇员
* @param id id
* @param name 姓名
*/
Emp *createEmp(int id, char *name);
/**
* 释放内存
* @param hashTable 哈希表
*/
void freeM(HashTable *hashTable);
/**
* 初始化哈希表以及底下的链表
* @return 哈希表引用
*/
HashTable *initHashTable();
/**
* 根据id 查找
* @param id id
* @param hashTable 哈希表
* @return emp
*/
Emp *findEmpById(int id, HashTable *hashTable);
/**
* 根据id删除雇员
* @param id id
* @param hashTable 哈希表
* @return 0失败1成功
*/
int del(int id, HashTable *hashTable);
int main() {
HashTable *hashTable = initHashTable();
char key = ' ';
int loop = 1;
while (loop) {
printf("a:添加雇员\n");
printf("l:显示雇员\n");
printf("f:查找雇员\n");
printf("d:删除雇员\n");
printf("e:退出系统\n");
scanf_s("%c", &key);
getchar();
int id;
switch (key) {
case 'a':
printf("输入用户id:\n");
char name[NAME_SIZE];
scanf_s("%d", &id);
getchar();
printf("输入用户name:\n");
scanf_s("%s", name);
getchar();
Emp *emp = createEmp(id, name);
add(emp, hashTable);
break;
case 'l':
list(hashTable);
break;
case 'e':
loop = 0;
break;
case 'f':
printf("输入查找的用户id:\n");
scanf_s("%d", &id);
getchar();
emp = findEmpById(id, hashTable);
if (emp == NULL) {
printf("没有查找到id为%d的雇员\n", id);
} else {
printf("查找到id为%d的雇员:id=%d\tname=%s\n", emp->id, emp->id, emp->name);
}
break;
case 'd':
printf("输入要删除的用户id:\n");
scanf_s("%d", &id);
getchar();
int success = del(id, hashTable);
if (success) {
printf("id为%d的雇员删除成功\n", id);
} else {
printf("id为%d的雇员删除失败s\n", id);
}
break;
default:
break;
}
}
freeM(hashTable);
return 0;
}
void addEmpLinked(Emp *emp, EmpLinkedList *empLinkedList) {
if (empLinkedList->head == NULL) {
empLinkedList->head = emp;
return;
}
//不是第一个雇员,则使用辅助指针定位到最后
Emp *cur = empLinkedList->head;
while (1) {
//到最后了
if (cur->next == NULL) {
break;
}
cur = cur->next;
}
cur->next = emp;
}
void listEmpLinkedList(EmpLinkedList *empLinkedList, int i) {
if (empLinkedList->head == NULL) {
printf("[%d]当前链表为空\n", i);
return;
}
printf("[%d]当前链表信息为:\n", i);
Emp *cur = empLinkedList->head;
while (1) {
printf("id=%d\tname=%s\n", cur->id, cur->name);
//到最后了
if (cur->next == NULL) {
break;
}
cur = cur->next;
}
}
void add(Emp *emp, HashTable *hashTable) {
int no = hashFun(emp->id);
//加入到对应链表
addEmpLinked(emp, hashTable->empLinkedList[no]);
}
int hashFun(int id) {
//根据员工的id,决定添加到哪条链表,简单使用取模法
return id % LINKED_SIZE;
}
void list(HashTable *hashTable) {
for (int i = 0; i < LINKED_SIZE; ++i) {
listEmpLinkedList(hashTable->empLinkedList[i], i);
}
}
Emp *createEmp(int id, char *name) {
Emp *emp = malloc(sizeof(Emp));
printf("申请的雇员对象地址:%p\n", emp);
emp->id = id;
strcpy(emp->name, name);
return emp;
}
void freeM(HashTable *hashTable) {
for (int i = 0; i < LINKED_SIZE; ++i) {
EmpLinkedList *empLinkedList = hashTable->empLinkedList[i];
if (empLinkedList->head == NULL) {
printf("释放的链表地址:%p\n", empLinkedList);
free(empLinkedList);
empLinkedList = NULL;
continue;
}
Emp *cur = empLinkedList->head;
Emp *temp = cur;
while (1) {
//到最后了
if (cur->next == NULL) {
printf("释放的雇员对象地址:%p\n", cur);
free(cur);
cur = NULL;
break;
}
temp = cur->next;
printf("释放的雇员对象地址:%p\n", cur);
free(cur);
cur = NULL;
cur = temp;
}
printf("释放的链表地址:%p\n", empLinkedList);
free(empLinkedList);
empLinkedList = NULL;
}
printf("释放的哈希表地址:%p\n", hashTable);
free(hashTable);
hashTable = NULL;
}
HashTable *initHashTable() {
HashTable *hashTable = malloc(sizeof(HashTable));
printf("申请的哈希表地址:%p\n", hashTable);
for (int i = 0; i < LINKED_SIZE; ++i) {
EmpLinkedList *empLinkedList = malloc(sizeof(EmpLinkedList));
empLinkedList->head = NULL;
printf("申请的链表地址:%p\n", empLinkedList);
hashTable->empLinkedList[i] = empLinkedList;
}
return hashTable;
}
Emp *findEmpById(int id, HashTable *hashTable) {
if (hashTable == NULL) {
printf("哈希表为空\n");
return NULL;
}
int flag = 0;
for (int i = 0; i < LINKED_SIZE; ++i) {
if (hashTable->empLinkedList[i] == NULL || hashTable->empLinkedList[i]->head == NULL) {
continue;
}
Emp *cur = hashTable->empLinkedList[i]->head;
while (1) {
if (cur->id == id) {
//找到
flag = 1;
break;
}
cur = cur->next;
if (cur == NULL) {
break;
}
}
if (flag) {
return cur;
}
}
return NULL;
}
int del(int id, HashTable *hashTable) {
if (hashTable == NULL) {
printf("哈希表为空\n");
return 0;
}
int flag = 0;
for (int i = 0; i < LINKED_SIZE; ++i) {
if (hashTable->empLinkedList[i] == NULL || hashTable->empLinkedList[i]->head == NULL) {
continue;
}
Emp *last = hashTable->empLinkedList[i]->head;
Emp *cur = hashTable->empLinkedList[i]->head;
while (1) {
if (cur->id == id) {
//找到
flag = 1;
if (last->id == cur->id) {
//就是head
hashTable->empLinkedList[i]->head = cur->next;
//这里不指空可能会出问题,如果创建一个已经被删除的雇员,可能分配的是删除之前的地址
cur->next = NULL;
printf("释放的雇员地址:%p\n", cur);
free(cur);
cur = NULL;
} else {
//不是head
last->next = cur->next;
printf("释放的雇员地址:%p\n", cur);
free(cur);
cur = NULL;
}
break;
}
last = cur;
cur = cur->next;
if (cur == NULL) {
break;
}
}
if (flag) {
return 1;
}
}
return 0;
}
十四、树结构
1.为什么需要树结构
在java中ArrayList底层是数组,按照比列扩容,实际上是数组拷贝。
1.数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低(遍历+数组拷贝)。
2.链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链接到链表中即可,删除效率也很好)。
缺点:在进行检索时,效率仍然较低(遍历),比如(检索某个值,需要从头节点开始遍历)。
3.树存储方式的分析
能提高数据存储,读取的效率,比如利用二叉排序树(Binary Sort Tree),既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
2.二叉树定义
树专业术语:节点、根节点、父节点、子节点、叶子结点、节点的权、路径、层、子树、树的高度、森林。
1.二叉树每个节点最多只能有两个子节点。
2.如果该二叉树的所有叶子结点都在最后一层,并且总数=2^n-1,n为层数,则为满二叉树。
3.如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续(从左往右看),倒数第二层的叶子节点在右边连续(从右往左看),我们称为完全二叉树。
3.二叉树遍历(前序、中序、后序)
实现见后序代码标题5。
前序遍历:先输出父节点,再遍历左子树和右子树。
中序遍历:先遍历左子树,再输出父节点,再遍历右子树出父节点。
后序遍历:先遍历左子树,再遍历右子树,最后输出父节点。
小结:看输出父节点的顺序,就确定是前序,中序还是后序。
二叉树遍历思路分析:
1.创建一个二叉树;
2.前序遍历
2.1先输出当前节点;
2.2如果当前节点左节点不为空,则递归继续前序遍历;
2.3如果当前节点右节点不为空,则递归继续前序遍历;
3.中序遍历
3.1如果当前节点左节点不为空,则递归继续中序遍历;
3.2输出当前节点;
3.3如果当前节点右节点不为空,则递归继续中序遍历;
4.后序遍历
4.1如果当前节点左节点不为空,则递归继续后序遍历;
4.2如果当前节点右节点不为空,则递归继续后序遍历;
4.3输出当前节点;
4.二叉树的查找
实现见后续代码,标题5。
二叉树查找的思路分析:
1.前序查找
1.1判断当前节点的no是否等于要查找的;
1.2如果相等,返回当前节点;
1.3不等则判断当前节点左子节点是否为空,如果不为空则递归前序查找;
1.4如果左递归前序查找找到节点则返回节点,否则判断当前节点右子节点是否为空,不为空继续向右递归前序查找。
2.中序查找思路
2.1.断当前结点的左子节点是否为空,如果不为空,则递归中序查找;
2.2如县找到,则返回,如果没有找到,就和当前结点比较,如果是则返回当前结点,否则继续进行右递归的中序查找;
2.3如果右递归中序查找,找到就返回,否则返回空。
3.后序查找思路
3.1判断当前结点的左子节点是否为空,如果不为空,则递归后序查找;
3.2如果找到,就返回,如果没有找到,就判断当前结点的右子节点是否为空如果不为空,则右递归进行后序查找,如果找到,就返回;
3.3就和当前结点进行,比如,如果是则返回,否则返回空。
5.二叉树删除节点
要求:
1)如果删除的节点是叶子节点,则删除该节点。
2)如果删除的节点是非叶子节点,则删除该子树。
3)测试,删除掉 5号叶子节点 和 3号子树。
思路:
1.因为我们的二叉树是单向的,所以我们是判断当前结点的子结点是否需要删除结点,而不能去判断当前这个结点是不是需要删除结点。
2.如果当前结点的左子结点不为空,并且左子结点就是要删除结点,就将this.left=null,释放左节点及其子节点内存;返回(结束递归删除)。
3.如果当前结点的右子结点不为空,并且右子结点就是要删除结点,就将this.right=null,释放右节点及其子节点内存 ;返回(结束递归删除)。
4.如果第2和第3步没有删除结点,那么我们就需要向左子树进行递归删除。
5.如果第4步也没有删除结点,则应当向右子树进行递归删除。
6.如果树是空树root,如果只有一个root节点,等价于将二叉树置空。
3、4、5章节代码小结:二叉树相关功能的实现 – 遍历、查找、删除
/**
* 二叉树相关实现
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//限制姓名最大长度为16
#define NAME_SIZE 16
/**
* 节点结构体
*/
typedef struct HeroNode {
//编号
int no;
//姓名
char name[NAME_SIZE];
//左节点
struct HeroNode *left;
//右节点
struct HeroNode *right;
} HeroNode;
/**
* 二叉树结构体
*/
typedef struct BinaryTree {
struct HeroNode *root;
} BinaryTree;
/**
* 打印详情信息
* @param heroNode 打印对象
*/
void infoHeroNode(HeroNode *heroNode);
/**
* 前序遍历
* @param heroNode 当前节点
*/
void preOrder(HeroNode *heroNode);
/**
* 中序遍历
* @param heroNode 当前节点
*/
void infixOrder(HeroNode *heroNode);
/**
* 后序遍历
* @param heroNode 当前节点
*/
void postOrder(HeroNode *heroNode);
/**
* 根据传参调用前序后序中序遍历
* @param order 遍历方法
* @param binaryTree 二叉树对象
*/
void treeList(void(*order)(HeroNode *heroNode), BinaryTree *binaryTree);
/**
* 创建一个节点
* @param no 编号
* @param name 姓名
* @return 节点引用
*/
HeroNode *createNode(int no, char *name);
/**
* 创建一个二叉树
* @param root 根节点
* @return 二叉树引用
*/
BinaryTree *createTree(HeroNode *root);
/**
* 释放所有内存
* @param binaryTree 二叉树
*/
void freeM(BinaryTree *binaryTree);
/**
* 收集所有节点的指针
* @param heroNode 当前节点
* @param pHeroNode 指针数组
*/
void treeListFree(HeroNode *heroNode, HeroNode **pHeroNode, int *num);
/**
* 根据传入的方法进行查找
* @param no 要查找的no
* @param binaryTree 二叉树引用
* @param search 方法
* @return 查找到的对象,未找到返回空
*/
HeroNode *treeSearch(int no, BinaryTree *binaryTree, HeroNode *(*search)(int no, HeroNode *root));
/**
* 前序查找
* @param no 要查找的no
* @param heroNode 当前节点
* @return 查找到的对象
*/
HeroNode *preSearch(int no, HeroNode *heroNode);
/**
* 中序查找
* @param no 要查找的no
* @param heroNode 当前节点
* @return 查找到的对象
*/
HeroNode *infixSearch(int no, HeroNode *heroNode);
/**
* 后序查找
* @param no 要查找的no
* @param heroNode 当前节点
* @return 查找到的对象
*/
HeroNode *postSearch(int no, HeroNode *heroNode);
/**
* 递归删除节点
* 1)如果删除的节点是叶子节点,则删除该节点。
*
* 2)如果删除的节点是非叶子节点,则删除该子树。
* @param no 节点编号
* @param heroNode 当前节点
*/
void delNode(int no, HeroNode *heroNode);
/**
* 传入树和no删除节点
* @param no 节点no
* @param binaryTree 树
*/
void delTreeNode(int no, BinaryTree *binaryTree);
/**
* 递归释放子树内存
* @param heroNode 子树根节点
*/
void freeChildTree(HeroNode *heroNode);
int main() {
//创建节点
HeroNode *root = createNode(1, "宋江");
HeroNode *node2 = createNode(2, "吴用");
HeroNode *node3 = createNode(3, "卢俊义");
HeroNode *node4 = createNode(4, "林冲");
HeroNode *node5 = createNode(5, "关胜");
BinaryTree *binaryTree = createTree(root);
//先手动创建二叉树
root->left = node2;
root->right = node3;
node3->right = node4;
node3->left = node5;
//遍历
//前序遍历1234
printf("开始前序遍历:\n");
treeList(preOrder, binaryTree);
//中序遍历2134
printf("开始中序遍历:\n");
treeList(infixOrder, binaryTree);
//后序遍历2431
printf("开始后序遍历:\n");
treeList(postOrder, binaryTree);
//查找
HeroNode *heroNode;
//前序查找
printf("开始前序查找:\n");
treeSearch(4, binaryTree, preSearch);
//中序查找
printf("开始中序查找:\n");
treeSearch(3, binaryTree, infixSearch);
//后序查找
printf("开始后序查找:\n");
treeSearch(2, binaryTree, postSearch);
//删除节点
printf("删除前前序遍历:\n");
treeList(preOrder, binaryTree);
delTreeNode(1, binaryTree);
printf("删除后前序遍历:\n");
treeList(preOrder, binaryTree);
//释放
freeM(binaryTree);
return 0;
}
void infoHeroNode(HeroNode *heroNode) {
if (heroNode == NULL) {
printf("要输出的节点为空!\n");
return;
}
printf("节点信息:id=%d;name=%s\n", heroNode->no, heroNode->name);
}
void preOrder(HeroNode *heroNode) {
//输出当前节点
infoHeroNode(heroNode);
//递归向左子树遍历
if (heroNode->left != NULL) {
preOrder(heroNode->left);
}
//递归向右子树遍历
if (heroNode->right != NULL) {
preOrder(heroNode->right);
}
}
void infixOrder(HeroNode *heroNode) {
//中序递归向左子树遍历
if (heroNode->left != NULL) {
infixOrder(heroNode->left);
}
//输出父节点
infoHeroNode(heroNode);
//中序递归向右子树遍历
if (heroNode->right != NULL) {
infixOrder(heroNode->right);
}
}
void postOrder(HeroNode *heroNode) {
//后序递归向左子树遍历
if (heroNode->left != NULL) {
postOrder(heroNode->left);
}
//后序递归向右子树遍历
if (heroNode->right != NULL) {
postOrder(heroNode->right);
}
//输出父节点
infoHeroNode(heroNode);
}
void treeList(void(*order)(HeroNode *heroNode), BinaryTree *binaryTree) {
if (binaryTree == NULL || binaryTree->root == NULL) {
printf("要输出的根节点为空!\n");
return;
}
order(binaryTree->root);
}
HeroNode *createNode(int no, char *name) {
HeroNode *heroNode = malloc(sizeof(HeroNode));
printf("申请的节点对象地址:%p\n", heroNode);
heroNode->no = no;
strcpy(heroNode->name, name);
return heroNode;
}
BinaryTree *createTree(HeroNode *root) {
if (root == NULL) {
printf("传入的根节点为空,创建二叉树失败。\n");
return NULL;
}
BinaryTree *binaryTree = malloc(sizeof(BinaryTree));
printf("申请的二叉树对象地址:%p\n", binaryTree);
binaryTree->root = root;
return binaryTree;
}
void freeM(BinaryTree *binaryTree) {
if (binaryTree == NULL) {
printf("要收集的树为空\n");
return;
}
if (binaryTree->root == NULL) {
printf("释放的树地址:%p\n", binaryTree);
free(binaryTree);
binaryTree = NULL;
return;
}
freeChildTree(binaryTree->root);
//开始递归删除
free(binaryTree);
binaryTree = NULL;
}
void treeListFree(HeroNode *heroNode, HeroNode **pHeroNode, int *num) {
//输出当前节点
pHeroNode[(*num)++] = heroNode;
//递归向左子树遍历
if (heroNode->left != NULL) {
HeroNode *temp = heroNode->left;
heroNode->left = NULL;
treeListFree(temp, pHeroNode, num);
}
//递归向右子树遍历
if (heroNode->right != NULL) {
HeroNode *temp = heroNode->right;
heroNode->right = NULL;
treeListFree(temp, pHeroNode, num);
}
}
HeroNode *treeSearch(int no, BinaryTree *binaryTree, HeroNode *(*search)(int no, HeroNode *root)) {
if (binaryTree == NULL || binaryTree->root == NULL) {
printf("未查找到节点no为:%d的节点", no);
return NULL;
}
HeroNode *result = search(no, binaryTree->root);
if (result != NULL) {
printf("查找到的节点:no=%d;name=%s\n", no, result->name);
} else {
printf("查找到的节点no为:%d的数据\n", no);
}
return result;
}
HeroNode *preSearch(int no, HeroNode *heroNode) {
//1.先判断当前是否相等
if (heroNode->no == no) {
return heroNode;
}
HeroNode *result = NULL;
//2.不相等,左递归查找
if (heroNode->left != NULL) {
result = preSearch(no, heroNode->left);
}
if (result != NULL) {
//左递归查找到了
return result;
}
//3.否则;递归向右子树遍历
if (heroNode->right != NULL) {
result = preSearch(no, heroNode->right);
}
return result;
}
HeroNode *infixSearch(int no, HeroNode *heroNode) {
HeroNode *result = NULL;
//1.左递归查找,找到就返回
if (heroNode->left != NULL) {
result = infixSearch(no, heroNode->left);
}
if (result != NULL) {
//左递归查找到了
return result;
}
//2.否则判断当前是否相等
if (heroNode->no == no) {
return heroNode;
}
//3.否则;递归向右子树遍历
if (heroNode->right != NULL) {
result = infixSearch(no, heroNode->right);
}
return result;
}
HeroNode *postSearch(int no, HeroNode *heroNode) {
HeroNode *result = NULL;
//1.左递归查找,找到就返回
if (heroNode->left != NULL) {
result = postSearch(no, heroNode->left);
}
if (result != NULL) {
//左递归查找到了
return result;
}
//2.否则;递归向右子树遍历
if (heroNode->right != NULL) {
result = postSearch(no, heroNode->right);
}
if (result != NULL) {
//左递归查找到了
return result;
}
//3.否则判断当前是否相等
if (heroNode->no == no) {
return heroNode;
}
return result;
}
void delNode(int no, HeroNode *heroNode) {
//当前节点的左节点是要删除的节点
if (heroNode->left != NULL && heroNode->left->no == no) {
//删除节点,释放内存
HeroNode *left = heroNode->left;
freeChildTree(left);
heroNode->left = NULL;
return;
}
//当前节点的右节点是要删除的节点
if (heroNode->right != NULL && heroNode->right->no == no) {
//删除节点,释放内存
HeroNode *right = heroNode->right;
freeChildTree(right);
heroNode->right = NULL;
return;
}
//当前节点左、右节点都非要删除节点,向左节点进入递归过程
if (heroNode->left != NULL) {
delNode(no, heroNode->left);
}
//当前节点左、右节点都非要删除节点,向右节点进入递归过程
if (heroNode->right != NULL) {
delNode(no, heroNode->right);
}
}
void freeChildTree(HeroNode *heroNode) {
//递归向左子树遍历
if (heroNode->left != NULL) {
freeChildTree(heroNode->left);
}
//递归向右子树遍历
if (heroNode->right != NULL) {
freeChildTree(heroNode->right);
}
printf("释放的节点地址:%p\n", heroNode);
free(heroNode);
heroNode = NULL;
}
void delTreeNode(int no, BinaryTree *binaryTree) {
if (binaryTree == NULL || binaryTree->root == NULL) {
printf("传入的树或树的根节点为空,不能删除\n");
return;
}
//判断root是否是要删除的节点
if (binaryTree->root->no == no) {
freeChildTree(binaryTree->root);
binaryTree->root = NULL;
return;
}
//开始递归删除
delNode(no, binaryTree->root);
}
6.顺序存储二叉树
堆排序用到了顺序存储二叉树的相关知识。
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组。
顺序存储二叉树特点:
1.顺序存储二叉树通常只考虑完全二叉树;
2.第n个元素的左节点为2*n+1;
3.第n个元素的右节点为2*n+2;
4.第n个元素的父节点为(n-1)/2。
需求:{1,2,3,4,5,6,7}要求以二叉树前序遍历方式进行遍历,前序遍历结果为1,2,4,5,3,6,7
/**
* 顺序存储树--前序遍历(中序后序待完善)
*/
#include <stdio.h>
#include <stdlib.h>
typedef struct ArrBinaryTree {
int *arr;
int size;
} ArrBinaryTree;
/**
* 创建二叉树
* @param arr 数组
* @param size 数组长度
* @return 顺序二叉树引用
*/
ArrBinaryTree *createTree(int *arr, int size);
/**
* 前序遍历
* @param binaryTree 顺序树
* @param index 数组下标
*/
void preOrder(ArrBinaryTree *binaryTree, int index);
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7};
int size = sizeof(arr) / sizeof(int);
ArrBinaryTree *pBinaryTree = createTree(arr, size);
//遍历顺序树数组
int *ptr = pBinaryTree->arr;
for (int i = 0; i < pBinaryTree->size; ++i) {
printf("输出值:%d\n", *ptr);
ptr++;
}
//遍历顺序树
preOrder(pBinaryTree, 0);
free(pBinaryTree);
return 0;
}
ArrBinaryTree *createTree(int arr[], int size) {
ArrBinaryTree *binaryTree = malloc(sizeof(ArrBinaryTree));
printf("申请的二叉树对象地址:%p\n", binaryTree);
binaryTree->arr = arr;
binaryTree->size = size;
return binaryTree;
}
void preOrder(ArrBinaryTree *binaryTree, int index) {
if (binaryTree == NULL || binaryTree->arr == NULL || binaryTree->size == 0) {
printf("数组为空,不能按照二叉树前序遍历。\n");
return;
}
//输出当前元素
printf("%d\n", binaryTree->arr[index]);
//向左递归遍历
if (index * 2 + 1 < binaryTree->size) {
preOrder(binaryTree, index * 2 + 1);
}
//向右递归
if (index * 2 + 2 < binaryTree->size) {
preOrder(binaryTree, index * 2 + 2);
}
}
申请的二叉树对象地址:0000021258d26ae0
输出值:1
输出值:2
输出值:3
输出值:4
输出值:5
输出值:6
输出值:7
1
2
4
5
3
6
7
7.线索化二叉树(构建、遍历)
示例:将{1,3,6,8,10,14}构建二叉树,6,8,10,14几个节点的左右指针没有完全利用上;
希望充分利用节点的左右指针:解决方案为线索二叉树。
1.n个结点的二叉链表中含有n+1 (公式 2n-(n-1)=n+1)个空指针域。利用二叉链表中的空指针域,存放指向该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索");
2.这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二又树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种;
3.一个结点的前一个结点,称为前驱结点;
4.一个结点的后一个结点,称为后继结点;
5.按照前序遍历线索化二叉树称为前序线索二叉树,按照中序遍历线索化二叉树称为中序线索二叉树,按照后序遍历线索化二叉树称为后序线索二叉树。
构建案例:
将以上二叉树中序遍历:8,3,10,1,14,6
线索二叉树:将8的右指针指向父节点3;将10的左指针,指向父节点3,将10的右指针指向后驱节点1;将14的左节点指向他的前驱指针1,14的右节点指向他的后继节点6.
说明: 当线索化二叉树后,Node节点的属性left和right,有如下情况:
1.left指向的是左子树,也可能是指向的前驱节点;比如 ① 节点left指向的左子树,而 ⑩节点的left指向的就是前驱节点
2.right指向的是右子树,也可能是指向后继节点,比如①节点right指向的是右子树,而⑩节点的right指向的是后继节点.
遍历:
说明:对前面的中序线索化的二叉树,进行遍历
分析:因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历因此无需使用递归方式,这样也提高了遍历的效率。遍历的次序应当和中序遍历保持一致。
/**
* 线索化二叉树--中序
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/**
* 节点结构体
*/
typedef struct HeroNode {
//编号
int no;
//姓名
char *name;
//左节点
struct HeroNode *left;
//右节点
struct HeroNode *right;
//如果leftType ==0 ,表示指向左子树,如果是1表示指向前驱节点
int leftType;
//如果rightType ==0 ,表示指向右子树,如果是1表示指向后继节点
int rightType;
} HeroNode;
/**
* 二叉树结构体
*/
typedef struct BinaryTree {
struct HeroNode *root;
struct HeroNode *pre;
} BinaryTree;
/**
* 中序线索化二叉树的方法 -- 多一个节点进行线索化 --重点是理解右子树线索化的顺序和退出逻辑;pre节点的指向
* @param binaryTree 树
* @param heroNode 节点
* @param pre heroNode的前驱节点
*/
void threadedNode(BinaryTree *binaryTree, HeroNode *heroNode);
/**
* 创建一个节点
* @param no 编号
* @param name 姓名
* @return 节点引用
*/
HeroNode *createNode(int no, char *name);
/**
* 创建一个二叉树
* @param root 根节点
* @return 二叉树引用
*/
BinaryTree *createTree(HeroNode *root);
/**
* 释放所有内存
* @param binaryTree 二叉树
*/
void freeM(BinaryTree *binaryTree);
/**
* 递归释放子树内存
* @param heroNode 子树根节点
*/
void freeChildTree(HeroNode *heroNode);
/**
* 遍历线索化二叉树的方法
* @param binaryTree
*/
void threadedList(BinaryTree *binaryTree);
int main() {
//创建节点
HeroNode *root = createNode(1, "tom");
HeroNode *node2 = createNode(3, "jack");
HeroNode *node3 = createNode(6, "smith");
HeroNode *node4 = createNode(8, "mary");
HeroNode *node5 = createNode(10, "king");
HeroNode *node6 = createNode(14, "dim");
BinaryTree *binaryTree = createTree(root);
//先手动创建二叉树关系
root->left = node2;
root->right = node3;
node2->left = node4;
node2->right = node5;
node3->left = node6;
//线索化
threadedNode(binaryTree, binaryTree->root);
//以10号写点测试
printf("线索化结果:\n");
printf("10号节点的前驱节点no:%d,name:%s\n", node5->left->no, node5->left->name);
printf("10号节点的后继节点no:%d,name:%s\n", node5->right->no, node5->right->name);
printf("遍历输出结果:\n");
threadedList(binaryTree);
freeM(binaryTree);
return 0;
}
void threadedNode(BinaryTree *binaryTree, HeroNode *heroNode) {
//这里的pre只能是通过结构体的局部变量每次去变换,而不能通过方法的参数传递,因为如果是1号节点,前驱为10,只有通过局部变量才能表示出来,通过参数传递无法表示
//void threadedNode(BinaryTree *binaryTree, HeroNode *heroNode,HeroNode *pre);
//如果node == null;直接返回
if (heroNode == NULL) {
return;
}
//数组:1,3,6,8,10,14
//中序:8,3,10,1,14,6
//先线索化左子树
threadedNode(binaryTree, heroNode->left);
//线索化当前节点
//处理前驱
//配图,比如当前节点为10,前驱节点为3,10的左节点为null,则将10的左节点指向3
if (heroNode->left == NULL && binaryTree->pre != NULL) {
//让当前节点的左指针指向前驱
heroNode->left = binaryTree->pre;
heroNode->leftType = 1;
}
//处理后继 --这里不太好理解重点理解
//配图,比如当前节点为1,前驱节点为10,10的右节点为null,则将10的右节点指向1
//以前驱节点右指针修改指向本节点,而不改变本节点的右指针,程序在下一次线索化右子树的时候顺利退出
if (binaryTree->pre != NULL && binaryTree->pre->right == NULL) {
//前驱节点的右指针指向当前节点
binaryTree->pre->right = heroNode;
//修改右指针指针类型
binaryTree->pre->rightType = 1;
}
binaryTree->pre = heroNode;
//线索化右子树
threadedNode(binaryTree, heroNode->right);
}
HeroNode *createNode(int no, char *name) {
HeroNode *heroNode = malloc(sizeof(HeroNode));
printf("申请的节点对象地址:%p\n", heroNode);
heroNode->no = no;
//strcpy(heroNode->name, name);
heroNode->name = name;
heroNode->rightType = 0;
heroNode->leftType = 0;
heroNode->right = NULL;
heroNode->left = NULL;
return heroNode;
}
BinaryTree *createTree(HeroNode *root) {
if (root == NULL) {
printf("传入的根节点为空,创建二叉树失败。\n");
return NULL;
}
BinaryTree *binaryTree = malloc(sizeof(BinaryTree));
printf("申请的二叉树对象地址:%p\n", binaryTree);
binaryTree->root = root;
return binaryTree;
}
void freeM(BinaryTree *binaryTree) {
if (binaryTree == NULL) {
printf("要收集的树为空\n");
return;
}
if (binaryTree->root == NULL) {
printf("释放的树地址:%p\n", binaryTree);
free(binaryTree);
binaryTree = NULL;
return;
}
freeChildTree(binaryTree->root);
//开始递归删除
printf("释放的树地址:%p\n", binaryTree);
free(binaryTree);
binaryTree = NULL;
}
void freeChildTree(HeroNode *heroNode) {
//先断线索化
if (heroNode->rightType == 1) {
heroNode->right = NULL;
}
if (heroNode->leftType == 1) {
heroNode->left = NULL;
}
//递归向左子树遍历
if (heroNode->left != NULL) {
freeChildTree(heroNode->left);
}
//递归向右子树遍历
if (heroNode->right != NULL) {
freeChildTree(heroNode->right);
}
printf("释放的节点地址:%p\n", heroNode);
free(heroNode);
heroNode = NULL;
}
void threadedList(BinaryTree *binaryTree) {
HeroNode *heroNode = binaryTree->root;
while (heroNode != NULL) {
//循环找到leftType == 1的节点 就是8节点;
while (heroNode->leftType == 0) {
heroNode = heroNode->left;
}
//输出节点
printf("节点no:%d,name:%s\n", heroNode->no, heroNode->name);
//如果右指针指向的是后继节点,就一直输出rightType == 1
while (heroNode->rightType == 1) {
//获取后继节点输出
heroNode = heroNode->right;
printf("节点no:%d,name:%s\n", heroNode->no, heroNode->name);
}
//替换节点
heroNode = heroNode->right;
}
}
申请的节点对象地址:000001d81dfa6d00
申请的节点对象地址:000001d81dfa6d30
申请的节点对象地址:000001d81dfa6d60
申请的节点对象地址:000001d81dfa6d90
申请的节点对象地址:000001d81dfa6dc0
申请的节点对象地址:000001d81dfa6df0
申请的二叉树对象地址:000001d81dfa6e20
线索化结果:
10号节点的前驱节点no:3,name:jack
10号节点的后继节点no:1,name:tom
遍历输出结果:
节点no:8,name:mary
节点no:3,name:jack
节点no:10,name:king
节点no:1,name:tom
节点no:14,name:dim
节点no:6,name:smith
释放的节点地址:000001d81dfa6d90
释放的节点地址:000001d81dfa6dc0
释放的节点地址:000001d81dfa6d30
释放的节点地址:000001d81dfa6df0
释放的节点地址:000001d81dfa6d60
释放的节点地址:000001d81dfa6d00
释放的树地址:000001d81dfa6e20
十五、堆排序(二叉树的实际应用)
堆排序基本介绍
1.堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为0(nlogn),它也是不稳定排序。
2.堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,注意:没有要求结点的左孩子的值和右孩子的值的大小关系。
3.每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
4.一般升序使用大顶堆,降序使用小顶堆。
举例:
堆排序基本思想:
构建顶堆的时候根本就没有创建树,是把树以数组的形式来存放的。
1.将待排序序列构造成一个大顶堆;
2.此时,整个序列的最大值就是堆顶的根节点;
3.将其与末尾元素进行交换,此时末尾就为最大值;
4.然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了。
要求:数组 {4,6,8,5,9},要求使用堆排序法,将数组升序排序
1.此时我们从最后一个非叶子结点开始(叶结点自然不用调整,第一个非叶子结点arr.length/2-1=5/2-1=1,也就是下面的6结点),从左至右,从下至上进行调整;[6,5,9]中9最大,6,9交换;
2.找到第二个非叶节点 4,由于[4,9,8]中9 元素最大,4和9交换。
3.这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和 6。
此时,我们就将一个无序序列构造成了一个大顶堆。
4.将堆顶元素与未尾元素进行交换,使末尾元素最大。然后继续调整堆,,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
5.重新调整结构,使其继续满足堆定义;(只调整4,6,8,5)
6.再将堆顶元素8与末尾元素5 进行交换,得到第二大元素8;
7.后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序。
/**
* 堆排序
* k = i * 2 + 1:左子节点 i * 2 + 2是右子节点要求树是完全二叉树
* 80000个测试数据10ms
* 时间复杂度:nlogn
*/
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define SIZE 80000
/**
* 堆排序
* @param arr 数组
* @param size 数组大小
*/
void heapSort(int *arr, int size);
/**
* 将一个数组(数组表示的前序二叉树),调整成一个大顶堆
* {4, 6, 8, 5, 9} 传入i=1,也就是对应元素6 得到{4,9,8,5,6}
* 再次调用传入i=0,得到{9,6,8,5,4}
* @param arr 待调整的数组
* @param len 对多少个元素进行调整 逐渐减小
* @param i 非叶子结点在数组的索引
*/
void adjustHeap(int *arr, int len, int i);
int main() {
//升序排列,调整为大顶堆
int arr[] = {4, 6, 8, 5, 9};
int size = sizeof(arr) / sizeof(arr[0]);
heapSort(arr, size);
printf("输出排序后数组:\n");
for (int i = 0; i < size; ++i) {
printf("%d ", arr[i]);
}
printf("\n");
//事后统计执行时间 -- 测试一下80000个随机数时间
srand(time(NULL));
int arrTestTime[SIZE];
for (int i = 0; i < SIZE; ++i) {
arrTestTime[i] = rand();
}
// 获取起始时间
struct timespec start_time;
clock_gettime(CLOCK_MONOTONIC, &start_time);
heapSort(arrTestTime, SIZE);
// 获取结束时间
struct timespec end_time;
clock_gettime(CLOCK_MONOTONIC, &end_time);
// 计算时间差
long seconds = end_time.tv_sec - start_time.tv_sec;
long nanoseconds = end_time.tv_nsec - start_time.tv_nsec;
// 将纳秒转换为毫秒
long milliseconds = nanoseconds / 1000000;
printf("\n耗费时间time: %ld s;共计%ldms\n", seconds, milliseconds);
return 0;
}
void heapSort(int *arr, int size) {
int temp;
//将无序序列构建成一个堆
for (int i = size / 2 - 1; i >= 0; i--) {
adjustHeap(arr, size, i);
}
//注释取消观察中间过程
// printf("输出大顶堆数组:\n");
// for (int i = 0; i < size; ++i) {
// printf("%d ", arr[i]);
// }
// printf("\n");
//依次将最大元素沉到数组最后
//一共size个数 最多调整size-1个数
for (int j = size - 1; j > 0; j--) {
//交换
temp = arr[j];
//arr[0]最大值
arr[j] = arr[0];
arr[0] = temp;
//前面已经调整为大顶堆的 这里只交换了根节点,只用从根节点开始调整即可
adjustHeap(arr, j, 0);
}
}
void adjustHeap(int *arr, int len, int i) {
int temp = arr[i];
//重点理解
//开始调整k = i * 2 + 1:左子节点 i * 2 + 2是右子节点
//k = k * 2 + 1 是为了遍历i的右子节点的下一个左子节点;将这个左子节点的子树也排成上大下小的子树;这个公式就要求我们的树是完全二叉树
for (int k = i * 2 + 1; k < len; k = k * 2 + 1) {
//左子节点的值小于右子节点的值 k为i的左子节点
if (k + 1 < len && arr[k] < arr[k + 1]) {
//将k指向右子节点
k++;
}
if (arr[k] > temp) {
//子节点大于父节点 需要交换
arr[i] = arr[k];
//i指向k继续循环比较
//发生了交换,也就是说当前节点比其某一个子节点的数值小
//既然交换了,之前排列好的子树顺序可能也有变化,需要将发生交换的子节点当前的值与其子子节点进行比较
i = k;
} else {
//是从左到右,从下到上比较的
//如果说本节点都没有发生数据交换,意味着本节点的数值大于两个子节点,
// 而子节点的数值大小再之前就遍历好了,也就可以直接退出了(先传入的是最后一个非叶子节点的子节点)
break;
}
}
//已经将以i为父节点的最大值,放在了这个子树的最顶
//需要将temp放到最后交换的位置(k的位置)已经赋值给i了
arr[i] = temp;
}
不适用测试数组,中间过程打印
输出大顶堆数组:
9 6 8 5 4
输出排序后数组:
4 5 6 8 9
80000个测试数据
耗费时间time: 0 s;共计10ms