一、中级阶段
1.作业,结构体,结构体指针
作业
- 动态申请空间
- 使用malloc之后的返回值p只是一个地址值,不能表示数组了,所以在使用fgets的时候不能在使用sizeof§,而是应该直接把输入的大小n放在这里
如何应对递归题
- 递归的重点就是找到递推公式,找递推公式可以多列举数据,使用归纳法推导
- 没做过的很难想出来,本身递归方面的题不是很多,增加练习题的见世面即可
结构体
- 类似于Java中的对象
- 必须使用关键字
struct
- 如何定义,声明,初始化,访问成员变量
#include <stdio.h>
#include <stdlib.h>
struct student {
int num;// 成员变量
char name[20];
char sex;
int age;
float score;
char addr[30];
};// 结尾加分号
int main() {
struct student s = {1001, "jack", 'm', 18, 98.5, "shenzhen"};
printf("%d %s %c %d %5.2f \n", s.num, s.name, s.sex, s.age, s.score, s.addr);
return 0;
}
结构体数组
- 结构体数组使用方法
- 用法类似于Java中的List集合
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
struct student {
int num;
char name[20];
char sex;
int age;
float score;
char addr[30];
};
int main() {
struct student sarr[3];
// 输入 1001 jack m 20 98.5 shenzhen
// 输入 1002 rose m 21 98.5 beijing
// 输入 1003 john m 22 99.5 shanghai
for (int i = 0; i < 3; i++) {
scanf("%d%s %c%d%f%s", &sarr[i].num, &sarr[i].name, &sarr[i].sex, &sarr[i].age, &sarr[i].score, &sarr[i].addr);
}
// 注意%c前面有空格
}
思考:结构体的大小?
- 可能比变量的字节总数还要大
- 为了高效访问内存,都是以4字节为单位进行存取的,不足4字节的也占位4字节,对齐
- 理解即可,实际计算空间时我们直接用sizeof()
- 详细的对齐规则,考试中不考
结构体指针
- 为了掌握结构体指针的偏移
- 重点理解下面代码的原理含义
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
struct student {
int num;
char name[20];
char sex;
};
int main() {
struct student s = {1001, "jack", 'm'};
struct student* p;
p = &s;
// *p要加括号,因为.的优先级高于*
printf("%d %s %c \n", (*p).num, (*p).name, (*p).sex);
// 也可以使用指针的成员选择 p->num 与上面是等价的
// .是结构体变量的成员选择运算符,->是指针的成员选择运算符
printf("%d %s %c \n", p->num, p->name, p->sex);
// 结构体数组
struct student sarr[3] = { 1001, "jack", 'm', 1002, "jack", 'm', 1003, "jack", 'm' };
int num;
p = sarr;
printf("----------------------\n");
// 充分理解打印的值的原理(考试几乎不考)
num = p->num++;
printf("num=%d,p->num=%d\n", num, p->num);
num = p++->num;
printf("num=%d,p->num=%d\n", num, p->num);
return 0;
}
2.typedef 使用,C++的引用,逻辑结构与存储结构,时间复杂度/空间复杂度
typedef 使用
- 作用:起别名,可以同时给结构体类型和结构体指针起别名,代码更简洁
- 为什么要对int 起别名?为了代码即注释
// 给结构体类型起别名stu,给结构体指针类型起别名pstu
typedef struct student {
int num;
char name[20];
char sex;
}stu, * pstu;
// 别名INTRGER和int等价的,起别名是为了见名知意
typedef int INTEGER;
int main() {
stu s = {1001, "jack", 'm'};
pstu p;// 与stu* p都是等价的
INTEGER i = 10;
p = &s;
printf("i=%d,p->num=%d\n", i, p->num);
}
C++引用
- 语法与C略有不同,但是编译之后都是一样的,C++封装程度更高
- 需要创建.cpp文件,需要在子函数中加引用符号&,
- 这时候,子函数里操作引用与主函数是等价的
- 这样写的优点是降低了代码错误率
#include <stdio.h>
#include <stdlib.h>
// C++语法可以在形参中用&,C中不可以,C++中称&为引用
// C++封装程度更高级,直接在形参里加&符号,即可传递变量名过来进行操作
void modify_num(int& b) {
b = b + 1;
}
void modefy_pointer(int*& p) {
p = (int*)malloc(20);
p[0] = 5;
}
int main() {
int a = 10;
modify_num(a);
printf("a=%d\n", a);
int* p = NULL;
modefy_pointer(p);
printf("p[0]=%d\n", p[0]);
return 0;
}
逻辑结构与存储结构
- 逻辑结构:数据与元素之间的逻辑关系,抽象的
- 存储结构:数据结构在计算机中的表示,具体的
- 逻辑结构包括:集合结构(无关系),线性结构(一对一),树形结构(一对多),图形结构(多对多)
- 存储结构包括:顺序存储,链式存储,索引存储,散列存储;其中顺序和链式是基础,可以实现所有的逻辑结构,索引与散列也是他们俩形成的
链式存储
- 每个节点其实是个结构体,包括了数据和指针,指针用于指向下一个节点地址
顺序存储与链式存储
- 顺序结构不利于频繁的数据增删,链式结构有利于数据频繁的增删
- 顺序存储中的实现随机存取,指的就是通过公式可以拿到任意一个元素
算法
- 对特定问题求解步骤的描述
- 特性:有穷,确定,可行,输入,输出
时间复杂度
- 不用时间表示,因为同样的算法,不同的计算机运算时间不同
- 使用运行次数表示时间复杂度,因为时间和运行次数是正相关的
T(n) = O(f(n))
含义,输入的数据量是n,实际的运行次数是f(n)- 表述时间复杂度的时候一定要说O(…)
- 常见的时间复杂度
常见的时间复杂度
- O(1):比如数组,一下就能访问
3.线性表
定义
- 相同类型,有序
- n个元素,n为线性表长度,n为0叫空表
- 唯一的前驱,唯一的后继
特点
- 线性表是逻辑结构,不是存储结构
- 元素个数优先
- 每个元素占用空间相同
- 具有逻辑上的顺序
注意区分
- 线性表是一种逻辑结构,表示元素直接一对一的相邻关系
- 顺序表和链表是一种存储结构,
- 二者具有不同的概念
- 习惯上,我们就说顺序表,就是指循序存储的线性表
用顺序表来实现线性表
- 逻辑上相邻的两个元素在物理位置上也相邻
- 优点:可以随机存取,表中任意一个;存储密度高
- 缺点:增删需要移动大量数据,难以确定存储空间,需要分配一段连续的存储空间,不灵活
顺序表插入操作
- 最好情况:表尾插入,不需要移动元素,时间复杂度O(1)
- 最坏情况:表头插入,所有元素移动,时间复杂度O(n)
- 平均情况,平均移动元素次数为n/2,时间复杂度为O(n)
顺序表删除操作与插入的时间复杂度都是一样的,最好,最坏,平均
插入和删除的代码实现过程,结合画图理解
动态分配的数组还属于顺序存储结构么(比如malloc)?
- 仍然是顺序存储结构,只不过是在堆空间上(考试不会区分)
顺序表的增删查
#include <stdio.h>
#include <stdlib.h>
#define MaxSize 50
//顺序表中的类型
typedef int ElemType;
// 静态分配
typedef struct {
ElemType data[MaxSize];//存元素
int length;//当前顺序表中有多少个元素
}SqList;
//自定义插入函数,i代表插入的位置,从1开始,e表示插入的元素
bool ListInsert(SqList& L, int i, ElemType e) {
if (i<1 || i>L.length + 1) {
return false;// 判断插入的位置是否合法
}
if (L.length >= MaxSize) {
return false;//元素存储满了,不能再存
}
//移动顺序表中元素,依次后移
for (int j = L.length; j >= i; j--)
{
L.data[j] = L.data[j - 1];
}
L.data[i - 1] = e;//数组下标从零开始,插入第一个位置,访问的下标为0
L.length++;
return true;
}
// 打印顺序表
void PrintList(SqList& L) {
for (int i = 0; i < L.length; i++)
{
printf("%3d", L.data[i]);// 所有元素打印成一排
}
printf("\n");
}
// 删除使用元素e的引用,目的是拿出对应的值
bool ListDelete(SqList& L, int i, ElemType& e) {
if (i < 1 || i > L.length) {
return false;// 删除位置不合法
}
if (L.length == 0) {
return false;
}
e = L.data[i - 1];
for (int j = i; j < L.length; j++) {
L.data[j - 1] = L.data[j];
}
L.length--;
return true;
}
// 元素查找,查找成功则返回位置,位置从1开始,查找失败,则返回0
int LocateElem(SqList L, ElemType e) {
int i;
for (i = 0; i < L.length; i++) {
if (L.data[i] == e) {
return i + 1;
}
return 0;
}
}
int main() {
SqList L;//顺序表名称
bool ret;//查看返回值,布尔型是true或false
ElemType del;//要删除的元素
//顺序表中手动赋值
L.data[0] = 1;
L.data[1] = 2;
L.data[2] = 3;
L.length = 3;//总计3个元素
ret = ListInsert(L, 2, 60);
if (ret) {
printf("插入成功\n");
PrintList(L);
}
else {
printf("插入失败\n");
}
ret = ListDelete(L, 1, del);// 删除第一个位置的元素,并把元素输出
if (ret) {
printf("删除成功\n");
printf("删除的元素值为 %d\n", del);
PrintList(L);
}
else {
printf("删除失败\n");
}
int elem_pos;
elem_pos = LocateElem(L, 30);
if (elem_pos) {
printf("查找成功\n");
printf("元素位置为 %d\n", ret);
}
else {
printf("查找失败\n");
}
return 0;
}
4.作业讲解,单链表的头插与尾插(考试重点)
线性表的链式表示,又称链表
回顾顺序表缺点:
- 插入、删除操作移动大量元素
- 数组的大小不好确定
- 占用一大段连续的存储空间,造成很多碎片
单链表
- 逻辑上相邻的两个元素在物理位置上不相邻
- 单链表节点定义,包括数据域、指针域,一共8字节
- 数据域存数据,指针域存下一个节点的地址
单链表结构
- 头指针:保存链表中第一个节点的存储位置,用来标识单链表
- 头结点:在单链表的第一个节点之前附加一个节点,为了操作方便
- 头指针是链表必须的元素,不论链表是否为空,头指针均不为空;若链表有头结点,则头指针永远指向头结点;
- 头结点是为了方便操作而设立的,其数据域一般为空,或存放链表长度;有了头结点,则对第一节点的前插入和删除第一节点的操作就统一了,头结点不是必须的
typedef int ElemType;
typedef struct LNode {
ElemType data;
struct LNode* next;
}LNode, *LinkList;
关于链表的声明
- 这里给结构体起了别名叫做LNode,给结构体的指针变量起别名叫LinkList
- 结构体中的指针变量为什么不能直接用别名LinkList?因为无法编译,编译按顺序进行,编译到结构体中的指针变量的时候,还不知道后面给结构体起别名
- 给struct LNode起别名,别名仍然是LNode,这是可行的,很多人都这么用;后续再使用LNode就是代表struct LNode;别名不同也没有问题
注意
- 重点掌握,考试常考,后面学数据结构的基础
- 如果不能理解代码,最好的办法就是,拿笔画图分析
- 如果想加大习题量,微信读书《跟龙哥学C语言编程》,非常提升思维
代码实现单链表的增删改查
- 头插法,尾插法的方法可以不用返回,因为传入的已经是指针变量了
- 尾插法:尾结点必须保证指针域是null,否则遍历打印的时候,回读取尾结点的指针域,而此时的指针域保存的是申请空间时默认的cdcdcdcd,因此读取cdcdcdcd时,微软会不给权限读取这种变量值
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
// 声明链表
typedef int ElemType;
typedef struct LNode {
ElemType data;
struct LNode* next;
}LNode, *LinkList;
// 头插法新建链表
LinkList CreateList1(LinkList& L) {
LNode* s; int x;
L = (LinkList)malloc(sizeof(LNode));//带头结点的链表
L->next = NULL;//头结点的data不存数据
scanf("%d", &x);
//我们规定输入9999表示结束
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode));//申请新空间s,放在头结点之后
s->data = x;//将输入的值赋给新空间中的data
s->next = L->next;//新结点的next指向链表的第一个存数据的元素
L->next = s;//s成为第一个保存数据的元素
scanf("%d", &x);
}
return L;//不返回也没关系,因为我们使用了引用
}
// 尾插法新建链表
LinkList CreateList2(LinkList& L) {
int x;
L = (LinkList)malloc(sizeof(LNode));
//r代表链表尾结点,s表示新增结点
LNode* s, * r = L;// LinkList s,r也可以,
scanf("%d", &x);
while (x != 9999) {
s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;//让尾部结点指向新结点
r = s;//r指向新的表尾结点
scanf("%d", &x);
}
r->next = NULL;
return L;
}
// 打印链表
void PrintList(LinkList L) {
L = L->next;
while (L != NULL) {
printf("%3d", L->data);
L = L->next;
}
printf("\n");
}
//主函数
int main() {
LinkList L;
//CreateList1(L);
CreateList2(L);
PrintList(L);
}
5.单链表的查找、删除,双链表的插入与删除(重点)
查找指定位置的元素
// 根据位置查找指定的元素并返回指针
LinkList GetElem(LinkList L, int i) {
int j = 1;
LinkList p = L->next;// 让p指向第一个节点
if (i == 0) {
return L;
}
if (i < 1) {
return NULL;
}
//
while (p && j < i) {
p = p->next;
j++;
}
return p;
}
根据值查找指定元素(多个的暂不考虑,找到就返回)
// 根据值查找指定的元素
LinkList FindElem(LinkList L, ElemType e) {
LinkList p = L->next;
while (p != NULL && p->data != e) {
p = p->next;
}
return p;
}
插入新元素到第i个位置(原有的i及后面的后移)
// 插入新元素到第i个位置(原有的i及后面的后移)
bool ListFrontInsert(LinkList L, int i, ElemType e) {
LinkList p = GetElem(L, i - 1);//先拿到i-1位置的地址值
if (NULL == p) {
return false;//i不对
}
LinkList s = (LinkList)malloc(sizeof(LNode));//给新的节点申请空间
s->data = e;//要插入的值放入对应空间
s->next = p->next;
p->next = s;
return true;
}
删除指定位置元素(头,中,尾都是一样的效果),需要有头结点
// 删除第i位置的元素
bool ListDelete(LinkList L, int i) {
LinkList p = GetElem(L, i - 1);//查找删除位置的前驱节点
if (NULL == p) {
return false;
}
LinkList q = p->next;
if (NULL == q) {
return false;
}
p->next = q->next;
free(q);
q = NULL;
return true;
}
双链表(双向链表),考试很少考,时间充足的话可以学
- 一个双链表结构体包含:数据域,前驱指针,后驱指针
- 一个双链表结构体占12字节(数据域为int类型)
- 为什么使用双链表?因为单链表只能单向遍历,双链表可以双向遍历
双链表的插入
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
// 声明链表
typedef int ElemType;
typedef struct DNode {
ElemType data;
struct DNode* prior, * next;
}DNode, * DLinkList;
// 双链表头插法
DLinkList Dlist_head_insert(DLinkList& DL) {
DNode* s; int x;
DL = (DLinkList)malloc(sizeof(DNode));// 带头结点的链表,DL就是头结点
DL->next = NULL;// 前驱指针和后继指针都填写NULL
DL->prior = NULL;
scanf("%d", &x);// 标准输入读取数据
while (x != 9999) {
s = (DLinkList)malloc(sizeof(DNode));//申请空间,保存新增节点
s->data = x;
s->next = DL->next;
if (DL->next != NULL) {
DL->next->prior = s;
}
s->prior = DL;
DL->next = s;
scanf("%d", &x);//读取标准输入
}
return DL;
}
// 打印链表
void PrintList(DLinkList L) {
L = L->next;
while (L != NULL) {
printf("%3d", L->data);
L = L->next;
}
printf("\n");
}
int main() {
DLinkList L;
Dlist_head_insert(L);
PrintList(L);
return 0;
}
双链表尾插法
// 双链表尾插法
DLinkList Dlist_tail_insert(DLinkList& DL) {
int x;
DL = (DLinkList)malloc(sizeof(DNode));//带头结点的链表
DNode* s, * r = DL;//r代表尾指针
DL->prior = NULL;
scanf("%d", &x);
while (x != 9999) {
s = (DNode*)malloc(sizeof(DNode));
s->data = x;
r->next = s;
s->prior = r;
r = s;//r指向尾结点
scanf("%d", &x);
}
r->next = NULL;//尾结点的next指针赋值为null
return DL;
}
疑问:为什么头插法和尾插法的形参必须用 去地址符&,打印和其他函数不需要?
- DNode是结构体类型的别名,DLinkList是结构体指针类型的别名
- 初始化 DLinkList L;得到的L是结构体DNode的指针,L也就是链表的头指针,L所指向的结构体应该是整个链表的头结点
- 打印、查找、删除、插入等操作,不需要改变链表的头指针,形参使用
DLinkList L
,拿到头指针以后,直接向后遍历即可 - 头插法尾插法创建链表,需要创建新的头指针;形参使用
DLinkList& DL
DL表示的头结点; DLinkList L
和DLinkList& DL
,L表示的是整个链表,DL表示的是链表的引用;打印、查找、删除、插入等操作,不需要改变链表的引用(因为已经创建好了),所以直接用L,头插法尾插法创建链表,是创建新的链表,需要修改链表引用,所以使用了&DL
在第i个位置插入新节点
// 新节点插入到第i个位置
bool DListFrontInsert(DLinkList DL, int i, ElemType e) {
DLinkList p = GetElem(DL, i - 1);//找到指定位置的前一个位置
if (NULL == p) {
return false;
}
DLinkList s = (DLinkList)malloc(sizeof(DNode));
s->data = e;
s->next = p->next;
p->next->prior = s;
p->next = s;
return true;
}
删除节点
// 删除第i个节点(头结点不可以删)
bool DListDelete(DLinkList DL, int i) {
DLinkList p = GetElem(DL, i - 1);
if (NULL == p) {
return false;
}
DLinkList q;
q = p->next;
if (NULL == q) {
return false;//要删除的元素不存在
}
p->next = q->next;//断链
if (q->next != NULL) {//如果为空,说明q为最后一个节点
q->next->prior = p;
}
free(q);//释放对应节点的空间
return true;
}
循环单链表:了解原理
循环双链表:了解原理
静态链表:了解原理
6.学习方法,引用解析,栈与循环队列(重点)
说明
- 考试考双向链表大题的概率非常低
- 一次不理解没关系,学习计算机重点在于反复的学习,就会越来越理解
解释为什么头插法尾插法创建链表需要使用引用
- 主函数创建了
LinkList L;
这个指针变量指向了链表的引用,也就是头结点的引用,因此L代表链表的引用,并且初始化的时候没有数据 - 头插法尾插法是从空的L到创建一个链表,L开始有指向,所以相当于改变了指向的内容,所以需要调用方法时,传递形参为引用值 &L
- 打印链表时,不需要对主函数中的L造成改变,所以直接传L即可,即使在子函数中对L进行复制,也不会改变主函数中的L
- L的值需要改,那么调用子函数时就用引用&L;L的值不需要改,那么调用子函数的时候就用L
- 其实这里的L本身就是个指针,&L其实是二级指针???
为什么要在形参的地方使用引用?
- 在子函数中给对应的形参赋值后,子函数结束,主函数中对应的实参就会发生变化
- 如果没有使用引用,那么在子函数中给形参赋值后,子涵数结束,主函数对应的实参不会改变的
学习方法?
- 听课,在纸上画图,能够独立写代码
- 多单步调试,把每个变量查看一下内存
- 报错时思考为什么报错,具体查看什么代码导致了什么报错
- 如果没有调适能力,实际刷题机试会寸步难行
如何写课堂的案例代码?
- 先理解代码的原理,画流程图
- 根据流程图写自己的代码
栈(stack)重点
- 栈:只允许在一端进行插入或删除操作的线性表
- 实际在计算机内存中,顺序表和链表都可以实现栈;业内习惯用顺序表实现栈
- 先进后出,为什么这么设计,因为有实际的使用场景
- 出栈之后,top指向的位置发生变化;其实原来的数据还在
- s.top=-1时,说明栈为空,所以我们在初始化top的时候,应该初始化成top
栈的代码实现,初始化,入栈,出栈,
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MaxSize 50
typedef int ElemType;
typedef struct {
ElemType data[MaxSize];//数组
int top;
}SqStack;
//初始化栈
void InitStack(SqStack& S) {
S.top = -1;//初始化栈为空
}
//判断栈是否为空
bool StackEmpty(SqStack& S) {
if (-1 == S.top) {
return true;
}
return false;
}
//入栈
bool Push(SqStack& S, ElemType x) {
if (S.top == MaxSize - 1) {
return false;
}
S.data[++S.top] = x;
return true;
}
//获取栈顶元素
bool GetTop(SqStack S, ElemType& m) {
if (StackEmpty(S)) {
return false;
}
m = S.data[S.top];
return true;
}
//出栈
bool Pop(SqStack& S, ElemType& x) {
if (StackEmpty(S)) {
return false;
}
x = S.data[S.top--];
return true;
}
int main() {
SqStack S;
bool flag;
ElemType m;
InitStack(S);
flag = StackEmpty(S);
if (flag) {
printf("栈是空的\n");
}
Push(S, 3);
Push(S, 4);
Push(S, 5);
flag = GetTop(S, m);//获取顶元素
if (flag) {
printf("获取栈顶元素为%d\n", m);
}
flag = Pop(S, m);
if (flag) {
printf("出栈元素为%d\n", m);
}
return 0;
}
链栈(使用比较少)
- 链表实现的栈,
- 用头插法实现入栈,头部删除法实现出栈
队列(Queue)
- 队首入队,队尾出队,先进先出
- 顺序表和链表都可以实现;
- 链表更方便实现,头插法,尾删法
- 数组也能实现(不常用),入队时正常顺序保存,出队时出第一个,然后后续元素整体向前平移,再设置一个标记队尾的位置
循环队列(考试会考)-难理解,多画图
- n个位置,只能存n-1个元素,
- rear的位置一直空着,标识队尾
- 队首出队,队尾入队
- 出队之后,原来位置保存的元素无需改变,只要front改变位置即可
- 入队的时候,rear的位置一直在后移;出队的时候,front也会指向下一个元素;所以伴随着入队出队,rear和front一直在转圈圈
- 我们判断当rear和front的位置相等的时候,说明循环对列为空
- 当rear+1=front的时候,说明队列是满的
- 如何判断队列是否满了?队列实际长度取余最大长度,如果等于1,说明队列满了
- 在使用中,如果rear逐渐的移动,从5到0,如何处理?(rear+1)%MaxSize 所得到的值,就是rear移动到下一个位置的序号
- 主要使用顺序表容易实现;链表也能实现,但是比较难,使用的很少
代码实现循环队列
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MaxSize 5
typedef int ElemType;
typedef struct {
ElemType data[MaxSize];//数组,最大存储MaxSize-1个元素
int front, rear;//队头,队尾
}SqQueue;
//初始化
void InitQueue(SqQueue& Q) {
Q.front = 0;
Q.rear = 0;
}
//判断是否为空
bool isEmpty(SqQueue Q) {
if (Q.front == Q.rear) {
return true;
}
return false;
}
//入队
bool EnQueue(SqQueue& Q, ElemType x) {
if ((Q.rear + 1) % MaxSize == Q.front) {
return false;//队列满了
}
Q.data[Q.rear] = x;
Q.rear = (Q.rear + 1) % MaxSize;//向后移动一格
}
//出队
bool DeQueue(SqQueue& Q, ElemType& x) {
if (Q.front == Q.rear) {
return false;
}
x = Q.data[Q.front];
Q.front = (Q.front + 1) % MaxSize;
return true;
}
//是否入队成功
void isSuccess(bool flag) {
if (flag) {
printf("入队成功\n");
}
else {
printf("入队失败\n");
}
}
int main() {
SqQueue Q;
bool ret;//保存返回值
ElemType element;//保存出队元素
InitQueue(Q);//初始化
ret = isEmpty(Q);
if (ret) {
printf("队列为空\n");
}
ret = EnQueue(Q, 3);
isSuccess(ret);
ret = EnQueue(Q, 4);
isSuccess(ret);
ret = EnQueue(Q, 5);
isSuccess(ret);
ret = EnQueue(Q, 6);
isSuccess(ret);
ret = EnQueue(Q, 7);
isSuccess(ret);
ret = DeQueue(Q, element);
if (ret) {
printf("出队成功,出队元素为%d\n", element);
}
}
7.作业,队列(链式),斐波那契,二叉树层次建树
作业讲解
队列–链式存储
- 需要满足需求,从链表头出队(删除),从链表尾入队(新增)
- 之前学的链表有一个头指针指向链表头部,但是不知道尾部在哪里;
- 现在增加一个指针指向链表尾,称为带有队头指针和队尾指针的单链表
- 考试有可能考,难度不大,只是比传统的链表多了一个尾指针
为什么第一个结构体的struct后面必须有名字,第二个结构体struct后可以省略名字? - 因为第一个结构体内部使用了名字LinkNode就是基于外层的结构体命名的
- 此时,代码编译按顺序执行,还没有后面起的别名,所以struct后面的名字不能省略,因为内部还有变量要使用
- 第一个结构体用来存储链表本体
- 第二个结构体就是我们定义的链表队列结构体,用来保存上面链表的头指针和尾指针
- 这里的链表依然带有头结点;考试基本不会要求写不带头结点的链表,因为没有头结点的链表实现起来比较麻烦,浪费时间没必要
- 考试的重点在实现的原理,不是那些不重要的代码细节
图示数据结构网站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
链表队列的代码实现
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
typedef int ElemType;
typedef struct LinkNode {
ElemType data;
struct LinkNode* next;
}LinkNode;
typedef struct {
LinkNode* front, * rear;//链表头,链表尾
}LinkQueue;//先进先出
//初始化
void InitQueue(LinkQueue& Q) {
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));//头和尾指向同一个节点
Q.front->next = NULL;//头结点的next指向null
}
//判断是否为空
bool IsEmpty(LinkQueue Q) {
if (Q.front == Q.rear) {
return true;
}
else {
return false;
}
}
//尾插法入队
void EnQueue(LinkQueue& Q, ElemType x) {
LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x; s->next = NULL;
Q.rear->next = s;//rear始终指向尾部
Q.rear = s;
}
//头部删除法
bool DeQueue(LinkQueue& Q, ElemType& x) {
if (Q.front == Q.rear) {
return false;
}
LinkNode* p = Q.front->next;//头结点什么也不存,,下一个才有数据
x = p->data;
Q.front->next = p->next;//断链
if (Q.rear == p) {//删除的是最后一个元素
Q.rear = Q.front;//队列置为空
}
free(p);
return true;
}
int main() {
LinkQueue Q;
bool ret;
ElemType element;
InitQueue(Q);
EnQueue(Q, 3);
EnQueue(Q, 4);
EnQueue(Q, 5);
EnQueue(Q, 6);
EnQueue(Q, 7);
ret = DeQueue(Q, element);
if (ret) {
printf("出队成功,出队元素为%d\n", element);
}
}
斐波那契数列
- 题目:n个台阶,每次只能上1个台阶或2个台阶,问有多少中走法?
- 比较简单,理解原理就好,考试可能考选择题
f(n+2)=f(n)+f(n+1)
- 需要注意的是f(1)=0和f(2)=1需要单独拿出来判断
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int Fib(int n) {
if (n == 0) {
return 0;
}
else if (n == 1)
{
return 1;
}
else
{
return Fib(n - 1) + Fib(n - 2);
}
}
int main() {
int num;
while (scanf("%d", &num) != EOF) {
printf("Fib(%d) = %d\n", num, Fib(num));
}
}
树(Tree)
- 任意一颗非空数应满足:
- 有且仅有一个特定的成为根的节点
- 截取任意一个子节点和下面的部分,单独都可以看做是一个数
- 数作为一个逻辑结构,分层结构也具备
- 根节点没有前驱节点,除了根节点外,其他所有节点有且仅有一个前驱
- 树中的所有节点可以有多个或零个后继
- 树的层
二叉树
- 多叉树(比如B树)考大题的概率约等于零,主要考大题
- 每个节点最多两个子节点,也叫度最多等于2
- 子树有左右之分,顺序不能颠倒
- 从任何一个子节点向下看,都可以是一个独立的二叉树
二叉树的顺序存储
- 左边是我们抽象出来的树形结构,其实真正是存储在右侧的数组中
- 节点不为空的直存储实际数值,节点不存在位置存储0
- 按照顺序从上到下,从左到右,一层层的存储
二叉树的链式存储
- 考试如果考二叉树建树,一定是考层次建树,因为简单直接,工作有实际使用;因为其他方式太杂了
- 使用每个节点使用左指针,右指针,指向子节点
- 满二叉树:每一层的节点数均为 2的(层数-1)次幂
- 完全二叉树:每一层只有放满才能放下一层,每一层放置的时候只能从左到右的存放
- 因此,满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树
代码实现建完全二叉树
- 层次建树一定要使用辅助队列,不用管为什么,学会使用就行
- 层次建树的时候,每新建一个节点,都要在辅助队列中保存新的节点到辅助队列中
- 如果某一个节点的左右指针多存放满了,那么辅助队列就把这个节点出队,继续队列中的下一个节点,
- 这样形成的效果就是,从上到下,从左到右,层次建树
- calloc申请的空间默认都置0,calloc申请的空间为第一个参数(通常为1)乘第二参数
- 我们的demo里一个节点结构体占12字节,因为第二第三个变量自动填满4字节了
二叉树层次建树,前序遍历代码实现
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
typedef char BiElemType;
//定义二叉树节点
typedef struct BiTNode {//一个节点占12字节,c的位置会自动对齐占满4字节
BiElemType c;//保存节点上的数据
struct BiTNode* lchild;//左指针
struct BiTNode* rchild;//右指针
}BiTNode, *BiTree;
//辅助队列的节点(还是个不带头结点的队列)
//每新增节点都会进入辅助队列,当这个节点的左右指针都有了,就出队列
//这里实际上我们并没有出队,而是又找了一个指针进行移动,但是效果都是一样的
typedef struct tag {
BiTree p;//指向对应的二叉树节点
struct tag* pnext;//指向辅助队列的下一个节点
}tag_t, *ptag_t;
//前序遍历(递归实现)
void preOrder(BiTree p) {
if (p != NULL) {
putchar(p->c);
preOrder(p->lchild);
preOrder(p->rchild);
}
}
int main() {
BiTree pnew;
int i, j, pos;
char c;
BiTree tree = NULL;//二叉树树根
ptag_t phead = NULL;//辅助队列头
ptag_t ptail = NULL;//辅助队列尾
ptag_t listpnew, pcur = NULL;
//开始二叉树层次建树
//输入:abcdefghij
while (scanf("%c", &c) != EOF) {
if (c == '\n') {
break;
}
//给二叉树节点申请空间,pnew用来中转的
pnew = (BiTree)calloc(1, sizeof(BiTNode));
pnew->c = c;//存入数据
//给辅助队列申请节点空间,listpnew用来中转的
listpnew = (ptag_t)calloc(1, sizeof(tag_t));
listpnew->p = pnew;
if (NULL == tree) {//新建的二叉树节点为null
tree = pnew;//树的根
phead = listpnew;//辅助队列头
ptail = listpnew;//辅助队列尾
pcur = listpnew;//辅助队列游标
continue;
}
else
{
ptail->pnext = listpnew;//新节点存入辅助队列,尾插法
ptail = listpnew;//更新一下尾结点的位置,ptail始终指向尾结点
}
if (NULL == pcur->p->lchild) {//辅助队列的游标指向的节点是否有左指针
pcur->p->lchild = pnew;//把新增的节点的引用存放在当前节点的左指针下
}
else if (NULL == pcur->p->rchild) {
pcur->p->rchild = pnew;
//节点的右指针也存放了,则辅助队列的游标更新到下一个队列节点
pcur = pcur->pnext;
}
}
printf("----前序遍历----\n");
preOrder(tree);
}
8.二叉树中序,后序,层序遍历(考试常考),线索二叉树
建树要结合画图进行,理解会更容易一些
前序,中序,后序
- 前序遍历:也叫先序遍历,先打印当前节点,再打印做孩子,再带引右孩子
- 中序遍历:先打印左孩子,再打印父节点,再打印右孩子;
- 如果中序遍历画的足够标准,一脚踩扁的得到的一维顺序,就是遍历的顺序
- 后序遍历:先打印左孩子,再打印右孩子,再打印当前节点
- 代码上,前序,中序,后序都差不多,就是前后顺序有些不同
- 有时候可能会给出二叉树的图形,问打印出来是什么结果
- 二叉树前序中序后序的遍历,考试时尽量使用递归法,因为代码好写,除非有特殊要求
解题思路:一步一步的利用递归思想新增
- 前序遍历推导:abc->abdecfg->abdhiejcfg
- 中序遍历推导:bac->dbeafcg->hdibjeafcg
- 后序遍历推导:bca->debfgca->hidjebfgca
中序遍历非递归
- 难度略大,如果出题也是出在大题上;需要借助栈来实现
- 递归的方式会反复的向系统申请和释放空间,比较占用系统资源;非递归的方法执行效率更高
- 非递归前序遍历非递归后续遍历也基本不会考
- 使用了栈作为辅助
- 中序遍历非递归的代码量不大,但是设计非常巧妙,需要画图一步一步的实现,去理解
- 个人理解:不断的找入栈,然后找左孩子;找不到左孩子了,就出栈,然后找右孩子;
- 找的到右孩子,继续入栈,然后找左孩子;找不到右孩子,则继续出栈,再找右孩子
- 总体就是:
- 入栈,找左孩子
- 找的到孩子,就继续入栈,找左孩子
- 找不到孩子,就改为出栈,找右孩子
注意:执行以下代码之前需要先,定义树,定义层次建树辅助队列,层次建树,非递归中序遍历辅助栈,
//中序遍历非递归
void InOrder2(BiTree T) {
SqStack S;
InitStack(S);
BiTree p = T;
while (p || !StackEmpty(S)) {
if (p) {
Push(S, p);
p = p->lchild;
}
else
{
Pop(S, p);
putchar(p->c);
p = p->rchild;
}
}
}
广度,深度遍历
- 前序遍历属于深度优先遍历
- 层次遍历属于广度优先遍历,
层次遍历
- 和层次建树一样,需要使用辅助队列,尾插入队,头部删除出队
- 基本上就是小题,考大题的概率很低;因为难度大,且代码多
按照如下代码,输入一串字符,得到的层次建树的顺序与层次遍历的结果是一样的
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#define MaxSize 50
typedef char BiElemType;
//定义二叉树节点
typedef struct BiTNode {
BiElemType c;
struct BiTNode* lchild;
struct BiTNode* rchild;
}BiTNode, * BiTree;
//给二叉树节点又起了个别名
typedef BiTree ElemType;
//层次遍历辅助队列节点
typedef struct LinkNode {
ElemType data;
struct LinkNode* next;
}LinkNode;
//层次遍历辅助队列(入队位置,出队位置)
typedef struct {
LinkNode* front, * rear;
}LinkQueue;
//初始化辅助队列(带头结点)
void InitQueue(LinkQueue& Q) {
Q.front = Q.rear = (LinkNode*)malloc(sizeof(LinkNode));
Q.front->next = NULL;//头结点不存数据
}
//判断队列是否为空
bool IsEmpty(LinkQueue Q) {
if (Q.front == Q.rear) {
return true;
}
else {
return false;
}
}
//入队
void EnQueue(LinkQueue& Q, ElemType x) {
LinkNode* s = (LinkNode*)malloc(sizeof(LinkNode));
s->data = x; s->next = NULL;
Q.rear->next = s;
Q.rear = s;
}
//出队
bool DeQueue(LinkQueue& Q, ElemType& x) {
if (Q.front == Q.rear) {
return false;
}
LinkNode* p = Q.front->next;//头结点的下一个节点才有数据
x = p->data;
Q.front->next = p->next;
if (Q.rear == p) {
Q.rear = Q.front;
}
free(p);
return true;
}
//层次遍历
void LevelOrder(BiTree T) {
LinkQueue Q;//辅助队列
InitQueue(Q);//初始化辅助队列
BiTree p;//临时中转用的树节点
EnQueue(Q, T);//树根入队列
while (!IsEmpty(Q)) {
DeQueue(Q, p);
putchar(p->c);
if (p->lchild != NULL) {
EnQueue(Q, p->lchild);
}
if (p->rchild != NULL) {
EnQueue(Q, p->rchild);
}
}
}
//**************二叉树层次建树重复代码**************
//层次建树辅助队列
typedef struct tag {
BiTree p;//指向对应的二叉树节点
struct tag* pnext;//指向辅助队列的下一个节点
}tag_t, * ptag_t;
//非递归中序遍历辅助栈
typedef struct {
ElemType data[MaxSize];
int top;
}SqStack;
//初始化栈
void InitStack(SqStack& S) {
S.top = -1;//初始化栈为空
}
//判断栈是否为空
bool StackEmpty(SqStack& S) {
if (-1 == S.top) {
return true;
}
return false;
}
//入栈
bool Push(SqStack& S, ElemType x) {
if (S.top == MaxSize - 1) {
return false;
}
S.data[++S.top] = x;
return true;
}
//出栈
bool Pop(SqStack& S, ElemType& x) {
if (StackEmpty(S)) {
return false;
}
x = S.data[S.top--];
return true;
}
//中序遍历非递归
void InOrder2(BiTree T) {
SqStack S;
InitStack(S);
BiTree p = T;
while (p || !StackEmpty(S)) {
if (p) {
Push(S, p);
p = p->lchild;
}
else
{
Pop(S, p);
putchar(p->c);
p = p->rchild;
}
}
}
//*************************************************
int main() {
BiTree pnew;
int i, j, pos;
char c;
BiTree tree = NULL;//二叉树树根
ptag_t phead = NULL;//辅助队列头
ptag_t ptail = NULL;//辅助队列尾
ptag_t listpnew = NULL, pcur = NULL;
//开始二叉树层次建树
//输入:abcdefghij
while (scanf("%c", &c) != EOF) {
if (c == '\n') {
break;
}
//给二叉树节点申请空间,pnew用来中转的
pnew = (BiTree)calloc(1, sizeof(BiTNode));
pnew->c = c;//存入数据
//给辅助队列申请节点空间,listpnew用来中转的
listpnew = (ptag_t)calloc(1, sizeof(tag_t));
listpnew->p = pnew;
if (NULL == tree) {//新建的二叉树节点为null
tree = pnew;//树的根
phead = listpnew;//辅助队列头
ptail = listpnew;//辅助队列尾
pcur = listpnew;//辅助队列游标
continue;
}
else
{
ptail->pnext = listpnew;
ptail = listpnew;
}
if (NULL == pcur->p->lchild) {
pcur->p->lchild = pnew;
}
else if (NULL == pcur->p->rchild) {
pcur->p->rchild = pnew;
pcur = pcur->pnext;
}
}
//层次遍历
LevelOrder(tree);
}
线索二叉树
- 考试很少考,大题更不可能出
- 因为在生产中几乎一无是处
- 学习纯粹是提高思维
- 比传统的二叉树节点又多了两个标记
- 当一个节点的左孩子或右孩子是空着的,线索二叉树不让节点的左右孩子是空着
- 当标记为0,表示左指针或右指针真实的指向孩子
- 当左标记为1时,左指针指向当前节点的前驱
- 当右标记为1时,右指针指向当前节点的后继
- 何为前驱,后继?线索二叉树建树,一定是先建立二叉树,
- 先有二叉树,然后开始遍历并 线索化,所以这里遍历二叉树的顺序,其实就是线索二叉树中的前驱,后继的来源
- 线索二叉树可以用前序遍历建立,也可以用中序遍历来简历,通常中序的情况考的多
- 考试时,考小题,某一节点左孩子右孩子之类的问题,记住连线图就足够考试
- 如果真考大题,可以用窍门,准备一个队列,存储遍历的顺序,方便求出前驱和后继
- 这的代码调试太乱了,先不看了 ,递归的调试难度很大,新手很容易看不懂
以中序遍历线索化为例,如图为线索化的顺序
代码略,见课件
9.二叉查找树,顺序查找,折半查找
课堂遗留问题
- 循环队列是存储结构还是逻辑结构
- 其实,数组与链表都可以实现循环队列,所以循环队列应该是个逻辑结构
- 但是,很多教材都默认使用了数组实现,默认是一种存储结构
- 为了考试,还是按照存储结构来记忆
二叉排序树(考试重点)
- 又称二叉查找树,满足条件
- 若左子树非空,则左子树上所有节点的值均小于根节点值
- 若右子树非空,则右子树上所有节点的值均大于根节点值
- 左右子树也分别是一个二叉排序树
- 动画网站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
建树过程使用了递归插入,每一次递归都会进入到下一层,下一层改变的引用会影响上一层的值,略有一点绕,需要注意下
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
typedef int KeyType;
typedef struct BSTNode {
KeyType key;
struct BSTNode* lchild, * rchild;
}BSTNode, * BiTree;
//54,50,66,40,28,79,58
//二叉排序树插入元素
int BST_Insert(BiTree& T, KeyType k) {
if (NULL == T) {
//为新增节点申请空间,第一个节点作为树根
T = (BiTree)malloc(sizeof(BSTNode));
T->key = k;
T->lchild = T->rchild = NULL;
return 1;//1代表插入成功
}
else if (k == T->key) {
return 0;//发现相同元素,不插入
}
else if (k < T->key) {
return BST_Insert(T->lchild, k);
}
else {
return BST_Insert(T->rchild, k);
}
}
//创建二叉排序树
void Creat_BST(BiTree& T, KeyType data[], int n) {
T = NULL;
int i = 0;
while (i < n) {
BST_Insert(T, data[i]);//把某一节点放入二叉排序树
i++;
}
}
//中序遍历,结果就是从小到大排列
void InOrder(BiTree T) {
if (T != NULL) {
InOrder(T->lchild);
printf("%3d", T->key);
InOrder(T->rchild);
}
}
//查找指定元素位置,这里使用的非递归;递归算法简单,但执行效率低,自行编写;
BSTNode* BST_Search(BiTree T, KeyType key, BiTree& p) {
p = NULL;
while (T != NULL && key != T->key) {
p = T;
if (key < T->key) {
T = T->lchild;
}
else {
T = T->rchild;
}
}
return T;
}
//删除某个节点,递归实现
void DeleteNode(BiTree& root, KeyType x) {
if (root == NULL) {
return;
}
if (root->key > x) {
DeleteNode(root->lchild, x);
}
else if (root->key < x) {
DeleteNode(root->rchild, x);
}
else {//查找到了要删除的节点
if (root->lchild == NULL) {//左子树为空,则右子树直接顶上
BiTree tempNode = root;//临时存储一下用来一会free
root = root->rchild;
free(tempNode);
}
else if (root->rchild == NULL) {
BiTree tempNode = root;
root = root->lchild;
free(tempNode);
}
else {
//左右子树都不为空
//一般删除策略为,选择左子树的最大数据,或者右子树的最小数代替要删除的节点数据,
//这里选择查询左子树最大数据
BiTree tempNode = root->lchild;
if (tempNode->rchild != NULL) {
tempNode = tempNode->rchild;
}
root->key = tempNode->key;
//顶替者原来的位置继续被下面的子节点顶替
DeleteNode(root->lchild, tempNode->key);
}
}
}
int main() {
BiTree T = NULL;//树根
BiTree parent;//临时存储父节点地址值
BiTree search;
KeyType data[] = { 54,20,66,40,28,79,58 };//将要进入二叉排序树的元素
Creat_BST(T, data, 7);
InOrder(T);
printf("\n");
search = BST_Search(T, 28, parent);
if (search) {
printf("查找成功,值为%d\n", search->key);
}
else {
printf("查找失败\n");
}
DeleteNode(T, 54);
InOrder(T);
printf("\n");
return 0;
}
二叉排序树结构
删除54节点之后
顺序查找
- 又称线性查找,从前到后,暴力查找
- 对于顺序表和链表都适用
- 对于顺序表,通过数组下标递增来依次扫描每个元素
- 对于链表,通next指针来依次扫描每个元素
顺序查找代码实现
- 动态数组的实现:根据实际的元素个数使用malloc动态申请内存空间,得到的也是个数组,只不过这个数组使用的堆空间
- 随机数生成的代码固定记住就行,不要用写死的代码,写死的代码排序查找有bug
- 动态数组多留了一个位置,用来存储哨兵,用来识别遍历结束;就是标记,通过限制i<length也一样实现
二分查找
- 又称折半查找,要求使用时,必须是针对有序数组;链表做不到,因为不能随机访问下标
- 这里使用现成的qsort接口进行了快排,机试时经常使用qsort
- 使用qsort需要自定义一个compare函数,用来定义qsort的排序规则;
如果compare的第一个参数大于第二个参数时,compare返回正,则sqort按从小到大排;反之同理
二分法查找代码实现
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
typedef int ElemType;
typedef struct {
ElemType* elem;//整型指针
int TableLen;//存储动态数组里边元素的个数
}SSTable;
//顺序遍历 暴力遍历
int Search_Seq(SSTable ST, ElemType key) {
ST.elem[0] = key;
int i;
for (i = ST.TableLen - 1; ST.elem[i] != key; --i);
return i;
}
//生成随机数
void ST_Init(SSTable& ST, int len) {
ST.TableLen = len + 1;//多申请一个位置,为了存哨兵
ST.elem = (ElemType*)malloc(sizeof(ElemType) * ST.TableLen);
int i;
srand(time(NULL));//随机数生成
for (i = 0; i < ST.TableLen; i++) {//为什么零号位置也随机了数据,为了折半查找服务
ST.elem[i] = rand() % 100;
}
}
//打印随机数组
void ST_print(SSTable ST) {
for (int i = 0; i < ST.TableLen; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
//二分查找
int Binary_Search(SSTable L, ElemType key) {
int low = 0, high = L.TableLen - 1, mid;
while (low <= high) {
mid = (low + high) / 2;
if (L.elem[mid] == key) {
return mid;
}
else if (L.elem[mid] > key) {
high = mid - 1;
}
else {
low = mid + 1;
}
}
return -1;
}
//自定义比较规则
//第一个参数比第二个参数大,则返回为正值;第一个参数比第二个参数小,返回负值;
//第一个参数等于第二个参数,则返回0;以上代表从小到大排列
int compare(const void* left, const void* right) {
//return *(ElemType*)left - *(ElemType*)right;
return *(ElemType*)left - *(ElemType*)right;
}
int main() {
SSTable ST;
ElemType key;
int pos;//存储查找元素的位置
ST_Init(ST, 10);
ST_print(ST);
printf("请输入要搜索的key值:\n");
scanf("%d", &key);
pos = Search_Seq(ST, key);
if (pos) {
printf("查找成功,位置为:%d\n", pos);
}
else {
printf("查找失败\n");
}
qsort(ST.elem, ST.TableLen, sizeof(ElemType), compare);
ST_print(ST);
printf("二分查找,请输入要搜索的key值:\n");
scanf("%d", &key);
pos = Binary_Search(ST, key);
if (pos != -1) {
printf("查找成功,位置为:%d\n", pos);
}
else {
printf("查找失败\n");
}
}
Bug:暴力查找的元素为什么在打印输出的时候有元素丢失??
10.作业,散列,串的暴力与KMP
作业
- 不要刻意的为了简化代码,而减少中间变量的定义;这么做会导致代码难以阅读,按需定义即可
- 养成经常单步调试,遇到问题先调试的习惯
- OJ在检查逻辑判断分支的时候,都要加上返回,以免出现wrong answer
哈希查找(散列查找)
- 代码考得几率很低,考试更多是在考散列原理
- 实际工作中经常使用,而且是拿来即用,无需自己设计
- 散列函数:
- 一个运算公式,也叫哈希,散列函数,
- 能够把查找表中的关键字给映射成对应的地址值
- 这个地址值可以使数组下标,索引,内存地址等,
Hash(key) = Addr
- 哈希函数可以针对任何元素进行计算,因为是针对存储的字节进行的运算
- 利用元素算出地址,直接对其访问,这样时间复杂度就是O(1)
- 可能会把不同的关键字映射到同一地址上,这叫做冲突
- 解决冲突:在对应的冲突的地址上形成链表
- 散列表
- 根据关键字而直接进行访问的数据结构
- 即,散列表建立了关键和存储地址之间的一种直接映射关系
- 理想状态下,对散列表查找的时间复杂度为O(1),与表中元素个数无关
- 所以,如果出现冲突,时间复杂度就不是O(1),因为还要在链表里找
- 哈希表一定是个数组,这样才支持随机访问
实现代码(考试不考,仅用于理解原理)
#define MaxKey 1000
#include <stdio.h>
//业界常用的一个哈希函数
int hash(const char* key) {
int h = 0, g;
while (*key) {
h = (h << 4) + *key++;
g = h & 0xf0000000;
if (g) {
h ^= g >> 24;
}
h &= ~g;
}
return h % MaxKey;//算出的下标总要有个范围,因此取模
}
int main() {
const char* pStr[5] = { "jack","mike","rose","john","tom" };
int i;
const char* pHash_table[MaxKey] = { NULL };//哈希表,散列表
for (i = 0; i < 5; i++) {
printf("%s is key=%d\n", pStr[i], hash(pStr[i]));
pHash_table[hash(pStr[i])] = pStr[i];
}
return 0;
}
KMP
- 先理解暴力匹配,搞清楚i与j的关系
- KMP理解原理即可,不要求撸代码,考试不会考大题,但是经常考选择题
- 一种改进的字符串匹配算法
- 核心在于如何确定next数组
- 不用特意记代码,理解原理能人工求出next数组就可以
- 有了next数组,就可以在不匹配的时候根据next数组进行回退
- next数组第一个位置存放长度
- 考试主要就是考,给定一个字符串,求出期对应的next数组
- next数组是根据
next数组的规律参考,主要借鉴思想,文章中的与这里的有一点区别
next数组举例
11.冒泡排序,快速排序,插入排序
概述
- 排序算法主要分:插入排序,交换排序,选择排序,归并排序
- 交换排序分:冒泡排序,快速排序
- 插入排序分:直接插入排序,折半插入排序,希尔排序
- 选择排序分:简单选择排序,堆排序
冒泡排序
- 基本思想:从前往后,或从后往前,每一趟冒泡都会两两元素比较,排好先后顺序,一趟一趟的进行两两交换
- 第一能得到最小的元素,第二趟能得到第二小的元素,以此类推
- 这个过程很像一个上浮的气泡
冒泡排序代码分析
- 使用了随机数生成,为了避免巧合,产生伪排序,接口详情可以参考手册
- 初始化数组完全可以直接使用数组,这里我们仍然使用了申请内存的方式
- 对于整型数组或浮点型的copy时,使用memcpy函数
- 为什么不用stycopy,因为strcpy拷贝时遇0结束;memcpy则是根据类型大小拷贝
- 这里我们准备一个固定数组,是为了便于写代码,等排序代码写好后,可以注释掉,换成随机生成数组即可
- 尽量按原理去记忆代码
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct {
ElemType* elem;//存储元素起始地址
int TableLen;//元素个数
}SSTable;
//打印数组
void ST_print(SSTable ST) {
int i;
for (i = 0; i < ST.TableLen; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
//初始化排序数组
void ST_Init(SSTable& ST, int len) {
ST.TableLen = len;
ST.elem = (ElemType*)malloc(sizeof(ElemType) * ST.TableLen);
int i;
srand(time(NULL));//随机数生成,每执行一次代码,就会随机生成10个元素
for (i = 0; i < ST.TableLen; i++) {
ST.elem[i] = rand() % 100;
}
}
//交换元素位置
void swap(ElemType& a, ElemType& b) {
ElemType tmp;
tmp = a;
a = b;
b = tmp;
}
//冒泡排序
void BubbleSort(ElemType A[], int n) {
//外层i控制已经有多少个有序数了,内层j控制比较和交换
//flag用于标记,如果一轮冒泡已经没有数据交换,说明后面不用再循环了
//注意找准循环的起始,结束的边界
int i, j, flag;
for (i = 0; i < n - 1; i++) {
flag = 0;
for (j = n - 1; j > i; j--) {
if (A[j - 1] > A[j]) {
swap(A[j - 1], A[j]);
flag = 1;
}
}
if (flag == 0) {
break;
}
}
}
int main() {
SSTable ST;
ElemType A[10] = { 64,94,95,79,69,84,18,22,12,78 };
ST_Init(ST, 10);
//先把A拷贝到ST中是为了方便调试,注释调拷贝代码重新变回随即数组
//memcpy(ST.elem, A, sizeof(A));//内存拷贝接口,适用于整型,浮点
ST_print(ST);
BubbleSort(ST.elem, ST.TableLen);
ST_print(ST);
}
快速排序
- 基本思想:分而治之。
- 找一个分割值,将数组一分为二,小的放左边,大的放右边
- 再次对上一步分开的两部分,分别进行分而治之,
- 不断地进行递归,不断地分而治之,到最后不能再分,也就排完了,得到的就是从小到大的排序
- 推荐使用龙哥的快排例子,最简单容易理解
- 动画网站演示排序原理
代码实现:
//i用来存遍历数组的下标
//k是比分割值小的元素将要存的下标
//分割过程:i从左到右遍历一遍,i遇到比最右侧小的,就放在k的后边,最后k再和最右调换;得到的就是以k为分割点,左边是小于k的,右边是大于k的结果;
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct {
ElemType* elem;//存储元素起始地址
int TableLen;//元素个数
}SSTable;
//打印数组
void ST_print(SSTable ST) {
int i;
for (i = 0; i < ST.TableLen; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
//初始化排序数组
void ST_Init(SSTable& ST, int len) {
ST.TableLen = len;
ST.elem = (ElemType*)malloc(sizeof(ElemType) * ST.TableLen);
int i;
srand(time(NULL));//随机数生成,每执行一次代码,就会随机生成10个元素
for (i = 0; i < ST.TableLen; i++) {
ST.elem[i] = rand() % 100;
}
}
//交换元素位置
void swap(ElemType& a, ElemType& b) {
ElemType tmp;
tmp = a;
a = b;
b = tmp;
}
//快排核心代码-分割
//如果想要单步调试的话,可以把形参ElemType arr[]换成int *arr,方便查看内存
int Partition(ElemType arr[], int left, int right) {
int i, k;
for (i = left, k = left; i < right; i++) {
if (arr[i] < arr[right]) {
swap(arr[k], arr[i]);
k++;
}
}
swap(arr[k],arr[i]);
return k;
}
//递归实现快排
//极限位置就是,low和high相邻且low<high,然后递归结束
void QuickSort(ElemType A[], int low, int high) {
if (low < high) {
int pivotpos = Partition(A, low, high);
QuickSort(A, low, pivotpos - 1);
QuickSort(A, pivotpos + 1, high);
}
}
int main() {
SSTable ST;
ElemType A[10] = { 64,94,95,79,69,84,18,22,12,78 };
ST_Init(ST, 10);
//先把A拷贝到ST中是为了方便调试,注释调拷贝代码重新变回随即数组
//memcpy(ST.elem, A, sizeof(A));//内存拷贝接口,适用于整型,浮点
ST_print(ST);
QuickSort(ST.elem, 0, 9);
ST_print(ST);
}
快速排序(挖坑法)
- 基本思想类似,都是不断地递归,分而治之,分到不能再分即排序结束
- 区别在于分割的方法不同,下面以一次分割循环为例
- 把最左边的Low取出,暂存为middle,作为分界点的值;
- 从右向左遍历,遇到比middle小的,丢到左侧,同时high–,循环结束,右侧多出了一个坑位
- 从左向右遍历,遇到比middle大的,丢到右侧的坑位里,同时low–,循环结束,右侧坑填上,左侧空出了坑位;内层循环结束
- 外层循环不断进行,极限位置,low和high重合的位置就是坑位,把middle放下来,本轮的分割结束,左侧比middle小,右侧比middle大
- 后面递归分割方法与上面快排相同
- 分割的核心思想就是,取一个middle暂存,左右两侧轮流向对面丢过去比middle大的或小的
挖坑法分割代码
//挖坑法-分割
int Partition2(ElemType A[], int low, int high)
{
ElemType pivot = A[low];//把最左边的值暂存起来
while (low < high)
{
while (low < high && A[high] >= pivot)//high从右边开始找,找到比分割值小的,循环结束,存到左边
{
--high;
}
A[low] = A[high];
while (low < high && A[low] <= pivot)//high从左边开始找,找到比分割值大的,循环结束,存到右边
{
++low;
}
A[high] = A[low];
}
A[low] = pivot;
return low;
}
直接插入排序
- 每新增一条数据都和已有的有序元素进行比较,并排好序
- 使用场景,适用于新增元素到已有的有序数组中
- 考试大纲没有规定必须使用哨兵,但是我们使用够可以简化代码
- 每次要插入的数据都放在哨兵的位置上
-
- 注意:第一个元素A[0]是哨兵,不计入排序结果
- 考试的时候如果使用哨兵,只需给代码加个备注说明即可
- 其实不用哨兵,而是在外面定义一个中间变量也是可以,只不过不如使用哨兵更简洁,更清晰
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct {
ElemType* elem;//存储元素起始地址
int TableLen;//元素个数
}SSTable;
//打印数组
void ST_print(SSTable ST) {
int i;
for (i = 0; i < ST.TableLen; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
//初始化排序数组
void ST_Init(SSTable& ST, int len) {
ST.TableLen = len + 1;//实际申请了11个空间
ST.elem = (ElemType*)malloc(sizeof(ElemType) * ST.TableLen);
int i;
srand(time(NULL));//随机数生成,每执行一次代码,就会随机生成10个元素
for (i = 0; i < ST.TableLen; i++) {
ST.elem[i] = rand() % 100;//第一个元素是没有用到的
}
}
//交换元素位置
void swap(ElemType& a, ElemType& b) {
ElemType tmp;
tmp = a;
a = b;
b = tmp;
}
//插入排序,从小到大
void InsertSort(ElemType A[], int n) {
int i, j;
//第一个元素是哨兵,从第二个元素开始拿,往前面插入
for (i = 2; i <= n; i++) {
if (A[i] < A[i - 1]) {
A[0] = A[i];//A[0]是暂存,也是哨兵
for (j = i - 1; A[0] < A[j]; --j) {//移动元素,内层循环控制有序序列中的每一个元素和要插入的元素比较
A[j + 1] = A[j];
}
A[j + 1] = A[0];//把暂存元素插入到哨兵的位置
}
}
}
int main() {
SSTable ST;
ElemType A[10] = { 64,94,95,79,69,84,18,22,12,78 };
ST_Init(ST, 10);
//先把A拷贝到ST中是为了方便调试,注释调拷贝代码重新变回随即数组
//memcpy(ST.elem, A, sizeof(A));//内存拷贝接口,适用于整型,浮点
ST_print(ST);
InsertSort(ST.elem, 10);
ST_print(ST);
}
12.折半插入排序,希尔排序,选择排序,堆排序
折半插入排序
- 本质上也是插入排序
- 与直接插入排序的区别是,内层循环找插入位置的时候,原来是遍历并比较找到待插入的位置,现在采用了二分法查找,找到待插入位置
- 相比于直接插入,减少了一些查找过程,但是时间复杂度依然与直接插入相同
- 时间复杂度为O(n平方),插入排序考大题概率很低,因为时间复杂度大,没有实用意义
折半插入代码
//折半插入排序
void MidInsertSort(ElemType A[], int n)
{
int i, j, low, high, mid;
for (i = 2; i <= n; i++)
{
A[0] = A[i];
low = 1; high = i - 1;
while (low <= high)
{
mid = (low + high) / 2;
if (A[mid] > A[0])
high = mid - 1;
else
low = mid + 1;
}
for (j = i - 1; j >= high + 1; --j)
A[j + 1] = A[j];
A[high + 1] = A[0];
}
}
希尔排序
- 考大题概率等于零,编写复杂,效率不如快排,堆排
- 考试会考有关步长的小题,能做到,手工调整一次,就满足考试需求了
- 思想:用更大的步长来进行比较,提高比较效率,让元素一次性跨越更大的位置
- 最外层多了一层循环,用来 控制步长,但运算次数比常规插入法进一步减少了
- 举例:长度为11的数组,首先用步长5来进行插入法比较;再用2作为步长来插入法比较
- 代码窍门:基于原有插入排序,所有i-1的位置,都变成了i-步长
- 调试技巧:自己手工排序与计算机单步调试对比,检验排序效果
代码实现
//基于直接插入排序的改进
//希尔排序
void ShellSort(ElemType A[], int n)
{
int dk, i, j;
for (dk = n / 2; dk >= 1; dk = dk / 2)//步长变化
{
for (i = dk + 1; i <= n; ++i)//以dk为步长进行插入排序
{
if (A[i] < A[i - dk])
{
A[0] = A[i];
for (j = i - dk; j > 0 && A[0] < A[j]; j = j - dk)
A[j + dk] = A[j];
A[j + dk] = A[0];
}
}
}
}
选择排序:简单选择排序,堆排序
考试重点:快排,堆排,主要考代码
简单选择排序
- 考试不会考大题,选择小题为主;因为代码太简单,出大题不值得
- 定义一个最小的下标,向后遍历,谁更小谁就换上,这样一轮下来,可以找到最小值
- 不断的重复剩余的,得到第二最小,第三最小的,以此类推
- 时间复杂度与冒泡是相同;只不过,一个是不断两两比较,交换,一个是一轮轮的找最小;其实有些类似
代码实现
void SelectSort(ElemType A[], int n)
{
int i, j, min;//记录最小元素下标
for (i = 0; i < n - 1; i++)
{
min = i;
for (j = i + 1; j < n; j++)//j最多可以到9
{
if (A[j] < A[min])
{
min = j;
}
}
if (min != i)
{
swap(A[i], A[min]);
}
}
}
堆排序 Heap(重点)
- 需要先掌握二叉树,层次建树,完全二叉树,大顶堆,小顶堆
- 将数组的元素按顺序与层次建树的节点形成对应关系
- 堆的结构是我们想象出来的,并没靠程序建立
- 堆排序的时间复杂度为O(NlogN)
代码思想
- 代码主要分为:建堆,堆的维护(调整),排序数组
- 需要明确堆的父子节点的下标关系
- 建堆是从下到上的顺序,维护堆是从上到下的顺序
- 维护堆的代码可以有两种写法:递归和非递归
- 建好堆之后,不断地取走堆,维护堆,筛选出的元素刚好是降序排列的
代码实现
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
typedef int ElemType;
typedef struct {
ElemType* elem;//存储元素起始地址
int TableLen;//元素个数
}SSTable;
//打印数组
void ST_print(SSTable ST) {
int i;
for (i = 0; i < ST.TableLen; i++) {
printf("%3d", ST.elem[i]);
}
printf("\n");
}
//初始化排序数组
void ST_Init(SSTable& ST, int len) {
ST.TableLen = len;
ST.elem = (ElemType*)malloc(sizeof(ElemType) * ST.TableLen);
int i;
srand(time(NULL));//随机数生成,每执行一次代码,就会随机生成10个元素
for (i = 0; i < ST.TableLen; i++) {
ST.elem[i] = rand() % 100;
}
}
//交换元素位置
void swap(ElemType& a, ElemType& b) {
ElemType tmp;
tmp = a;
a = b;
b = tmp;
}
//调整子树
void AjustDown(ElemType A[], int k, int len)
{
int dad = k;
int son = 2 * dad + 1;//左孩子下标
while (son <= len)
{
if (son + 1 <= len && A[son] < A[son + 1])//看有没有右孩子
{
son++;
}
if (A[son] > A[dad])//比较孩子和父亲
{
swap(A[son], A[dad]);
dad = son;//继续向下层走
son = 2 * dad + 1;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(ElemType A[], int len)
{
int i;
//建立大顶堆
for (i = len / 2; i >= 0; i--)
{
AjustDown(A, i, len);
}
swap(A[0], A[len]);//交换顶部和数组最后一个元素
for (i = len - 1; i > 0; i--)
{
AjustDown(A,0,i);//剩下元素调整为大顶堆
swap(A[0], A[i]);
}
}
int main()
{
SSTable ST;
ST_Init(ST, 10);
ST_print(ST);
HeapSort(ST.elem, 9);//所有元素参与排序
ST_print(ST);
}