复习数据结构
顺序表:
- 数组:
缺点:
- 中间或者头部插入删除数据要挪动数据,效率低
- 空间不够,要扩容
- 按倍数扩容,存在空间浪费
优点: - 下标随机访问,排序,二分查找方便
- CPU高速缓存命中率高,缓存利用率高
- 链表
优点:
- 任意位置插入删除效率高
- 按需申请释放,不存在扩容
缺点: - 不能下标随机访问
- CPU高速缓存命中率低
- 栈和队列
底层还是数组或者链表
先进先出,后进先出
注意:
- 链表和顺序表是互补的数据结构
- 顺序表是一种抽象的数据结构,他可以用数组,链表等其他数 据结构来实现
- 数组是一种数据结构,在内存中以连续的方式存储相同类型元素,数组可以是一维或多维,具有随机访问和快速访问,适合顺序表
- 可以说数组是顺序表的一种实现方式
如:数组实现:顺序表,哈希表,堆,B树
1. 树的概念(族谱)及结构
二叉树,多叉树,结构为树形,更复杂
1.1 节点的度
一个结点含有的子树的个数称为结点的度,如上图:A的为6
1.2 叶节点或终端结点(重要)
度为0的结点称为叶结点;如上图:B,C,H,I等结点为叶结点
1.3 非终端结点或分支结点(重要)
度不为0的结点;如上图:D,E,F,G等结点为分支节点
1.4 双亲结点或父结点(重要)
若一个结点含有子结点,则这个节点称为其子结点的父结点;如上图:A是B的父节点
1.5 孩子结点或子结点(重要)
一个结点含有的子树的跟结点称为该结点的子结点;如上图:B是A的孩子结点
1.6 兄弟结点
具有相同父结点的结点称为兄弟结点;如上图:B,C是兄弟结点
1.7 树的度
一棵树中,最大的结点的度称为树的度;如上图:树的度为6
1.8 结点的层次(重要)
从跟开始定义起,跟为第一层,跟的子结点为第二层,以此类推
1.9 树的高度或深度
树中结点的最大层次,如上图:树的高度为4
1.10 堂兄弟结点
双亲在同一层的结点互为堂兄弟,如上图,H,I互为堂兄弟结点
1.11 结点的祖先
从跟到该结点所经分支上的所有结点,如上图,A是所有结点的祖先
1.12 子孙
以某结点为根的子树中任一结点都称为该节点的子孙,如上图,所有节点都是A的子孙
1.13 森林
由m(m>0)棵互不相交的树的集合称为森林;
树与非树:
- 子树是不相交的
- 除了根结点外,每个结点有且只有一个父结点(树没有相交的结点)
如果出现相交的,是图 - 一颗N个结点的树有N-1条边
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它的根朝上,而叶朝下。
- 有一个特殊的结点,称为根结点,根节点没有前驱结点
- 除根结点外,其余结点被分成M(M>0)个不相交的集合T1,T2…,Tm,其中每一个集合Ti(1<=i<=m)又是一颗结构与树类似的子树。每棵树的根节点有且只有一个前驱,可以有0个或多个后继
- 因此,树是递归定义的
树的存储方式
树结构相对线性表来说就较为复杂,要储存表示起来就较为麻烦,既要保存值域,又要保存结点与结点之间的关系,实际中树有很多种表示方式:双亲表示法,孩子表示法,孩子双亲表示法以及孩子兄弟表示法等
struct TreeNode
{
int val;
struct TreeNode*child1;
struct TreeNode*child2;
struct TreeNode*child3;
//......
//不确定有几个子节点
}
#define N 3
struct TreeNode
{
int val;
struct TreeNode*childArr[N];
} //顺序表存储孩子指针
1. 左孩子右兄弟(孩子兄弟表示法)
struct TreeNode
{
int val;
struct TreeNode*firstchild;
struct TreeNode*nextbrother;
}
这种结构,只需要遍历类似链表的结构就可以找出一个结点的所有子节点
2. 双亲表示法
双亲表示法,只存储双亲的下标或指针,一般是数组实现
两个结点有没有在同一棵树上,只需要找他们的根是否相同(跟无父亲,用-1表示)
3. 二叉树概念及结构
3.1概念
一棵二叉树是结点的一个有限集合,该集合:
- 或者为空
- 由一个根节点加上两棵分别称为左子树和右子树的二叉树组成
- 二叉树不存在度大于2的结点,最多两个孩子,可以为1或者是0
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序数
二叉树可以出现以下情况:
3.2 特殊二叉树
1.满二叉树
一个二叉树,如果每一层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为k,切结点总数是2^k-1,则为满二叉树
2.完全二叉树
完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为k的,有n个结点的二叉树,当且仅当每一个结点都与深度为k的满二叉树中编号从1到n的结点一一对应时,称之为完全二叉树,
满二叉树是一种特殊的完全二叉树
- 满二叉树:
- 每一层都是2^(i-1)个结点 每一层都是满的
高度为h的满二叉树有多少个结点:
F(h)=20+21+22+…………2(h-2)+2^(h-1);
F(h)=2^h-1;(等比求和,错位相减)
假设这棵满二叉树的结点是N个
N=2^h-1;
h=log2(N+1)
如果N是10亿,h约等于30
- 完全二叉树:
- 假设他的高度是h,前h-1层是满的,最后一层不一定满,从左到右是连续的
节点范围[2(h-1),2h-1]
4. 堆的实现
1.逻辑实现
任意位置通过下标可以找到父亲和孩子
如果是非完全二叉树
满二叉树和完全二叉树适合用数组储存
适合这种结构–堆
栈:线性表,后进先出
堆:非线性结构,二叉树(完全二叉树),和内存的堆不一样,适 合用数组存储
小堆:树中的任意一个父亲(值)都小于等于孩子
大堆:树中的任意一个父亲(值)都大于等于孩子
底层:物理结构,数组
逻辑结构,完全二叉树
小堆,底层数组是否升序?(不一定)
小堆的根是整棵树的最小值(topk,堆排序)->O(N*logN)
堆只分大堆和小堆
堆只是想象出来的结构,实际上的储存还是按照数组的方式储存,不管是插入还是删除数据,都是在数组上面修改
1. 不管是大数字还是小数字,先要将其插入数组,数组尾插方便就先尾插
2. 尾插后,这个数字的值不会对其他分支的结点产生影响,只会与自己的父亲结点相比
3. 可以使用parent=(leftchild-1)/2或者(rightchild-2)/2来算出父结点的下标
50下标为6,(6-2)/2=2,也就是父结点,然后进行数据交换
如果小堆插入的数字小于根结点,则需要一直替换直到根结点最小
堆的代码实现
int main()
{
int a[] = { 65,100,70,32,50,60 };
HP hp;
HeapInit(&hp);
for (size_t i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
HeapPush(&hp, a[i]);
}
HeapPrint(&hp);
HeapDestory(&hp);
return 0;
}
3.1堆的结构体
和创建顺序表类似,先有个动态数组
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int HPdatatype;
typedef struct Heap
{
HPdatatype* a;
int size;
int capacity;
}HP;
3.2.初始化堆
void HeapInit(HP*php)
{
assert(php);
php->a=NULL;
php->size=0;
php->capacity=0;
}
3.3 销毁堆
void HeapDestory(HP*php)
{
free(php->a);
php->a=NULL;
php->size=0;
php->capacity=0;
}
3.4 入堆
void HeapPush(HP*php,HPdatatype x)
{
assert(php);
if(php->size==php->capacity)
{
int newCapacity=(php->capacity==0?4:2*php->capacity);
HPdatatype*tmp=(HDdatatype*)realloc(php->a,newCapacity*sizeof(HPdatatype));
if(tmp==NULL)
{
perror("realloc falied");
exit(-1);
}
php->a=tmp;
php->capacity=newCapacity;
}
php->a[php->size++]=x;
AdjustUp(php->a,php->size-1);
}
3.向上调整
向上调整,前提:前面的数据是大堆/小堆
void Swap(HPdatatype*p1,HPdatatype*p2)
{
HPdatatype tmp=*p1;
*p1=*p2;
*p2=tmp;
}
void AdjustUp(HPdatatype*a,int child)
{
int parent=(child-1)/2;
while(child>0)
{
if(a[child)<a[parent])
{
Swap(&a[child],&a[parent]);
child=parent;
parent=(parent-1)/2;
}
else
{
break;
}
}
}
3.6输出
void HeapPrint(HP*php)
{
assert(php);
for(size_t i=0;i<php->size;i++)
{
printf("%d ",php-a[i]);
}
printf("\n");
}
3.7删除数据
删除根结点
1.数据向前覆盖(failed)
上面的数据是小堆,如果采用覆盖的方法,覆盖后的不能构成堆(要么大堆,要么小堆)
- 首尾交换
根和最后一个值交换,然后再删除
这种方法是向下调整,前提:左右子树是大堆/小堆
然后将转化后的根,与两个子结点比较,和小的结点互换,直到无结点可换,或者两个子结点都大于该结点(小堆)
void HeapPop(HP*php)
{
assert(php);
assert(php->size>0);
swap(&php->a[0],&php->a[php->size-1]);
--php->size;
Adjustdown(php->a,php->size,0);
}
时间复杂度:O(logN)(也就是树的层数)
同时,通过这个方法,可以找到第二小的元素(大于删除的根)
加入输出根的这段代码,就可以得到排序号的一串树
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));
HeapPop(&hp);
}
3.8向下调整位置
void Adjustdown(HPdatatype*a,int n,int parent)
{
int child=parent*2+1;
while(child<n)
{
if(child+1<n&&a[child+1]<a[child])
//child+1<n保证不越界
{
child++;
}
if(a[child]<a[parent])
{
swap(&a[child],&a[parent】);
parent=child;
child=parent*2+1;
}
else
{
break;
}
}
}
3.9返回根的值
HPdatatype HeapTop(HP* php)
{
assert(php);
assert(php->size>0);
return php->a[0];
}
3.10判断是否为空
bool HeapEmpty(HP*php)
{
assert(php);
return php->size==0;
}
3.11改变向上和向下调整的符号
void AdjustUp(HPdatatype* a,int child )
{
int parent = (child - 1) / 2;
while(child>0)
{
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
child = parent;
parent = (parent - 1) / 2;
}
else
{
break;
}
}
}
void Adjustdown(HPdatatype* a,int n,int parent)
{
int child = parent * 2 + 1;
while (child<n)
{
if (child+1<n&&a[child + 1] < a[child])
{
++child;
}
if (a[child] > a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
我们就可以得到一个由大到小的排列
第一行为大堆
第二行为不断根删的结果
3.12建堆
通过上面建堆,不断删除根节点,从而排出数组的顺序
void HeapSort(int *a,int n)
{
int a[] = { 60,100,70,32,50,65 };
HP hp;
HeapInit(&hp);
for (size_t i = 0; i < sizeof(a) / sizeof(a[0]); i++)
{
HeapPush(&hp, a[i]);
}
HeapPrint(&hp);
int i=0;
while (!HeapEmpty(&hp))
{
printf("%d ", HeapTop(&hp));
a[i++]=HeapTop(&hp);
HeapPop(&hp);
}
HeapDestory(&hp);
}
方法缺点:
- 先有个堆的数据结构
- 空间复杂度的消耗大
建堆输入数组的值太麻烦,干脆直接对修改输入的数组
向上建堆处理
向上调整和向下调整只能是,下方已经是堆的情况,所以对于下方不是堆的不能直接使用向上和向下调整
直接向上处理不能将数组转换成堆
原数组不是堆,我们可以通过变换,将原数组转化成堆,虽然不能直接使用,但是我们可以借助这个思想
我们可以先把70单独看做一个堆,然后通过数列后面的元素不断与根元素替换
void HeapSort(int*a,
{
for(int i=0;i<n;i++)
{
AdjustUp(a,i);
}
}
3.14堆排序
堆排序是一种选择排序
升序;建大堆
堆顶跟最后一个位置交换,最大的数据排好了,剩下的数据向下调整,选出次大的,代价是logN
向下调整需要下方为大堆
先找到非叶子结点的最后一个结点,然后与下方最大的子比较交换,将下方转换为大堆,此时上方节点可以通过向下调整转换大堆
void Adjustdown(HPdatatype* a,int n,int parent)
{
int child = parent * 2 + 1;
while (child<n)
{
if (child+1<n&&a[child + 1] < a[child])
{
++child;
}
if (a[child] < a[parent])
{
swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
//向下调整建堆
//O(N)
for (int i = (n - 1 - 1)/2; i >= 0; i--)
{
Adjustdown(a, n, i);
}
int end = n - 1;
//时间复杂度N*(logN)
while (end > 0)
{
swap(&a[0], &a[end]);
Adjustdown(a, end, 0);
--end;
}
}
降序:建小堆
void HeapSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
AdjustUp(a, i);
}
int end = n - 1;
while (end > 0)
{
swap(&a[0], &a[end]);
Adjustdown(a, end, 0);
--end;
}
}
4. topk问题
假设有10亿个数据,要找到最大的前k个
k=10;
1. 读取文件前10个数据,在内存数组中建立一个小堆
2. 在依次读取剩下的数据,跟堆顶进行比较,大于堆顶,就替换他进堆,向下调整
3. 所有数据读完,堆里面数据就是最大的前10个
创建一个含有10000个随机数的文件
void DataCreate()
{
int n = 10000;
FILE* fin = fopen("data.txt", "w");
//以写入打开文件
if (fin == NULL)//判断文件是否打开成功
{
perror("fopen failed");
return;
}
srand(time(0));
for (int i = 0; i < n; i++)
{
int x = (rand() + i) % 10000000;
fprintf(fin, "%d\n", x);//写入文件
}
fclose(fin);
}
先创建一个k空间的内存,然后将10000个数据中的k个数据传入,组成一个小堆
然后后续进来的值都与根相比,直到所有的数据都遍历过
void Testtopk(int k)
{
int* a = (int*)malloc(k * sizeof(int));
FILE* fout = fopen("data.txt", "r");
if (fout == NULL)
{
perror("fopen failed");
exit(-1);
}//打开文件
int n = 0;
for (int i = 0; i < k; i++)
{
fscanf(fout, "%d", &a[i]);
}
//前k个数建小堆
for (int i = (k - 2) / 2; i > 0; i--)
{
AdjustDown(a, k, i);
}
int x = 0;
//遍历所有数据
while (fscanf(fout, "%d", &x) != EOF)
{
if (x > a[0])
{
a[0] = x;
AdjustDown(a, k, 0);
}
}
//输出堆的值
for (int i = 0; i < k; i++)
{
printf("%d ", a[i]);
}
printf("\n");
fclose(fout);
}