一、数据结构
1、什么是数据结构
数据结构:数据的逻辑结构、存储结构及操作。
数据元素及数据元素的相互关系,或组织数据的形式。
2、数据
1、数据(data)
数据不再是单纯的数值了,而是能输入到计算机并识别的总称。
2、数据元素(Data Element)
数据元素是数据的基本单位,一般由若干个数据项组成。
3、节点:相当于数据元素
数据元素------》一条数据 -----》一条记录--------》节点
3、逻辑结构
逻辑结构:研究的是数据元素与数据元素之间的规律和联系。
逻辑关系 | 线性关系 | 层次关系 | 网状关系 |
---|---|---|---|
特点 | 一对一 | 一对多 | 多对多 |
逻辑结构 | 线性结构 | 树状结构 | 图状结构 |
应用 | 顺序表,链表,栈,队列 | 树(二叉树) | 图 |
4、存储结构
存储结构:就是逻辑结构在计算机上应用某种语言的具体实现。
1、顺序存储
要将数据元素与数据元素“连续”存储在内存空间上,本质是数组。
2、链式存储
数据元素与数据元素不连续存储在内存空间上,通过指针将节点链接在一起。
3、索引存储
存源数据表的同时还要建立索引表,查找数据时先去索引表中锁定范围,再到源数据表中查找数据。
4、散列存储
哈希查找:存数据的时候按照对应关系存,取数据的时候按照对应关系取。
5、操作(数据运算)
对数据进行的操作(或运算)不再是加、减、乘、除;而是增、删,改、查
二、算法
算法:就是解决问题的思想办法,有限代码有序的集合
算法的5个重要特性:
(1)有穷性
(2)确定性
(3)可行性
(4)输入
(5)输出
衡量算法的标准
时间复杂度:消耗的时间越少越好
空间复杂度:占用的空间越小越好
三、顺序表
1、顺序表
是一种线性表的存储结构,它使用一段连续的空间来存储数据元素。顺序表中的元素在内存中是顺序存放的,可以通过下标直接访问。
2、顺序表结构体的定义
#define N 10
typedef struct
{
//data 数据
//last 最后 这里表示有效元素的个数
int data[N];//数据域 存放数据的 数据类型可以是int float struct student...
int last;//有效元素的个数
}sequence_t;
3、顺序表的操作
//1.创建顺序表
//2.判断顺序表是否为空 空的话返回1 不空的话反0
//3.判断顺序表是否为满 满的话返回1 不满的话反0
//4.遍历顺序表所有有效元素
//5.在顺序中指定位置插入数据
//6.删除顺序中指定位置上数据
//7.求顺序表的长度
//8.查找某个数据在顺序表中的位置
//9.清空顺序表
#include<stdio.h>
#include<stdlib.h>
#define N 100
//定义一个顺序表结构体
typedef struct
{
int data[N];
int last;//元素个数
}seqlist_t;
//定义一个空的顺序表
seqlist_t* create()
{
seqlist_t* p=(seqlist_t*)malloc(sizeof(seqlist_t));
if(p==NULL)
{
printf("p malloc failed\n");
return NULL;//空间申请失败
}
return p;//申请成功返回首地址
}
//判断一个顺序表是否为空 空的话返回1 不空返回0
int isEmpty(seqlist_t* p)
{
return p->last == 0 ? 1 : 0;
}
//判断一个顺序表是否为满 满的话返回1 不满返回0
int isFull(seqlist_t* p)
{
return p->last == N ? 1 : 0;
}
//遍历顺序表中的所有有效元素
int show(seqlist_t* p)
{
int i=0;
for(i=0;p->data[i]!=0;i++)
{
printf("%d ",p->data[i]);
}
putchar('\n');
}
//在指定位置插入数据
int insert(seqlist_t* p,int post,int x)
{
//容错判断
if(post<=0||post>N||isFull(p))
{
printf("插入数据位置不合理或表已经满啦\n");
return -1;
}
int i=0;
for(i=p->last-1;i>=post-1;i--)
{
p->data[i+1]=p->data[i];
}
p->data[post-1]=x;
p->last++;
return 0;
}
//删除指定位置上的数据
int delete(seqlist_t* p,int post)
{
//容错判断
if(post<=0||post>N||isEmpty(p))
{
printf("删除数据的位置不合理或表是空的\n");
return -1;
}
int i=0;
for(i=post-1;i<p->last;i++)
{
p->data[i]=p->data[i+1];
}
p->last--;
return 0;
}
int lenght(seqlist_t* p)
{
return p->last;
}
//查找某个元素出现在顺序表中的位置
int position(seqlist_t* p,int x)//x是需要查找的数据
{
int i=0;
for(i=0;i<p->last;i++)
{
if(p->data[i]==x)
{
printf("查找的数据所在的位置是:%d\n",i+1);
break;
}
}
if(i==p->last)
printf("该表中没有要查找的数据\n");
return 0;
}
//清空顺序表
int clean(seqlist_t* p)
{
p->last=0;
return 0;
}
int main(int argc, const char *argv[])
{
seqlist_t* p=create();
if(p==NULL)
{
return -1;
}
printf("顺序表中元素的个数:%d\n",p->last);
printf("isEmpty is %d\n",isEmpty(p));
printf("isFull IS %d\n",isFull(p));
show(p);
insert(p,1,10);
show(p);
insert(p,2,20);
insert(p,3,30);
insert(p,4,40);
insert(p,5,50);
show(p);
delete(p,2);
show(p);
printf("顺序表的长度为:%d\n",lenght(p));
position(p,20);
clean(p);
show(p);
printf("顺序表中元素的个数:%d\n",p->last);
insert(p,1,10);
show(p);
printf("顺序表中元素的个数:%d\n",p->last);
return 0;
}
4、顺序表总结
顺序表查找速度快,插入和删除数据慢。
1、顺序表的缺点
a.预估内存,小了不够用涉及到后期扩容,大了浪费内存空间。使用数组必须给长度,长度一旦确定不能改。
b.当数据量大的时候,插入和删除造作麻烦需要移动大量数据。
2、顺序表的优点
a.使用数组有下标,可以实现随机访问
b.查找速度快
四、链表
链表:将线性表中各数据元素储存在不同的存储块,称为节点,通过指针建立它们之间的联系,所得的存储结构为链表结构。
链表分类:单向链表 、单向循环链表、双向链表、双向循环链表
逻辑结构:线性关系 线性结构
存储结构:链式存储
1、单向链表
1.1 单向链表的操作
#include<stdio.h>
#include<stdlib.h>
//链表节点结构体定义
typedef struct node
{
int data;
struct node* next;
}linklist_t;
//单向的有头的链表
//1. 创建一个空的链表
linklist_t* create()
{
linklist_t* p=(linklist_t*)malloc(sizeof(linklist_t));
if(p==NULL)
{
printf("p malloc failed\n");
return NULL;
}
//申请成功
//数据域不用赋值 指针域赋为空
p->next=NULL;
return p;
}
//2. 遍历链表的有效元素
void show(linklist_t* p)
{
while(p->next!=NULL)
{
p=p->next;
printf("%d ",p->data);
}
printf("\n");
}
//3. 求链表的长度
int lenght(linklist_t* p)
{
int len=0;
while(p->next!=NULL)
{
p=p->next;
len++;
}
return len;
}
//4. 向链表的指定位置插入数据
int insert(linklist_t* p,int post,int x)
{
//容错判断
if(post<=0||post>lenght(p)+1)
{
printf("insert failed\n");
return -1;
}
//创建新节点保存要插入的数据
linklist_t* pnew=(linklist_t*)malloc(sizeof(linklist_t));
if(pnew==NULL)
{
printf("pnew malloc failed\n");
return -1;
}
pnew->data=x;
pnew->next=NULL;
//将头指针移动到插入位置的前一个位置上
int i=0;
for(i=0;i<post-1;i++)
p=p->next;
//将新节点连接到链表上 先连后面 再连前面
pnew->next=p->next;
p->next=pnew;
return 0;
}
//5. 判断链表是否为空,空返回1,未空返回0
int isEmpty(linklist_t* p)
{
//仅有有个头节点 头结点后面无节点
//p头节点的地址 p->next存放的下一个节点为NULL这链表为空
return p->next==NULL ? 1 : 0;
}
//6. 删除链表指定位置的数据
int delete(linklist_t* p,int post)
{
//容错判断
if(post<=0||post>lenght(p)||isEmpty(p))
{
printf("delete is failed\n");
return -1;
}
int i=0;
for(i=0;i<post-1;i++)
p=p->next;
linklist_t* pdel=p->next;
p->next=pdel->next;
free(pdel);
pdel=NULL;
return 0;
}
//7. 查找指定数据,出现在链表中的位置
int grap(linklist_t* p,int x)
{
int n=0;
while(p->next!=NULL)
{
n++;
p=p->next;
if(p->data==x)
{
printf("查找的数据位置为:%d\n",n);
break;
}
}
return n;
}
//8. 清空链表
int clean(linklist_t* p)
{
linklist_t* pdel=p->next;
while(p->next!=NULL)
{
p->next=pdel->next;
free(pdel);
pdel=NULL;
}
return 0;
}
int main(int argc, const char *argv[])
{
linklist_t* p=create();
if(p == NULL)
{
return -1;
}
insert(p,1,11);
show(p);
printf("leght is %d\n",lenght(p));
printf("isEmpty is %d\n",isEmpty(p));
delete(p,1);
show(p);
insert(p,1,11);
insert(p,2,22);
insert(p,3,33);
insert(p,4,44);
show(p);
grap(p,22);
clean(p);
show(p);
free(p);
return 0;
}
2、双向链表
2.1 插入操作
插入思想:
0. 容错判断
1. 创建新节点保存数据
2. 将头指针移到插入位置
3. 将新节点链到链上(连前面 再后面)
//先连前面
1.p->pri->next = pnew
2.pnew->pri = p->pri
//再连后面
3.pnew->next = p;
4.p->pri = pnew
2.2 删除操作
0. 容错判断
1. 将头指针移动删除位置
2. 进行删除
p-pri->next = p->next;
p->next->pri = p->pri;
free(p);
p = NULL;
2.3 双向循环链表
3、合并链表
递增有序的链表A 1 3 5 7 9 10
递增有序的链表B 2 4 5 8 11 15
新链表:1 2 3 4 5 5 7 8 9 10 11 15
将链表A和B合并,形成一个递增有序的新链表
代码实现如下:
#include <stdio.h>
#include <stdlib.h>
typedef struct node
{
int data; //数据域
struct node* next;//指针域 指向下一个节点的指针
}link_node_t;
int getLength(link_node_t* p);
//1.创建一个空的链表(本质上只有一个头节点链表,将头节点的首地址返回)
link_node_t* create()
{
//创建一个空的头节点
link_node_t* p = malloc(sizeof(link_node_t));
if(p == NULL)
{
printf("createEmptyLinklist malloc failed!!\n");
return NULL;//表达申请失败
}
//申请空间,就是为了装东西
p->next = NULL;//指针域初始化为NULL,数据域不用初始化,因为头节点数据域无效
//将头节点的首地址返回
return p;
}
//2.向链表的指定位置插入数据
int insert(link_node_t* p, int post, int x)
{
int i;
//0.容错判断
if(post < 1 || post > getLength(p)+1)
{
printf("insertIntoLinklist failed!!\n");
return -1;
}
//1.创建一个新的节点,保存插入的数据x
link_node_t* pnew = malloc(sizeof(link_node_t));
if(pnew == NULL)
{
printf("pnew malloc failed!!\n");
return -1;
}
//申请空间,就是为了装东西,立刻装东西
pnew->data = x;
pnew->next = NULL;
//准备开始插入
//2.将头指针指向(移动)插入位置的前一个位置
for(i = 0; i < post-1; i++)
p = p->next;
//3.将新节点插入链表(先连后面,再连前面)
pnew->next = p->next;//连后面
p->next = pnew;//连前面
return 0;
}
//3. 遍历有头单向链表
void show(link_node_t* p)
{
while(p->next != NULL)
{
p = p->next;
printf("%d ",p->data);
}
printf("\n");
}
//4. 求链表的长度
int getLength(link_node_t* p)
{
int len = 0;//统计长度
while(p->next != NULL)
{
p = p->next;
len++;//打印一次,就计数一次
}
return len;
}
//5. 清空链表
void clear(link_node_t* p)
{
//砍头思想:每次删除的是头节点的下一个节点
while(p->next != NULL)//只要链表不为空,就执行删除操作 p->next == NULL循环结束,此时是空的表
{
//1. 将头指针移动到删除位置的前一个位置
//第一步可以省略不写,因为每次删除的是头节点的下一个节点,
//那么头节点就是每次被删除节点的前一个位置
//2.定义了指针变量4个字节,指向被删除节点
link_node_t* pdel = p->next;
//3.跨过被删除节点
p->next = pdel->next;
//4. 释放被删除节点
free(pdel);
pdel = NULL;
}
}
void hebin(link_node_t* pa,link_node_t* pb)
{
//选择pa作为新表的头
link_node_t* ptail = pa;
pa = pa->next;
pb = pb->next;
//遍历两个无头表
while(pa != NULL && pb != NULL)
{
if(pa->data <= pb->data)//说明pa小
{
ptail->next = pa;
ptail = pa;
pa = pa->next;
}
else//pb小
{
ptail->next = pb;
ptail = pb;
pb = pb->next;
}
}
if(pa == NULL)//说明pb有剩余
{
ptail->next = pb;
}
else
{
ptail->next = pa;
}
}
int main(int argc, const char *argv[])
{
//创建链表a
link_node_t* pa = create();
//创建链表b
link_node_t* pb = create();
int a[6] = {1,3,5,7,9,10};
int b[6] = {2,4,5,8,11,15};
int i;
for(i = 0;i < 6;i++)
{
insert(pa,i+1,a[i]);
insert(pb,i+1,b[i]);
}
show(pa);
show(pb);
hebin(pa,pb);
show(pa);
clear(pa);
return 0;
}
五、栈c
1、什么是栈
是线性表中的一种;
限制在一端进行插入和删除操作的线性表称为栈,可以插入和删除这端称为栈顶,另一端称为栈底。
2、栈的特点
先进后出 后进先出
3、栈的实现方式
栈是解决问题的一种思想 先进后出
栈分为:顺序栈 和 链式栈
顺序栈:采用顺序表来实现 先进后出的思想 —》本质数组
链式栈:采用链表来实现 先进后出的思想
顺序栈和链栈最大的区别是什么???
存储结构不一样 顺序栈数据是连续 链式栈 数据在内存不连续
4、顺序栈的结构体定义
顺序栈结构定义
#define N 10
typedef struct
{
int data[N];
int top;//top顶 栈顶
5. 顺序栈操作
}stack_t;
top变量的作用:
1.时刻代表栈中有效元素的个数
2.标识栈顶的位置 插入(入栈)和删除(出栈)操作时用来当下标使用
5、顺序栈的操作
1.创建一个空的栈 === 创建一个空的顺序表 create()
2.判断栈是否为满 === 判断顺序表是否为满 isFull()
3.判断栈是否为空 === 判断顺序表是否为空 isEmpty()
4.入栈 === 在顺序表的尾巴上插入一个数据 有效元素个数+1 inStack()
5.出栈 === 删除顺序表的尾巴 尾巴里面的数据返回 有效元素个数-1 outStack()
6.获取栈顶元素 === 将顺序表的尾巴节点里面的数据域返回 getTopStack()
7.清空栈 === 清空顺序表 clear()
#include<stdio.h>
#include<stdlib.h>
#define N 10
//
typedef struct
{
int data[N];
int top;//栈顶
}stack_t;
//top的作用:1、时刻记录栈中有效元素的个数
//2、标识着栈顶的位置,插入删除操作时当下标使用
//创建一个空栈
stack_t* create()
{
stack_t*p=(stack_t*)malloc(sizeof(stack_t));
if(p==NULL)
{
printf("p malloc failed\n");
return NULL;
}
p->top=0;
return p;
}
//判断栈是否为满 满返回1 不满返回0
int isFull(stack_t* p)
{
return p->top==N ? 1 : 0;
}
//判断栈是否为空 空返回1 不空返回0
int isEmpty(stack_t* p)
{
return p->top==0 ? 1 : 0;
}
//插入元素
int insert(stack_t* p,int x)
{
if(isFull(p))
{
printf("栈已满\n");
return -1;
}
p->data[p->top]=x;
p->top++;
return 0;
}
//删除元素
int outstack(stack_t* p)
{
if(isEmpty(p))
{
printf("栈为空\n");
return -1;
}
p->top--;
return p->data[p->top];
}
//获取栈顶的元素
int getTopstack(stack_t* p)
{
if(isEmpty(p))
{
printf("这是个空栈\n");
return -1;
}
return p->data[p->top-1];
}
void clean(stack_t* p)
{
p->top=0;
}
void showBin(stack_t* p,int num)
{
while(num!=0)
{
insert(p,num%2);
num=num/2;
}
while(!isEmpty(p))
{
printf("%d",outstack(p));
}
printf("\n");
}
int main(int argc, const char *argv[])
{
stack_t* p=create();
if(p==NULL)
return -1;
printf("top:%d\n",getTopstack(p));
insert(p,11);
insert(p,22);
insert(p,33);
insert(p,44);
printf("%d\n",getTopstack(p));
insert(p,55);
insert(p,66);
insert(p,77);
insert(p,88);
insert(p,99);
insert(p,111);
printf("%d\n",getTopstack(p));
insert(p,2222);
printf("%d\n",getTopstack(p));
while(!isEmpty(p))
{
printf("%d ",outstack(p));
}
printf("\n");
clean(p);
printf("p->top:%d\n",p->top);
int num;
scanf("%d",&num);
showBin(p,num);
free(p);
return 0;
}
六、队列
1、什么是队列
限制在两端进行插入和删除操作的线性表,可以插入(入队列)这端称为队尾,可以删除(出队列)这端称为队头。
2、队列的特点
特点:先进先出 后进后出
F I F O L I L O
3、队列的实现方式
队列是解决问题的一种思想 先进先出
队列的实现方式:顺序队列(循环队列) 和链式队列
顺序队列(循环队列):采用顺序表来实现—》本质数组
链式队列:采用链表来实现
顺序队列和链队列最大的区别是什么???
存储结构不一样 一个连续 一个不连续
4、循环队列结构体定义
#define N 10
typedef struct
{
int data[N];//存数据的数据域 类型根据需求来
int front;//队头
int rear;//队尾
}queue_t;
front的作用:front指向的队列的第一个元素标识着队头的位置 出对队列时当下标使用的
rear的作用:rear指向队列最后一个元素的后一个位置空的(无效数据) 入队列时当下标使用的
//front 前
//rear 后
//queue 队列
5、循环队列的操作
//1. 创建一个空的队列
//2. 判断队列是否为空,空返回值是1,未空是0
//3. 判断队列是否为满 满返回值是1,未满是0
//4. 入队
//5. 出队
//6. 求队列的长度
#include<stdio.h>
#include<stdlib.h>
#define N 10
//循环队列结构体定义
typedef struct
{
int data[N];//存放数据的数据域
int front;//队头作用:指向队列的第一个元素标识着队头的位置,出队列时当下标使用
int rear;//队尾作用:指向队列最后一个元素的后一个位置空的,入队列时当下标使用
}queue_t;
//创建一个空队列
queue_t* create()
{
queue_t* p=(queue_t*)malloc(sizeof(queue_t));
if(p==NULL)
{
printf("p malloc failed\n");
return NULL;
}
p->front=0;
p->rear=0;
return p;
}
//判断队列是否为满
int isFull(queue_t* p)
{
return (p->rear+1)%N == p->front ? 1 : 0;
}
//判断队列是否为空
int isEmpty(queue_t* p)
{
return p->front==p->rear ? 1 : 0;
}
//插入队列元素
int inQueue(queue_t* p,int x)
{
//判满
if(isFull(p))
{
printf("这是个满队列\n");
return -1;
}
p->data[p->rear]=x;
p->rear++;
p->rear=p->rear%N;
//p->rear=(p->rear+1)%N //与前两行代码等价
return 0;
}
//删除队列元素
int outQueue(queue_t* p)
{
//判空
if(isEmpty(p))
{
printf("这是一个空队列\n");
return -1;
}
int temp=p->data[p->front];
p->front=(p->front+1)%N;
return temp;
}
//求队列的元素个数
int getLenght(queue_t* p)
{
if(p->rear >= p->front)
{
return p->rear - p->front;
}
else
{
return (p->rear+N) - p->front;
}
}
int main(int argc, const char *argv[])
{
queue_t* p=create();
if(p==NULL)
return -1;
printf("%d\n",isEmpty(p));
inQueue(p,11);
inQueue(p,22);
inQueue(p,33);
inQueue(p,44);
inQueue(p,55);
inQueue(p,66);
inQueue(p,77);
inQueue(p,88);
inQueue(p,99);
inQueue(p,1000);
printf("lenght is %d\n",getLenght(p));
//循环出队
while(!isEmpty(p))
{
printf("%d ",outQueue(p));
}
printf("\n");
free(p);
return 0;
}
6、循环队列的总结
1.循环队列的本质就是采用顺序表(数组)来实现的,循环队列中元素是在内存连续存储的
2.循环队列中,数组长度是N,那么队列的最大容量是N-1
3.循环队列中,循环队列的最大容量是N,那么数组长度是N+1
7、栈和队列的区别
1.栈是限制在一端进行插入和删除操作的线性表,它的特点先进后出 后进先出。
2.队列是限制在两端进行插入和删除操作的线性表,可以插入这端是队尾,可以删除这端是队头,它的特点是先进先出,后进后出。
栈和队列 限制了插入和删除的位置
七、时间复杂度
时间复杂度只不过是对算法运行时间和处理问题规模之间,关系的一种估算描述。
一个算法的时间复杂度越高,那么也就说明这个算法在处理问题的时候所花费的时间也就越长。
1、时间复杂度的排序
>时间复杂度优于
快-->慢
O(c) > O(log2 n) > O(n) > O(n*log2 n) > O(n^2) > O(n^3) > O(2^n)
2、时间复杂度的简化方法
1. 我们遵从如下“忽略”标准描述其时间复杂度:
1.忽略公式中的常数项
2.忽略公式中的低次幂项,仅保留公式中的最高次幂项
3.忽略公式中最高次幂项的常数系数
4.如果一个公式中所有的项都是常数项,那么这个算法的时间复杂度统一表示为O(1)
2. 下面我们来举几个相关的案例进行说明:
例1:算法1的运算过程用时公式是:2*n^2 + 5*n + 6,则这个算法的时间复杂度是O( n^2 )
例2:算法2的运算过程用时公式是:nlogn + 5n + 2,则这个算法的时间复杂度是O(nlogn)
例3:算法3的运算过程用时公式是:2n + 7,则这个算法的时间复杂度是O(n)
例4:元素交换算法只有3个步骤,其运算过程用时公式为:1+1+1,则其时间复杂度是O(1)
int a = 10;
int b = 20;
int temp = a;
a = b;
b = temp;
八、查找算法
1、顺序查找
顺序查找:从第一个元素开始挨个比较
//查找x,出现在数组的位置的下标
(1)main函数中定义一个数组 int a[8] = {12,34,2,11,32,123,1,2};
(2)定义一个函数,查找指定数据
(3)如果找到了,返回它在数组中的位置下标即可,未找到返回-1
(4)main函数中测试
#include <stdio.h>
#include <stdlib.h>
int findByOrder(int* p, int n, int x)
{
int i;
for(i = 0; i < n; i++)
{
if(p[i] == x)
return i;
}
return -1;
}
int main(int argc, const char *argv[])
{
int a[8] = {12,34,2,11,32,123,1,2};
int ret = findByOrder(a, 8, 11);
if(ret == -1)
{
printf("没有找到!!\n");
}
else
{
printf("%d的下标是%d\n",a[ret],ret);
}
return 0;
}
1.1 顺序查找总结
顺序查找的时间复杂是O(n)
缺点是:当数据量大的时候查找速度慢 因为是从头开始一个一个比较
优点是:对查找数据没有任何要求
2、二分查找
2.1 使用前提
前提:要求数必须是有序的(升序或降序)
2.2 算法思想
假设要查找的数据是x ,以升序为例。
要求出一个中间位置,让x与中间位置上的数据做比较。
如果x与中间位置上的数据相等,找到了返回middle
如果x大于中间位置上的数据,说明可能在右边,修改区间去掉左侧。
如果x小于中间位置上的数据,说明可能在左边,修改区间去掉右侧。
重复上面的操作,直到找到为止。
low //低位 左侧
high//高位 右侧
middle//中间位置
2.3 二分查找的时间复杂度
二分查找时间复杂度: O(log2 n)
||
二分查找时间复杂度: O(log n)
循环的次数 与 问题规模n的关系式子
问题规模 n
查找数据长度为n,每次查找后减半,
第一次 n/2 n/2^1
第二次 n/2/2 n/2^2
第三次 n/2/2/2 n/2^3
...
第k次 n/2^k
最坏的情况下第k次才找到,此时只剩一个数据,长度为1。
即 n/2^k = 1 // 2^k = n; 求k = log2 n
查找次数 k=logn
2.4 非递归实现二分查找
#include <stdio.h>
#include <stdlib.h>
int findByHalf(int* p, int n, int x)
{
int low = 0;
int high =n-1;
int middle;//用来保存中间位置下标
while(low <= high)//low > high循环结束
{
//1.得到中间位置的下标
middle = (low + high) / 2;
//2.x与中间位置的元素做比较,判断在middle位置的左边还是右边,来缩小范围
if(x == p[middle])
return middle; //找到了
else if(x > p[middle])//说明在右侧,需要移动low
low = middle + 1;
else //x < p[middle] //说明在左侧,需要移动high
high = middle - 1;
}
//上面的循环结束后,如果都没有执行return middle,说明没有找到
return -1;
}
int main(int argc, const char *argv[])
{
int a[8] = {11,22,33,40,55,60,70,89};
int num;
scanf("%d",&num);
int ret = findByHalf(a, 8, num);
if(ret == -1)
{
printf("没有找到!!\n");
}
else
{
printf("%d的下标是%d\n",a[ret],ret);
}
return 0;
}
2.5 递归实现二分查找
#include <stdio.h>
int findByHalf(int *p, int low, int high, int x)
{
if(low > high)//递归的结束条件
return -1;
//1.得到中间位置元素下标
int middle = (low + high) / 2;
//2.用x与中间位置的元素做比较
if(x == p[middle])
return middle;
else if(x < p[middle])//在左边,移动high
return findByHalf(p, low, middle-1, x);
else if(x > p[middle])//在右边,移动low
return findByHalf(p, middle+1, high, x);
}
int main(int argc, const char *argv[])
{
int a[6] = {11,22,33,44,55,66};
//将数组中的每个元素都查找一遍,测试程序
int i;
for(i = 0; i < 6; i++)
{
printf("%d的位置下标是%d\n",a[i], findByHalf(a,0,5,a[i]) );
}
int ret = findByHalf(a, 0, 5, 100);
if(ret == -1)
{
printf("not find!!!\n");
}
return 0;
}
3、分块查找
3.1 使用前提
块间有序 块内无序
3.2 算法思想
分块后,先去锁定在第几块,然后再到块内去查找。
3.3 分块查找时间复杂度
顺序查找的时间复杂度是:O(n)
二分查找的时间复杂度是: O(log2 n)
分块查找的时间复杂度是:O(log2 n) ----O(n)
不是一个确定的值,与分块多少,块内有多少元素都有关系的。
二分 》分块 》 顺序
3.4 分块查找的实现
#include <stdio.h>
typedef struct
{
int max;//每一块的最大值
int post;//每一块的起始下标
}index_t;
int findByBlock(int* a, int n1, index_t* b, int n2, int x)
{
int start,end;//用来保存块的起始和终止下标
//1.先确定x可能在哪一块中
int i;
for(i = 0; i < n2; i++)
{
if(x <= b[i].max)//说明可能在b[i]块中
break;
}
if(i == n2)//说明没有执行过break,说明x,比最后一块的最大值还要大
return -1;//没找到
//2.锁定这一块的起始和终止下标,去源数据表中进行顺序查找
start = b[i].post;
if(i == n2-1)//说明在最后一块,最后一块没有后一块
end = n1-1;//数组长度-1,就是最后一个有效元素下标
else
end = b[i+1].post - 1; //后一块的起始下标-1 得到前一块的终止下标
//取源数据表 在start --- end之间取查找
for(i = start; i <= end; i++)
{
if(x == a[i])
return i;
}
return -1;//没有找到
}
int main(int argc, const char *argv[])
{
//源数据表 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
int a[19] = { 18, 10, 9, 8, 21, 20, 38,42, 19, 50, 51, 72, 56, 55, 76,100, 90, 88, 108};
//索引表
index_t b[4] = {{18, 0},{42, 4},{72, 9},{108, 14}};
//将数组中的每个元素都查找一遍
int i;
for(i = 0; i < 19; i++)
{
printf("%d的位置下标%d\n",a[i], findByBlock(a, 19, b, 4, a[i]));
}
printf("%d的位置下标%d\n",66,findByBlock(a, 19, b, 4, 66));
return 0;
}
九、哈希查找
存储结构:顺序存储 链式存储 索引存储(分块查找) 散列存储(哈希查找)
散列存储:存数据的时候要按照某种对应关系存,取数据的时候也得按照对应关系取。
哈希查找时间复杂度是:O(1)
哈希函数:就是记录关键字与存储位置之间的关系
构建哈希函数的方法:
1.直接地址法
2.叠加法
3.数字分析法
4.平方取中法
5.保留余数法
1、直接地址法
保存全国各个年龄的人口数
输入年龄和人口数,按照对应关系保存到哈希表中,
输入要查询的年龄,打印输出该年龄的人口数
1.构建哈希表---》数组
int hashlist[130] = {0};
2.设计哈希函数---》记录关键字跟存储位置之间的关系
int hashFun(int age)
{
int post = age-1;
return post;
}
3.往哈希表中存数据--必须调用对应关系(hashFun)
4.从哈希表中读取数据---必须调用对应关系(hashFun)
#include <stdio.h>
//哈希函数: 保存的是关键字与存储位置的关系
int hashFun(int key)
{
int post = key - 1; //age - 1得到的是哈希表中的存储位置
return post;
}
//存数据
//int* p,用来保存哈希表的首地址
void saveAgeNum(int* p, int age, int num)
{
//存的时候按照对应关系存储
//1.调用哈希函数,得到哈希表中的存储位置
int post = hashFun(age);
//2.有了存储位置,将人口数,保存到哈希表中
p[post] = num;
}
//取数据
int getAgeNum(int* p, int age)
{
//取的时候按照对应关系取
//1.调用哈希函数,得到age对应的人口数哈希表中的存储位置
int post = hashFun(age);
//2.将人口数返回
return p[post];
}
int main(int argc, const char *argv[])
{
int hash_list[150] = { 0 };
int age,num;
int i;
for(i = 0; i < 5; i++)
{
printf("请输入年龄 和 对应的人口数:\n");
scanf("%d%d",&age,&num);
saveAgeNum(hash_list ,age, num);
}
for(i = 0; i < 3; i++)
{
printf("请输入您要查找的年龄:\n");
scanf("%d",&age);
printf("%d年龄的人口数是%d万个!!\n",age, getAgeNum(hash_list,age));
}
return 0;
}
2、叠加法
将图书馆的条形码叠加
int a[] = {321432543,432321543,654657345,234456300,213342345,123453333};
321432543
前三位 321 :321432543/1000000
中间三位 432 :321432543/1000%1000
后三位 543 :321432543%1000
----------
1296 % 1000---->296
//哈希表
int hashlist[1000] = {0};
//哈希函数
int hashFun(int key)
{
int post = (key/1000000 + key/1000%1000+key%1000)%1000;
return post;
}
#include <stdio.h>
typedef struct
{
int number;//用来保存图书条形码
char book_name[20];
}book_t;
int hashFun(int key)
{
int post = (key / 1000000 + key / 1000 % 1000 + key % 1000) % 1000;
return post;
}
//保存图书信息
//book_t* p //保存哈希表的首地址
//book_t* q //保存一本图书的首地址
void saveBookInfo(book_t* p, book_t* q)
{
//存的时候按照对应关系存
//1.调用哈希函数,得到存储位置
int post = hashFun(q->number);
p[post] = *q;//q里面保存的是main函数中结构体变量b的地址,所以*q代表的就是main函数中的b
}
//通过条形码,查找图书信息
//book_t* p //用来保存哈希表的首地址
book_t* getBookInfo(book_t* p ,int number)
{
//取的时候按照对应关系取
//1.调用哈希函数,得到存储位置
int post = hashFun(number);
//2.将图书信息返回
return &p[post];// return p+post;
}
int main(int argc, const char *argv[])
{
book_t b;
book_t* p = NULL;
int number;
book_t hash_list[1000];//元素类型是book_t因为保存的是图书信息
//图书条形码
//输入3本图书信息,将图书信息保存到哈希表中
int i;
for(i = 0; i < 3; i++)
{
printf("请输入条形码 和 图书编号:\n");
scanf("%d%s",&b.number, b.book_name);
saveBookInfo(hash_list, &b);
}
//输入3个条形码,查找图书的信息
for(i = 0; i < 3; i++)
{
printf("请输入条形码:\n");
scanf("%d",&number);
p = getBookInfo(hash_list, number);
printf("条形码:%d 书名:%s\n",p->number, p->book_name);
}
return 0;
}
3、数字分析法
//k1 k2 k3 k4 k5 k6
//2 3 1 5 8 6
//2 4 2 3 4 6
//2 3 3 7 9 6
//2 3 9 8 8 6
//2 4 5 7 8 6
//2 3 4 2 9 6
231586 --->15
242346 --->23
//通过数字分析,只有中间两位数重复的次数最少,所以将中间两个字作为关键字key
//哈希函数
int hashFun(int key)
{
int post = key/ 100 %100;
return post;
}
//哈希函数
int hashFun(int key)
{
int post = key % 10000 / 100;
return post;
}
//将数据存储
void saveNum(int *hash_list,int key)
{
int post = hashFun(key);
hash_list[post] = key;
}
//将数据取出
int getNum(int *hash_list,int key)
{
int post = hashFun(key);
return hash_list[post];
}
int main(int argc, const char *argv[])
{
//k1 k2 k3 k4 k5 k6
//2 3 1 5 8 6
//2 4 2 3 4 6
//2 3 3 7 9 6
//2 3 9 8 8 6
//2 4 5 7 8 6
//2 3 4 2 9 6
int i;
int a[] = {231586,242346,233796,239886,245786,234296};
//创建哈希表
int hash_list[100];//100因为只取中间两位所以post为两位数
//将数据存入哈希表
for(i = 0; i < sizeof(a)/sizeof(a[0]); i++)
saveNum(hash_list,a[i]);
//查找数据
for(i = 0; i < sizeof(a)/sizeof(a[0]); i++)
printf("post:%d --- %d\n",hashFun(a[i]),getNum(hash_list,a[i]));
return 0;
}
4、平方取中法
当取key中的某些值,不能是记录均匀分布时,根据数学原理,对key进行key的2次幂(取平方)
取key平方中的某些位可能会比较理想
key key的平方 H(key)
0100 00 100 00 100
0110 00 121 00 121
1010 10 201 00 201
1001 10 020 01 020
0111 00 123 21 123
#include <stdio.h>
// key key的平方 H(key)
// 0100 00 100 00 100
// 0110 00 121 00 121
// 1010 10 201 00 201
// 1001 10 020 01 020
// 0111 00 123 21 123
//对key平方后,发现中间的三位重复次数最少
//哈希函数
int hashFun(int key)
{
int post = key*key % 100000 / 100;
return post;
}
//存储数据
void saveNum(int *hash_list, int key)
{
int post = hashFun(key);
hash_list[post] = key;
}
//取数据
int getNum(int *hash_list,int key)
{
int post = hashFun(key);
return hash_list[post];
}
int main(int argc, const char *argv[])
{
int i;
int a[] = {100,110,1010,1001,111}; //key
int hash_list[1000] = { 0 };//哈希表
for(i = 0; i < sizeof(a)/sizeof(a[0]); i++)
saveNum(hash_list,a[i]);
for(i = 0; i < sizeof(a)/sizeof(a[0]); i++)
printf("post:%3d --- %d\n",hashFun(a[i]),getNum(hash_list,a[i]));
return 0;
}
5、保留余数法
保留余数法又称质数去余法:
设Hash表空间长度为m,选取一个不大于m的最大质数p,令:H(key)=key%p;
int a[11] = {12,23,4,24,2,4,23,1,24,45,23};
源数据表长是n = 11
哈希表长m = n/α ;α是装填因子 0.7---0.8之间最为合理
m = 11/0.75 = 15
int hashlist[15]={0};
int hashFun(int key)
{
int post = key % p;//p不超过哈希表长的最大质数 m = 15 p = 13
}
hashlist[0]---》 23
hashlist[1]---》 1
hashlist[2]---》 2
hashlist[3]---》
hashlist[4]---》 4
hashlist[5]---》 4
hashlist[6]---》 45
hashlist[7]---》
hashlist[8]---》
hashlist[9]---》
hashlist[10]---》 23
hashlist[11]---》 24
hashlist[12]---》 12
hashlist[13]---》 23
hashlist[14]---》 24
哈希冲突--》不同的关键字带到哈希函数中获得的是同一个位置
解决哈希冲突方法:
1.线性探查法 (开放地址法)
2.链地址法
十、哈希冲突解决
1、线性探查法
又称开放地址法
2、链地址法
十一、树
1、树的概念
树(Tree)是n(n≥0)个节点的有限集合T,它满足两个条件 :
有且仅有一个特定的称为根(Root)的节点;
其余的节点可以分为m(m≥0)个互不相交的有限集合T1、T2、……、Tm,其中每一个集合又是一棵树,并称为其根的子树(Subtree)。
树上的任意一个节点都可以有0--多个后继节点,但任意节点只能有一个前驱节点。根节点无前驱。叶子节点无后继。
后继节点 :往下找的 子节点
前驱节点 :往上找的 父节点
根节点 :树的根 最上面的节点
叶子节点:最下面的节点
1、树的特征
逻辑结构 逻辑关系 特点 应用
线性结构 线性关系 一对一 顺序表 链表 栈 队列
层次结构 树状关系 一对多 树(二叉树)
网状结构 图状关系 多对多
2、树的相关概念
(1)度数:一个节点的子树的个数 //节点生儿子的个数
(2)树度数:树中节点的最大度数 //生儿子最多的那个节点的度数,代表这颗树的度数
(3)叶节点或终端节点: 度数为零的节点 //没有儿子的都是叶子
(4)分支节点:度数不为零的节点 //除了叶子节点以外的都是分支节点
(5)内部节点:除根节点以外的分支节点 (去掉根和叶子)
//除了根节点 和 叶节点以外剩下的就是 内部节点
(6)节点层次: 根节点的层次为1,根节点子树的根为第2层,以此类推
(7)树的深度或高度: 树中所有节点层次的最大值
2、二叉树
2.1二叉树的概念
二叉树的定义 : 二叉树(Binary Tree)是n(n≥0)个节点的有限集合,它或者是空集(n=0),或者是由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。二叉树与普通有序树不同,二叉树严格区分左孩子和右孩子,即使只有一个子节点也要区分左右。
二叉树:节点度数最大为2,严格区分左右子。
2.2二叉树的特性
深度:层次最大值
(1)一颗深度为k的二叉树,在第k层上最多有( 2^(k-1) )个节点。
(2)一颗深度为k的二叉树,总共最多有( 2^k - 1 )个节点。
(3)任意一颗二叉树度数为0的节点个数,都比度数为2的节点个数多一个。
n0:代表度数为0的节点个数
n1:代表度数为1的节点个数
n2:代表度数为2的节点个数
n2 = n0 - 1
n0 = n2 + 1
2.3满二叉树与完全二叉树
(1)满二叉树: 深度为k(k>=1)时节点为2^k - 1(2的k次幂-1)//满二叉树上没有度数为1节点
(2)完全二叉树:只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置上。
//在满二叉树的最后一层,自右向左 连续 缺失若干个节点,并且保证最后一层剩余的节点都集中在左侧
2.4二叉树的存储结构
1、顺序存储
顺序存储结构: --->本质是数组
完全二叉树节点的编号方法是从上到下,从左到右,根节点为1号节点。设完全二叉树的节点数为n,
有n个节点的完全二叉树可以用有n+1 个元素的数组进行顺序存储,节点号和数组下标一一对应,下标为零的元素不用。利用以上特性,可以从下标获得节点的逻辑关系。不完全二叉树通过添加虚节点构成完全二叉树,然后用数组存储,这要浪费一些存储空间。
节点编号
若根节点编号 1
根节点左子节点编号: 2
根节点右子节点编号: 3
第n个节点
左子节点编号:2*n
右子节点编号:2*n+1
第n个节点的父节点编号 n/2
若根节点编号 0
根节点左子节点编号: 1
根节点右子节点编号: 2
第n个节点
左子节点编号:2*n+1
右子节点编号:2*n+2
第n个节点的父节点编号:(n-1)/2
2、链式存储结构
二叉树链式存储结构结构体定义
typedef struct node
{
char data;//数据域 存放当前节点的数据
struct node* lchild;//左指针域 存放当前节点的左子树的首地址
struct node* rchild;//右指针域 存放当前节点的右子树的首地址
}tree_t;
node //节点
data //数据
left //左
right //右
child //孩子
tree //树
大名:struct node
小名: tree_t
2.5二叉树的遍历
遍历:将二叉树上的所有节点都要访问一遍
pre_order 前序遍历:根 左 右 先访问根节点,再访问根节点左子树,最后访问根节点右子树。
in_order 中序遍历:左 根 右 先访问根节点的左子树,再访问根节点,最后访问根节点右子树。
post_order 后序遍历:左 右 根 先访问根节点的左子树,再访问根节点右子树,最后访问根节点。
#include <stdio.h>
#include <stdlib.h>
typedef struct node
{
char data;//数据域
struct node* lchild;//指向左子的指针
struct node* rchild;//指向右子的指针
}tree_node_t,*tree_list_t;
//前序遍历 根 左 右 root 根
void preorder(tree_node_t* r)// 等价于(tree_list_t r)
{
if(r == NULL)//递归的结束条件
return;
printf("%c ",r->data);//根
preorder(r->lchild); //左
preorder(r->rchild); //右
//&a //根节点的地址
//r->lchild //左子树节点的地址 &b
//r->rchild //右子树节点的地址 &c
}
//中序遍历 左 根 右
void inorder(tree_node_t* r)
{
if(r != NULL)
{
inorder(r->lchild); //左
printf("%c ", r->data);//根
inorder(r->rchild);//右
}
}
void postorder(tree_node_t* r)
{
if(r == NULL)
return;
postorder(r->lchild);
postorder(r->rchild);
printf("%c ",r->data);
}
//前序 中序 后序 同一类型的函数
// void (tree_node_t*)
//定义指向这个函数类型的指针
//第一个参数 void (*p)(tree_node_t*) 函数指针
//第二个参数 tree_node_t* r
//r根节点的首地址,给第一个参数服务的,第一个参数,通过函数指针调用函数的时候,需要一个 tree_node_t* 参数
void showBinTree(void (*p)(tree_node_t*), tree_node_t* r, char* s)
{
printf("%s",s);//打印的是提示
p(r);//通过函数指针调用函数
printf("\n");
}
int main(int argc, const char *argv[])
{
//定义6个二叉树节点
tree_node_t a = {'A', NULL, NULL};
tree_node_t b = {'B', NULL, NULL};
tree_node_t c = {'C', NULL, NULL};
tree_node_t d = {'D', NULL, NULL};
tree_node_t e = {'E', NULL, NULL};
tree_node_t f = {'F', NULL, NULL};
//形成二叉树
a.lchild = &b;
a.rchild = &c;
b.lchild = &d;
c.lchild = &e;
c.rchild = &f;
//遍历二叉树
printf("前序:");
preorder(&a);
printf("\n");
showBinTree(preorder, &a, "前序:");
printf("中序:");
inorder(&a);
printf("\n");
showBinTree(inorder, &a, "中序:");
printf("后序:");
postorder(&a);
printf("\n");
showBinTree(postorder, &a, "后序:");
return 0;
}
2.6 二叉树的操作
#include <stdio.h>
#include <stdlib.h>
typedef struct node
{
char data;//数据域
struct node* lchild;//指向左子的指针
struct node* rchild;//指向右子的指针
}tree_node_t,*tree_list_t;
//创建二叉树,返回值是根节点的首地址 AB##
tree_node_t* createTree()
{
char ch;
scanf("%c",&ch);//ch保存的是字符,即将存储在节点的数据域
if(ch == '#')//用输入'#'来代表当前的r没有左子或右子
return NULL;
tree_node_t* r = malloc(sizeof(tree_node_t));
if(r == NULL)
{
printf("createTree malloc failed!!\n");
return NULL;
}
//申请空间之后,装东西
r->data = ch;
r->lchild = createTree();//递归去创建左子树
r->rchild = createTree();//递归取创建右子树
return r;//将根节点的首地址返回值
}
//前序遍历 根 左 右 root 根
void preorder(tree_node_t* r)// 等价于(tree_list_t r)
{
if(r == NULL)//递归的结束条件
return;
printf("%c ",r->data);//根
preorder(r->lchild); //左
preorder(r->rchild); //右
//&a //根节点的地址
//r->lchild //左子树节点的地址 &b
//r->rchild //右子树节点的地址 &c
}
//中序遍历 左 根 右
void inorder(tree_node_t* r)
{
if(r != NULL)
{
inorder(r->lchild); //左
printf("%c ", r->data);//根
inorder(r->rchild);//右
}
}
void postorder(tree_node_t* r)
{
if(r == NULL)
return;
postorder(r->lchild);
postorder(r->rchild);
printf("%c ",r->data);
}
//前序 中序 后序 同一类型的函数
// void (tree_node_t*)
//定义指向这个函数类型的指针
//第一个参数 void (*p)(tree_node_t*) 函数指针
//第二个参数 tree_node_t* r
//r根节点的首地址,给第一个参数服务的,第一个参数,通过函数指针调用函数的时候,需要一个 tree_node_t* 参数
void showBinTree(void (*p)(tree_node_t*), tree_node_t* r, char* s)
{
printf("%s",s);//打印的是提示
p(r);//通过函数指针调用函数
printf("\n");
}
//删除二叉树
void clearTree(tree_node_t* r)
{
if(r == NULL)
return;
clearTree(r->lchild);
clearTree(r->rchild);
//printf("%c ",r->data); 打印多少次,就释放了多少次,先打印谁就是先释放哪个节点
free(r);
}
ABD###CE##F##
int main(int argc, const char *argv[])
{
#if 0
//定义6个二叉树节点
tree_node_t a = {'A', NULL, NULL};
tree_node_t b = {'B', NULL, NULL};
tree_node_t c = {'C', NULL, NULL};
tree_node_t d = {'D', NULL, NULL};
tree_node_t e = {'E', NULL, NULL};
tree_node_t f = {'F', NULL, NULL};
//形成二叉树
a.lchild = &b;
a.rchild = &c;
b.lchild = &d;
c.lchild = &e;
c.rchild = &f;
#endif
tree_node_t* r = createTree();
//遍历二叉树
printf("前序:");
preorder(r);
printf("\n");
showBinTree(preorder, r, "前序:");
printf("中序:");
inorder(r);
printf("\n");
showBinTree(inorder, r, "中序:");
printf("后序:");
postorder(r);
printf("\n");
showBinTree(postorder, r, "后序:");
clearTree(r);//清空树
return 0;
}
3、霍夫曼树
3.1概念
赫夫曼(Huffman)树,又称最优二叉树,是带权路径长度最短的树,有着广泛的应用。
从树中一个结点到另外一个结点的分支构成一条路径,分支的数目称为路径的长度。树的路径长度是指从树根
到每个结点的路径长度之和。
进一步推广,考虑带权的结点。结点的带权路径长度指的是从树根到该结点的路径长度和结点上权的乘积。树
的带权路径长度是指所有叶子节点的带权路径长度之和,记作 WPL 。WPL最小的二叉树就是最优二叉树,又
称为赫夫曼树。
霍夫曼树(Huffman Tree),又称最优二叉树,是一类带权路径长度最短的树。
typedef struct node
{
char data;//数据域 A
int w;//权值 weight
struct node* lchild;//left 左子的指针
struct node* rchild;//right 右子的指针
}bintree_t;
假设有n个权值{w1,w2,…,wn},如果构造一棵有n个叶子节点的二叉树,而这n个叶子节点的权值是
{w1,w2,…,wn},则所构造出的带权路径长度 最小 的二叉树就被称为霍夫曼树
树带权路径长度:
树的带权路径长度指树中所有叶子节点到根节点的路径长度与该叶子节点权值的乘积之和,
如果在一棵二叉树中共有n个叶子节点,用Wi表示第i个叶子节点的权值,Li表示第i个叶子
节点到根节点的路径长度,则该二叉树的带权路径长度
WPL=W1*L1 + W2*L2 + … Wn*Ln。
3.2 霍夫曼树的特征
(1)赫夫曼树的左右子树可以互换,因为这并不影响树的带权路径长度。
(2)带权值的节点都是叶子节点,不带权值的节点都是某棵子二叉树的根节点。
(3)权值越大的节点越靠近霍夫曼的根节点,权值越小的节点越远离霍夫曼的根节点。
(4)霍夫曼中只有叶子节点和度为2的节点,没有度为1的节点。
3.3 霍夫曼树的构建
(1)将给定的n个权值看做n棵只有根节点(无左右孩子)的二叉树,组成一个集合HT,每棵树的权值为该节点
的权值。 对四个二叉树,按照权值进行排序,排完序之后,前两个就是最小的
(2)从集合HT中选出2棵权值最小的二叉树,组成一棵新的二叉树,其权值为这2棵二叉树的权值之和。
(3)将步骤2中选出的2棵二叉树从集合HT中删去,同时将步骤2中新得到的二叉树加入到集合HT中。
(4)重复步骤2和步骤3,直到集合HT中只含一棵树,这棵树便是霍夫曼。
3.4 霍夫曼编码
赫夫曼树的应用十分广泛,比如众所周知的在通信电文中的应用。在等传送电文时,我们希望电文的总长尽可
能短,因此可以对每个字符设计长度不等的编码,让电文中出现较多的字符采用尽可能短的编码。
左0 右1
3.5 霍夫曼树的应用场景
霍夫曼树在数据压缩中的应用主要体现在霍夫曼编码(Huffman Coding)上。霍夫曼编码是一种基于
霍夫曼树的变长编码方法,能够根据数据中字符(或符号)的出现频率生成最优的编码方案,从而实现
高效的数据压缩。以下是霍夫曼树在数据压缩中的具体应用过程:
1、统计字符出现频率
在进行数据压缩之前,首先需要统计待压缩数据中每个字符的出现频率。频率越高,说明该字符在数据
中出现的次数越多。
示例:假设有一段文本数据 “abracadabra” ,统计每个字符的频率如下:
a :5次
b :2次
r :2次
c :1次
d :1次
2、构建霍夫曼树
根据字符的频率构造霍夫曼树。频率越高的字符,其对应的叶节点在霍夫曼树中越靠近根节点,路径长
度越短;频率越低的字符,其对应的叶节点越远离根节点,路径长度越长。
构造步骤:
\1. 将所有字符及其频率作为叶节点,放入一个优先队列(通常使用最小堆)。
\2. 每次从队列中取出两个频率最小的节点,合并为一个新的节点,新节点的频率为两个子节点频率之
和。
\3. 将新节点重新插入队列。
\4. 重复上述步骤,直到队列中只剩下一个节点,该节点即为霍夫曼树的根节点。
以 “abracadabra” 的字符频率为例,构造霍夫曼树的过程如下:
初始节点: a(5) , b(2) , r(2) , c(1) , d(1)
合并 c(1) 和 d(1) ,生成新节点 cd(2)合并 b(2) 和 cd(2) ,生成新节点 bcd(4)
合并 r(2) 和 bcd(4) ,生成新节点 rbcd(6)
合并 a(5) 和 rbcd(6) ,生成根节点 arbcd(11)
最终构造的霍夫曼树如下:
3、生成霍夫曼码
根据霍夫曼树的结构,为每个字符生成唯一的编码。从根节点到叶节点的路径上,向左走记为 0 ,向右
走记为 1 。每个叶节点对应的路径即为该字符的霍夫曼编码。
以构造的霍夫曼树为例:
a 的编码: 0
r 的编码: 10
b 的编码: 110
c 的编码: 1110
d 的编码: 1111
4、数据压缩
使用生成的霍夫曼编码对原始数据进行编码,将每个字符替换为其对应的霍夫曼编码。由于高频字符的
编码较短,低频字符的编码较长,因此整体编码长度会比原始数据的固定长度编码更短,从而实现数据
压缩。
示例:
原始数据: “abracadabra”
使用霍夫曼编码后:
a → 0
b → 110
r → 10
c → 1110
d → 1111
编码后的数据: 0 110 10 0 1110 0 1111 0 10
原始数据长度(假设每个字符占8位): 11 × 8 = 88 位
编码后数据长度: 1 + 3 + 2 + 1 + 4 + 1 + 4 + 1 + 2 = 19 位
可以看到,数据被显著压缩。
5、数据解压压缩
解压缩的过程是压缩过程的逆过程。使用霍夫曼树的结构,从编码数据中逐位读取编码,根据霍夫曼树
的路径找到对应的字符,从而恢复原始数据。
示例:
编码数据: 0 110 10 0 1110 0 1111 0 10
从根节点开始逐位读取:
0 → a
110 → b
10 → r
0 → a
1110 → c
0 → a
1111 → d
0 → a
10 → r
最终恢复的原始数据: “abracadabra”
6、优点与局限性
优点:
高效压缩:霍夫曼编码能够根据字符频率生成最优的编码方案,显著减少数据的存储空间和传
输时间。
无损压缩:压缩和解压缩过程完全可逆,不会丢失任何原始数据信息。
局限性:
需要统计频率:在压缩之前需要对数据进行频率统计,这会增加一定的计算开销。
对数据分布敏感:如果数据中字符频率分布不均匀,压缩效果较好;如果字符频率分布均匀,
则压缩效果不明显。
通过上述过程,霍夫曼树在数据压缩中发挥了重要作用,广泛应用于文件压缩工具(如 ZIP、RAR)、图
像压缩(如 JPEG)、音频压缩(如 MP3)等领域。
十二、排序
1、排序算法分类
2、算法的复杂度
3、排序相关概念
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
- 时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
- **空间复杂度:**是指算法在计算机
4、冒泡排序(Bubble Sort)
4.1算法描述
1、比较相邻的元素。如果第一个比第二个大,就交换它们两个;
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
3、针对所有的元素重复以上的步骤,除了最后一个;
4、重复步骤1~3,直到排序完成。
4.2 代码实现
void bubbleSort(int* p, int n)
{
int i,j;
for(i = 0; i < n-1; i++)
{
for(j = 0; j < n-1-i; j++)
{
if(p[j] >p[j+1])
{
int temp = p[j];
p[j] = p[j+1];
p[j+1] = temp;
}
}
}
}
5、选择排序(Selection Sort)
5.1 算法描述
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
初始状态:无序区为R[1..n],有序区为空;
第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
n-1趟结束,数组有序化了。
5.2 代码实现
void selectSort(int* p, int n)
{
int i,j;
//外循环找了几轮最小值,同时i代表每一轮选择的基准位置的下标
for(i = 0; i < n-1; i++)
{
for(j = i+1; j < n; j++)// j代表 基准位置后的元素 到 最后一个元素,每一个元素,与基准位置做比较
{
//p[i]代表的是基准位置元素
if(p[i] > p[j])
{
int temp = p[i];
p[i] = p[j];
p[j] = temp;
}
}
}
}
6、插入排序
6.1 算法描述
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有
1个元素,就是第一个元素。比较是从有序序列的末尾开 始,也就是想要插入的元素和已经有序的最大者开始
比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素
相 等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序
序列出去的顺序就是排好后的顺序,所以插入排序是稳定的。
1、从第一个元素开始,该元素可以认为已经被排序;
2、取出下一个元素,在已经排序的元素序列中从后向前扫描;
3、如果该元素(已排序)大于新元素,将该元素移到下一位置;
4、重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5、将新元素插入到该位置后;
6、重复步骤2~5。
6.2 代码实现
int a[6] = {3,'Q', 7, 'J', 5, 4};
//核心思想: 始终保持自己手里的牌是一个有序序列
3,'Q', 7, 'J', 5, 4
3
3 Q
3 7 Q
3 7 J Q
3 5 7 J Q
3 4 5 7 J Q
int i,j;
//for循环变量i,代表的是每次抓牌的下标
for(i = 1; i < n; i++) //i从1开始,默认手里有一张牌a[0],就是一个有序序列
{
for(j = i; j > 0; j--)
{
if(a[j] < a[j-1])
{
int temp = a[j];
a[j] = a[j-1];
a[j-1] = temp;
}
else //思想的精髓在break
break;
}
}
3
//抓牌a[1] , a[1]放在手里牌的最后,从后向前两两相比较
3 'Q'
//抓牌a[2] , a[2]放在手里牌的最后,从后向前两两相比较
0 1 2
3 7 'Q'
//抓牌a[3] , a[3]放在手里牌的最后,从后向前两两相比较
0 1 2 3
3 7 'J' 'Q'
//抓牌a[4] , a[4]放在手里牌的最后,从后向前两两相比较
0 1 2 3 4
3 5 7 'J' 'Q'
void insertSort(int* p, int n)
{
int i,j;
for(i = 1; i < n; i++) //i从1开始,默认手里有一张牌a[0],就是一个有序序列
{
for(j = i; j > 0; j--)
{
if(p[j] < p[j-1])
{
int temp = p[j];
p[j] = p[j-1];
p[j-1] = temp;
}
else //思想的精髓在break,抓到的牌比手中的最后一颗牌大,没必要继续向前两两相比较
break;
}
}
}
7、快速排序
7.1 快排的思想(面试中的回答)
请你介绍快排的编程思想:
1. 大概说
快速排序使用递归,先找枢轴所在位置,实现枢轴左侧的数都小于枢轴的值,枢轴右侧的所有数都
大于枢轴的值。 然后利用递归,将枢轴左侧和枢轴右侧,再进行快速排序。
2. 再细说
(1)选取区间左侧第一个数作为镖旗flag = p[low]
(2)进行从右向左扫描,只要右边的数大于flag p[high]>=flag,执行high--,不需要赋值到
左边。
(3)直到遇到一个比flag小的数,需要将其赋值到左边 p[low] = p[high]
(4)移动之后,刚过来的数不用再比较,执行low++
(5)进行从左向右扫描,只有左边的数小于flag p[low] <= flag ,执行low++,不需要赋值到
右边。
(6)直到遇到一个比flag大的数,需要将其赋值到右边 p[high]= p[low]
(7)移动之后,刚过来的数不用再比较,执行high--
重复上面的操作,直到low == high的时候,结束循环。找到了枢轴所在位置。
将镖旗归位 p[low] = flag
(8)递归,对枢轴左右两侧进行同样的方式进行快速排序。
7.2 快排的实现
void quick(int * p,int low,int high)
{
//递归结束条件low < high是正常区间
if(low >= high)
{
return;
}
//1.选区间左侧第一个元素当镖旗
//找枢轴所在位置 将镖旗放到该位置能达到左边的数均小于镖旗 右边的数均大于镖旗
int i = low;//记录原来的low
int j = high;//记录原来的high
int flag = p[low];
while(low < high)//low == high 既是枢轴所在位置
{
//从右开始比较
while(p[high]>=flag && low < high)//只要p[high]大于flag就应该在右边 只需要执行high--遇到p[high]<flag循环结束了
high--;
if(low < high)
{
p[low] = p[high];//将小于flag的数赋值到左边
low++;//刚移动过来的数不需要再比较
}
//从左开始比较
while(p[low] <= flag && low < high)//只要p[low]小于flag就应该在左边只需要执行low++ 当遇到p[low]>flag循环结束了
low++;
if(low < high)
{
p[high] = p[low];
high--;
}
}
//上面循环结束时 low==high 找到了枢轴位置
//把镖旗归位
p[low] = flag;
printf("枢轴位置为:%d\n",low);
//将枢轴左侧 和 枢轴右侧 看成新的待排序列 再次调用函数进行排序
//枢轴左侧
quick(p,i,low-1);
//枢轴右侧
quick(p,low+1,j);
}
8、总结
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int* create(int n)
{
int* p = (int*)malloc(n*sizeof(int));
if(p == NULL)
{
printf("p malloc failed\n");
return NULL;
}
return p;//返回首地址
}
void set(int* p,int n)
{
//种随机种子
srand(time(NULL));
int i;
for(i = 0;i < n;i++)
{
p[i] = rand()%n+1;//1---n
}
}
void show(int* p,int n)
{
int i;
for(i = 0;i < n;i++)
{
printf("%d ",p[i]);
}
printf("\n");
}
//冒泡排序的时间复杂度是O(n^2)
void sort(int* p,int n)
{
int i,j;
for(i = 0;i < n-1;i++)
{
for(j = 0;j < n-1-i;j++)
{
if(p[j] > p[j+1])
{
int temp = p[j];
p[j] = p[j+1];
p[j+1] = temp;
}
}
}
}
//选择排序 时间复杂度O(n^2)
void selectSort(int* p,int n)
{
int i,j;
//外循环i代表每一轮的基准位置
for(i = 0;i < n-1;i++)
{
for(j = i+1;j < n;j++)
{
//都要与基准位上的数据做比较 小就换
if(p[j] < p[i])
{
int temp = p[j];
p[j] = p[i];
p[i] = temp;
}
}
}
}
//插入排序 时间复杂度O(n^2)
void insertSort(int* a,int n)
{
int i,j;
//for循环变量i,代表的是每次抓牌的下标
for(i = 1; i < n; i++) //i从1开始,默认手里有一张牌a[0],就是一个有序序列
{
for(j = i; j > 0; j--)
{
if(a[j] < a[j-1])
{
int temp = a[j];
a[j] = a[j-1];
a[j-1] = temp;
}
else //思想的精髓在break
break;
}
}
}
//快速排序low = 0 high=7
void quick(int * p,int low,int high)
{
//递归结束条件low < high是正常区间
if(low >= high)
{
return;
}
//1.选区间左侧第一个元素当镖旗
//找枢轴所在位置 将镖旗放到该位置能达到左边的数均小于镖旗 右边的数均大于镖旗
int i = low;//记录原来的low
int j = high;//记录原来的high
int flag = p[low];
while(low < high)//low == high 既是枢轴所在位置
{
//从右开始比较
while(p[high]>=flag && low < high)//只要p[high]大于flag就应该在右边 只需要执行high--遇到p[high]<flag循环结束了
high--;
if(low < high)
{
p[low] = p[high];//将小于flag的数赋值到左边
low++;//刚移动过来的数不需要再比较
}
//从左开始比较
while(p[low] <= flag && low < high)//只要p[low]小于flag就应该在左边只需要执行low++ 当遇到p[low]>flag循环结束了
low++;
if(low < high)
{
p[high] = p[low];
high--;
}
}
//上面循环结束时 low==high 找到了枢轴位置
//把镖旗归位
p[low] = flag;
printf("枢轴位置为:%d\n",low);
//将枢轴左侧 和 枢轴右侧 看成新的待排序列 再次调用函数进行排序
//枢轴左侧
quick(p,i,low-1);
//枢轴右侧
quick(p,low+1,j);
}
int main(int argc, const char *argv[])
{
int n;
scanf("%d",&n);
int* p = create(n);//去堆区开辟n个连续的内存空间
if(p == NULL)
return -1;
set(p,n);//为连续内存空间赋值
show(p,n);//打印所有值
//sort(p,n);//排序
//selectSort(p,n);
//insertSort(p,n);
quick(p,0,n-1);
show(p,n);//打印
free(p);//释放内存
return 0;
}