剑指offer 学习笔记 数组中出现次数超过一半的数字
C/C++中我们要养成使用指针或引用传递复杂类型参数的习惯,如采用值传递,则从实参到形参会产生一次赋值操作。
对同一算法用递归和循环的时间效率可能也不会一样,递归的本质是将一个大的复杂问题分解成两个或多个小的简单问题,如果小问题中有相互重叠的部分,那么直接使用递归实现虽然代码会比较简洁,但时间效率可能会很差,对于这种问题,我们可以用递归的思路来分析问题,但写代码时候可以用数组来保存中间结果从而基于循环实现,绝大部分动态规划算法的分析和代码实现都是分这两个步骤完成的。
面试题39:数组中出现次数超过一半的数字。数组中有一个数字出现的次数超过数组长度的一半,找出这个数字。
最直观的想法是将数组排序,之后找出数字出现的频率就简单了。但排序的时间复杂度是O(nlogn),太慢了。
数组中有一个数字的出现次数超过了数组长度的一半,那么如果把这个数组排序,排序之后的位于数组中间位置的数字一定是那个出现次数超过数组长度一半的数字。这个数字就是中位数,即长度为n的数组中第n / 2大的数字。有成熟的时间复杂度为O(n)的算法得到数组中任意第k大的数字。这种算法受快排的启发,在随机快排算法中,我们先在数组中随机选择一个数字,然后调整数组中数字的顺序,使得比选中的数字小的数字都排在它的左边,比选中数字大的数字都排在它的右边,如果此轮排序后这个选中的数字下标正好是n / 2,那么这个数字就是数组的中位数,如果它的下标大于n / 2,那么中位数位于它的左边,我们可以在接着它的左边部分的数组中查找,如果它的下标小于n / 2,那么中位数在它的右边,接下来就在它的右边部分的数组中查找:
#include <iostream>
using namespace std;
bool g_bInputValid = false; // 输入错误标志位
bool CheckInvalidArray(int* nums, int length) {
if (nums == nullptr || length <= 0) {
g_bInputValid = true;
return g_bInputValid;
}
return g_bInputValid;
}
bool CheckMoreThanHalf(int *nums, int res, int length) {
int times = 0;
for (int i = 0; i < length; ++i) {
if (nums[i] == res) {
++times;
}
}
if ((times << 1) <= length) {
g_bInputValid = true;
return false;
}
return true;
}
int Partition(int* nums, int start, int end) {
if (nums == nullptr) {
return false;
}
int target = start + (rand() % (end - start + 1)); // 从start + 0 ~ start + end - start中随机选择一个数字
swap(nums[target], nums[end]); // 将选中的数字与下标为end的数字交换
int small = start - 1;
int index = start;
while (index < end) {
if (nums[index] < nums[end]) {
++small;
swap(nums[small], nums[index]);
}
++index;
}
swap(nums[++small], nums[end]);
return small;
}
int MoreThanHalfNum(int *nums, int length, int start, int end) {
g_bInputValid = false; // 先初始化出错标志
if (CheckInvalidArray(nums, length)) {
return 0;
}
int middle = length >> 1;
int index = Partition(nums, start, end); // index即为随机选中的数字下标,此时它的左(右)边的数都比它小(大)
while (middle != index) {
if (middle > index) { // 如果index在middle左区域,说明中位数在index的右边
start = index + 1; // 更新start,缩小范围
index = Partition(nums, start, end);
} else {
end = index - 1;
index = Partition(nums, start, end);
}
}
if (!CheckMoreThanHalf(nums, nums[middle], length)) { // 检查中位数是否出现了超过数组长度一半的次数
return 0;
}
return nums[middle];
}
int main() {
int nums[] = { 1,1,1,1,1,2,3,2,2,2,2,2,2,2,2,4 };
int res = MoreThanHalfNum(nums, sizeof(nums) / sizeof(*nums), 0, sizeof(nums) / sizeof(*nums) - 1);
if (!g_bInputValid) { // 如果没出错
cout << res << endl;
}
}
该方法的时间效率为O(n),且此方法修改了输入数组,接下来介绍一种不用修改输入数组的方法。
数组中有一个数字出现的次数超过数组长度的一半,意味着它的出现次数比其他所有数字出现的次数之和还要多,因此我们可以遍历整个数组,遍历时保存两个值,一个是数组中的第一个数字,另一个是次数。当我们遍历到下一个数字时,如果下一个数字与我们保存的数字相同,则次数加1,如果下一个数字和我们之前保存的数字不同,则次数减1,如果减1后次数为0,那么需要保存该位置的下一个数字,并把次数重新设为1。由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为1时对应的数字:
#include <iostream>
using namespace std;
bool g_bInputValid = false; // 输入错误标志位
bool CheckInvalidArray(int* nums, int length) {
if (nums == nullptr || length <= 0) {
g_bInputValid = true;
return g_bInputValid;
}
return g_bInputValid;
}
bool CheckMoreThanHalf(int* nums, int res, int length) {
int times = 0;
for (int i = 0; i < length; ++i) {
if (nums[i] == res) {
++times;
}
}
if ((times << 1) <= length) {
g_bInputValid = true;
return false;
}
return true;
}
int MoreThanHalfNum(int* nums, int length) {
g_bInputValid = false;
if (CheckInvalidArray(nums, length)) {
return 0;
}
int res = nums[0];
int times = 1;
for (int i = 1; i < length; ++i) {
if (times == 0) {
res = nums[i];
times = 1;
} else if (nums[i] == res) {
++times;
} else{
--times;
}
}
if (!CheckMoreThanHalf(nums, res, length)) {
return 0;
}
return res;
}
int main() {
int nums[] = { 0,1,2,2,2,2,1,1,1,1,1,1,1 };
int res = MoreThanHalfNum(nums, sizeof(nums) / sizeof(*nums));
if (!g_bInputValid) {
cout << res << endl;
} else {
cout << "Invalid Input." << endl;
}
}
以上算法的时间复杂度为O(n),它的实质是当相邻的两个数字不同时,忽略这两个不同数字,并继续在剩下的数组部分中接着查找,因为被忽略的要么两个都不是出现次数最多的数字,要么一个是另一个不是,第一种情况下,剩下的数组中出现次数超过数组一半的数字不变,另一种情况下,也没改变数组中出现次数超过数组一半的数字。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)