那么如何才能正确的掌握Redis呢?
为了让大家能够在Redis上能够加深,所以这次给大家准备了一些Redis的学习资料,还有一些大厂的面试题,包括以下这些面试题
-
并发编程面试题汇总
-
JVM面试题汇总
-
Netty常被问到的那些面试题汇总
-
Tomcat面试题整理汇总
-
Mysql面试题汇总
-
Spring源码深度解析
-
Mybatis常见面试题汇总
-
Nginx那些面试题汇总
-
Zookeeper面试题汇总
-
RabbitMQ常见面试题汇总
JVM常频面试:
Mysql面试题汇总(一)
Mysql面试题汇总(二)
Redis常见面试题汇总(300+题)
}
else//已成堆
{
break;
}
}
}
接下来我们对堆进行比较全面的实现
3.2.5堆的实现
typedef int HPDataType;
typedef struct Heap
{
HPDataType* a;
int size;
int capacity;
}Hp;
// 堆的构建
void HeapInit(Hp* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Hp* hp);
// 堆的插入
void HeapPush(Hp* hp, HPDataType x);
// 堆的删除
void HeapPop(Hp* hp);
// 取堆顶的数据
HPDataType HeapTop(Hp* hp);
// 堆的数据个数
int HeapSize(Hp* hp);
// 堆的判空
int HeapEmpty(Hp* hp);
初始化堆
首先,必须创建一个堆类型,该类型中需包含堆的基本信息:存储数据的数组、堆中元素的个数以及当前堆的最大容量。
typedef int HPDataType;//堆中存储数据的类型
typedef struct Heap
{
HPDataType* a;//用于存储数据的数组
int size;//记录堆中已有元素个数
int capacity;//记录堆的容量
}HP;
然后我们需要一个初始化函数,对刚创建的堆进行初始化,并将传入数据实现建堆操作。
//初始化堆
void HeapInit(HP* php, HPDataType* a, int n)
{
assert(php);
HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType)*n);//申请一个堆结构
if (tmp == NULL)
{
printf(“malloc fail\n”);
exit(-1);
}
php->a = tmp;
memcpy(php->a, a, sizeof(HPDataType)n);//拷贝数据到堆中–memcpy满足任意类型的拷贝(因为接收f的是void)
php->size = n;
php->capacity = n;
int i = 0;
//建堆
for (i = (php->size - 1 - 1) / 2; i >= 0; i–)
{
AdjustDown(php->a, php->size, i);
}
}
销毁堆
为了避免内存泄漏,使用完动态开辟的内存空间后都要及时释放该空间,所以,一个用于释放内存空间的函数是必不可少的。
//销毁堆
void HeapDestroy(HP* php)
{
assert(php);
free(php->a);//释放动态开辟的数组
php->a = NULL;//及时置空
php->size = 0;//元素个数置0
php->capacity = 0;//容量置0
}
打印堆
打印堆中的数据,这里用了两种打印格式。第一种打印格式是按照堆的物理结构进行打印,即打印为一排连续的数字。第二种打印格式是按照堆的逻辑结构进行打印,即打印成树形结构。
//求结点数为n的二叉树的深度
int depth(int n)
{
assert(n >= 0);
if (n>0)
{
int m = 2;
int hight = 1;
while (m < n + 1)
{
m *= 2;
hight++;
}
return hight;
}
else
{
return 0;
}
}
//打印堆
void HeapPrint(HP* php)
{
assert(php);
//按照物理结构进行打印
int i = 0;
for (i = 0; i < php->size; i++)
{
printf("%d ", php->a[i]);
}
printf(“\n”);
//按照树形结构进行打印
int h = depth(php->size);
int N = (int)pow(2, h) - 1;//与该二叉树深度相同的满二叉树的结点总数
int space = N - 1;//记录每一行前面的空格数
int row = 1;//当前打印的行数
int pos = 0;//待打印数据的下标
while (1)
{
//打印前面的空格
int i = 0;
for (i = 0; i < space; i++)
{
printf(" ");
}
//打印数据和间距
int count = (int)pow(2, row - 1);//每一行的数字个数
while (count–)//打印一行
{
printf(“%02d”, php->a[pos++]);//打印数据
if (pos >= php->size)//数据打印完毕
{
printf(“\n”);
return;
}
int distance = (space + 1) * 2;//两个数之间的空格数
while (distance–)//打印两个数之间的空格
{
printf(" ");
}
}
printf(“\n”);
row++;
space = space / 2 - 1;
}
}
堆的插入
数据插入时是插入到数组的末尾,即树形结构的最后一层的最后一个结点,所以插入数据后我们需要运用堆的向上调整算法对堆进行调整,使其在插入数据后仍然保持堆的结构。
//堆的插入
void HeapPush(HP* php, HPDataType x)
{
assert(php);
if (php->size == php->capacity)
{
HPDataType* tmp = (HPDataType*)realloc(php->a, 2 * php->capacity*sizeof(HPDataType));
if (tmp == NULL)
{
printf(“realloc fail\n”);
exit(-1);
}
php->a = tmp;
php->capacity *= 2;
}
php->a[php->size] = x;
php->size++;
//向上调整
AdjustUp(php->a, php->size - 1);
}
堆的删除
堆的删除,删除的是堆顶的元素,但是这个删除过程可并不是直接删除堆顶的数据,而是先将堆顶的数据与最后一个结点的位置交换,然后再删除最后一个结点,再对堆进行一次向下调整。
原因:我们若是直接删除堆顶的数据,那么原堆后面数据的父子关系就全部打乱了,需要全体重新建堆,时间复杂度为 O ( N ) 。若是用上述方法,那么只需要对堆进行一次向下调整即可,因为此时根结点的左右子树都是小堆,我们只需要在根结点处进行一次向下调整即可,时间复杂度为 O ( log ( N ) ) 。
//堆的删除
void HeapPop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
Swap(&php->a[0], &php->a[php->size - 1]);//交换堆顶和最后一个结点的位置
php->size–;//删除最后一个结点(也就是删除原来堆顶的元素)
AdjustDown(php->a, php->size, 0);//向下调整
}
获取堆顶的数据
获取堆顶的数据,即返回数组下标为0的数据。
//获取堆顶的数据
HPDataType HeapTop(HP* php)
{
assert(php);
assert(!HeapEmpty(php));
return php->a[0];//返回堆顶数据
}
获取堆的数据个数
获取堆的数据个数,即返回堆结构体中的size变量。
//获取堆中数据个数
int HeapSize(HP* php)
{
assert(php);
return php->size;//返回堆中数据个数
}
堆的判空
堆的判空,即判断堆结构体中的size变量是否为0。
//堆的判空
bool HeapEmpty(HP* php)
{
assert(php);
return php->size == 0;//判断堆中数据是否为0
}
3.3 堆的应用
3.3.1 堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 建堆
- 排升序:建大堆
- 排降序:建小堆
- 利用堆删除思想来进行排序
建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。
以排升序(建大堆)为例:
1.先利用向下调整法建堆:
//建堆(大堆)
for (int i = (n - 1 - 1) / 2; i >= 0; i–)
{
AdjustDown(php->a, php->size, i);
}
2.再利用堆删除思想来进行排序:
步骤如下:
1、将堆顶数据与堆的最后一个数据交换,然后对根位置进行一次堆的向下调整,但是调整时被交换到最后的那个最大的数不参与向下调整。
2、完成步骤1后,这棵树除最后一个数之外,其余数又成一个大堆,然后又将堆顶数据与堆的最后一个数据交换,这样一来,第二大的数就被放到了倒数第二个位置上,然后该数又不参与堆的向下调整…反复执行下去,直到堆中只有一个数据时便结束。此时该序列就是一个升序。
实例:
将下面这组数先建大堆,再排升序
动图演示:(动图来自菜鸟网站->堆排序)
堆排序代码:
//堆排序
void HeapSort(int* a, int n)
{
//排升序,建大堆
//从第一个非叶子结点开始向下调整,一直到根
int i = 0;
for (i = (n - 1 - 1) / 2; i >= 0; i–)
{
AdjustDown(a, n, i);
}
int end = n - 1;//记录堆的最后一个数据的下标
while (end)
{
Swap(&a[0], &a[end]);//将堆顶的数据和堆的最后一个数据交换
AdjustDown(a, end, 0);//对根进行一次向下调整
end–;//堆的最后一个数据的下标减一
}
}
时间复杂度: O ( N l o g N ) 空间复杂度: O ( 1 )
3.3.2 TOP-K问题
TOP-K问题:即求数据集合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等
我们从时间和空间的角度逐步来看:
假设当前我们输入数组
arr[2,7,4,6,2,3,9,8]
,找出其中最大的k
个数。
例如,将k设为4,则在这8个数字中,最大的
k
个数是6、7、8 、9。
这就是所谓的TOP-K问题
角度一
对于Top-K问题,能想到的最简单直接的方式就是排序,利用时间复杂度较低的堆排序将数组排为降序,然后输出前k个数就可以了
代码:
//交换函数
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//堆的向下调整(小堆)
void AdjustDown(int* a, int n, int parent)
{
//child记录左右孩子中值较小的孩子的下标
int child = 2 * parent + 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 = 2 * parent + 1;
}
else//已成堆
{
break;
}
}
}
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
*returnSize = k;
int i = 0;
//建小堆
for (i = (arrSize - 1 - 1) / 2; i >= 0; i–)
{
AdjustDown(arr, arrSize, i);
}
//排降序
int end = arrSize - 1;
while (end > 0)
{
Swap(&arr[0], &arr[end]);
AdjustDown(arr, end, 0);
end–;
}
//将最大的k个数存入数组
int* retArr = (int*)malloc(sizeof(int)*k);
for (i = 0; i < k; i++)
{
retArr[i] = arr[i];
}
return retArr;//返回最大的k个数
}
时间复杂度: O ( N + N l o g N )
【建堆为N,向下调整一次为log N,调整N次为NlogN,所以时间复杂度为O ( N + N l o g N ) 】
空间复杂度: O ( N )
————————————————
角度二
在角度一的基础上,我们可不可以将时间复杂度再降低一些?
我们可以将数组建成一个大堆,因为堆顶的元素最大,所以我们取k次堆顶的元素就可以实现要求了,即把N个数建堆,取出前k个
注意:
1.取出数据后要让其与最后的元素替换,因为你已经取出这个元素了,所以不需要它了,这时让它去堆尾,不让它算入堆的个数中就行了。
2. 如果在取到堆顶数据后直接删除数据,那么就要重新建堆了。正确的做法应该是上面所说的方法,因为那样只要进行一次向下调整,就可以保证堆的结构了。要知道建堆的复杂度为O(N),而一次向下调整的复杂度仅为O(logn),这样大大提升了效率。
代码:
//交换函数
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//堆的向下调整(大堆)
void AdjustDown(int* a, int n, int parent)
{
//child记录左右孩子中值较大的孩子的下标
int child = 2 * parent + 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 = 2 * parent + 1;
}
else//已成堆
{
break;
}
}
}
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
*returnSize = k;
int i = 0;
//建大堆
for (i = (arrSize - 1 - 1) / 2; i >= 0; i–)
{
AdjustDown(arr, arrSize, i);
}
//将最大的k个数存入数组
int* retArr = (int*)malloc(sizeof(int)*k);
int end = arrSize - 1;
for (i = 0; i < k; i++)
{
retArr[i] = arr[0];//取堆顶数据
Swap(&arr[0], &arr[end]);//交换堆顶数据与最后一个数据
//进行一次向下调整,不把最后一个数据看作待调整的数据,所以待调整数据为end=arrSize-1
AdjustDown(arr, end, 0);
end–;//最后一个数据的下标改变
}
return retArr;//返回最大的k个数
}
时间复杂度:O(N+klogN)
空间复杂度:O(N)
角度三
如果数据量非常大,将会占用的内存是巨大的,上面的排序就不太可取了。
我们可以用下面的方法:
- 用数据集合中前K个元素来建堆
- 前k个最大的元素,则建小堆
- 前k个最小的元素,则建大堆
- 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
我们以找出最大的k个数为例:先建一个k个数的小堆,然后将数组中n-k个元素依次与堆顶的元素比较,若比堆顶元素大,则把堆顶元素换为该元素,然后再进行一次向下调整,使其仍为小堆。那么问题来了,为什么不用大堆呢,其实这很容易理解,如果建一个大堆,万一堆顶的数据就是我们所求的k个数中的一个,那么我们所求的k个数中比它小的数就永远不可能入堆,因此要用小堆来解决这个问题。
代码:
//交换函数
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
//堆的向下调整(小堆)
void AdjustDown(int* a, int n, int parent)
{
//child记录左右孩子中值较小的孩子的下标
int child = 2 * parent + 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 = 2 * parent + 1;
}
else//已成堆
{
break;
}
}
}
int* getLeastNumbers(int* arr, int arrSize, int k, int* returnSize)
{
*returnSize = k;
if (k == 0)
return NULL;
//用数组的前K个数建小堆
int i = 0;
int* retArr = (int*)malloc(sizeof(int)*k);
for (i = 0; i < k; i++)
{
retArr[i] = arr[i];
}
for (i = (k - 1 - 1) / 2; i >= 0; i–)
{
AdjustDown(retArr, k, i);
}
//剩下的N-k个数依次与堆顶数据比较
for (i = k; i < arrSize; i++)
{
if (arr[i]>retArr[0])
{
retArr[0] = arr[i];//堆顶数据替换
}
AdjustDown(retArr, k, 0);//进行一次向下调整
}
return retArr;//返回最大的k个数
}
时间复杂度:O(k+n*logk)
空间复杂度:O(n)
与之前两种方法对比,这种方法大大提高了效率。
链式二叉树,那必须得有自己的结点类型,以下是链式二叉树结点类型的定义,下面的问题都统一使用该结点类型。
//二叉树的链式结构
typedef int BTDataType;//结点中存储的元素类型(以int为例)
typedef struct BinaryTreeNode
{
BTDataType data;//结点中存储的元素类型
struct BinaryTreeNode* left;//左指针域(指向左孩子)
struct BinaryTreeNode* right;//右指针域(指向右孩子)
}BTNode;
在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二叉树结构掌握还不够深入,此处先手动快速创建一棵简单的二叉树,方便我们学习。
BTNode* BuyNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
//自建二叉树
BTNode* CreatBinaryTree()
{
BTNode* node1 = BuyNode(‘A’);
BTNode* node2 = BuyNode(‘B’);
BTNode* node3 = BuyNode(‘C’);
BTNode* node4 = BuyNode(‘D’);
BTNode* node5 = BuyNode(‘E’);
BTNode* node6 = BuyNode(‘F’);
BTNode* node7 = BuyNode(‘G’);
node1->left = node2;
node1->right = node3;
node2->left = node4;
node3->left = node5;
node3->right = node6;
node4->left = node7;
return node1;
}
下面的学习以上面自建的二叉树为准
4.1二叉树的深度优先遍历
前序遍历
前序遍历,又叫先根遍历。
遍历顺序:根 -> 左子树 -> 右子树
代码:
//二叉树前序遍历
void PreOrder(BTNode* root) {
if (root == NULL)
{
printf("NULL ");
return;
}
printf("%c ", root->data);
PreOrder(root->left);
PreOrder(root->right);
}
中序遍历
中序遍历,又叫中根遍历。
遍历顺序:左子树 -> 根 -> 右子树
代码:
// 二叉树中序遍历
void InOrder(BTNode* root)
{
if (root == NULL) {
printf("NULL ");
return;
}
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
}
后序遍历
后序遍历,又叫后根遍历。
遍历顺序:左子树 -> 右子树 -> 根
代码:
// 二叉树后序遍历
void PostOrder(BTNode* root)
{
if (root == NULL) {
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
}
4.2二叉树的广度优先遍历
层序遍历
层序遍历,从左往右逐层访问树的结点的过程就是层序遍历。
设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推。
思路(借助一个队列):
借助队列先进先出的性质
1.先把根入队列,然后开始从队头出数据。
2.出队头的数据,把它的左孩子和右孩子依次从队尾入队列(NULL不入队列)。
3.重复进行步骤2,直到队列为空为止。
代码:(自己先写队列及相关功能)
//层序遍历
void BinaryLevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);//初始化队列
if (root != NULL)
QueuePush(&q, root);
while (!QueueEmpty(&q))//当队列不为空时,循环继续
{
BTNode* front = QueueFront(&q);//读取队头元素
QueuePop(&q);//删除队头元素
printf("%c ", front->data);//打印出队的元素
if (front->left)
{
QueuePush(&q, front->left);//出队元素的左孩子入队列
}
if (front->right)
{
QueuePush(&q, front->right);//出队元素的右孩子入队列
}
}
QueueDestroy(&q);//销毁队列
}
4.3 求节点个数以及高度等
4.3.1求二叉树节点个数
//1、遍历(前序) – 全局变量
int size = 0;
void BinaryTreeSize(BTNode* root)
{
if (root == NULL)
return;
else
++size;
BinaryTreeSize(root->left);
BinaryTreeSize(root->right);
}
int main()
{
BTNode* root = CreatBinaryTree();
//1.全局–缺点:第二次使用(例如查看另一颗树节点个数),size值会继承上一次
BinaryTreeSize(root);
printf(“BinaryTreeSize:%d\n”, size);
BinaryTreeSize(root);
printf(“BinaryTreeSize:%d\n”, size);
}
// 2、遍历(前序) – 局部变量
传参时注意不能使用传值传参
void BinaryTreeSize(BTNode* root, int* psize)
{
if (root == NULL)
return;
else
++(*psize);
BinaryTreeSize(root->left, psize);
BinaryTreeSize(root->right, psize);
}
int main()
{
BTNode* root = CreatBinaryTree();
int size1 = 0;//局部
BinaryTreeSize(root, &size1);
printf(“BinaryTreeSize:%d\n”, size1);
int size2 = 0;//局部
BinaryTreeSize(root, &size2);
printf(“BinaryTreeSize:%d\n”, size2);
}
// 3.分治–利用递归(推荐使用)
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : 1
- BinaryTreeSize(root->left)
面试结束复盘查漏补缺
每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。
以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~
重要的事说三遍,关注+关注+关注!
更多笔记分享
printf(“BinaryTreeSize:%d\n”, size);
}
// 2、遍历(前序) – 局部变量
传参时注意不能使用传值传参
void BinaryTreeSize(BTNode* root, int* psize)
{
if (root == NULL)
return;
else
++(*psize);
BinaryTreeSize(root->left, psize);
BinaryTreeSize(root->right, psize);
}
int main()
{
BTNode* root = CreatBinaryTree();
int size1 = 0;//局部
BinaryTreeSize(root, &size1);
printf(“BinaryTreeSize:%d\n”, size1);
int size2 = 0;//局部
BinaryTreeSize(root, &size2);
printf(“BinaryTreeSize:%d\n”, size2);
}
// 3.分治–利用递归(推荐使用)
int BinaryTreeSize(BTNode* root)
{
return root == NULL ? 0 : 1
- BinaryTreeSize(root->left)
面试结束复盘查漏补缺
每次面试都是检验自己知识与技术实力的一次机会,面试结束后建议大家及时总结复盘,查漏补缺,然后有针对性地进行学习,既能提高下一场面试的成功概率,还能增加自己的技术知识栈储备,可谓是一举两得。
以下最新总结的阿里P6资深Java必考题范围和答案,包含最全MySQL、Redis、Java并发编程等等面试题和答案,用于参考~
重要的事说三遍,关注+关注+关注!
[外链图片转存中…(img-I7oOf0CY-1715589340510)]
[外链图片转存中…(img-BZ9sFtHm-1715589340510)]
更多笔记分享
[外链图片转存中…(img-RNSLGPY7-1715589340510)]