面试题 17.10. 主要元素
数组中占比超过一半的元素称之为主要元素。
给定一个整数数组,找到它的主要元素。若没有,返回-1。
示例 1:
输入:[1,2,5,9,5,9,5,5,5]
输出:5
示例 2:
输入:[3,2]
输出:-1
示例 3:
输入:[2,2,1,1,1,2,2]
输出:2
说明:
你有办法在时间复杂度为 O(N),空间复杂度为 O(1) 内完成吗?
思路1:排序取中值
这个思路还是蛮容易想到的,由于主要元素超过整个数组的以上,所以对数组进行排序后,取中间值,必定是所求元素。
int
cmpfun(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
int
majorityElement(int* nums, int numsSize) {
qsort(nums, numsSize, sizeof(int), cmpfun);
return nums[numsSize / 2];
}
思路2:摩尔投票法
摩尔投票法,一次遍历即可实现,
时间复杂度为O(n),空间复杂度为O(1)
具体操作为:
取数组第一个元素为候选人,记当前票数为1;
然后往后遍历的时候,如果遇到相同的元素,则票数加一,
如果遇到相异的元素,则票数减一。
一旦票数为0,则更换候选人为下一元素。
这样,相当于每次相异的元素会被抵消掉,由于主要元素占过半,
所以它还是最能耗的,最终留下来的就是它了。
int
majorityElement(int* nums, int numsSize) {
int major = nums[0], cnt = 1;
for (int i = 1; i < numsSize; i++) {
nums[i] == major ? cnt++ : cnt--;
if (cnt == 0)major = nums[i + 1]; //元素过半,不会溢出
} return major;
}
.
.
.
给定一个整数数组和一个整数 k,
判断数组中是否存在两个不同的索引 i 和 j,
使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k
示例 1:
输入: nums = [1,2,3,1], k = 3
输出: true
示例 2:
输入: nums = [1,0,1,1], k = 1
输出: true
示例 3:
输入: nums = [1,2,3,1,2,3], k = 2
输出: false
思路1:暴力滑动窗口 (超时)
本来想到快慢指针,然后发现它并不是固定为k,而是最大为k
所以其实是一个窗口大小可变的滑动窗口,就,很暴力:
两层循环,外层从头到尾,里层循环k次,复杂度爆炸
所以这种解法自然而然的在最后一个测试用例的时候timeout了
(超时)
bool
containsNearbyDuplicate(int* nums, int numsSize, int k) {
for (int i = 0; i < numsSize - 1; i++) {
for (int j = i + 1; j < i + k + 1 && j < numsSize; j++) {
if (nums[i] == nums[j])return true;
}
} return false;
}
思路2:哈希表
一次遍历实现,每次读取一个元素,判断是否存在哈希表中
如果存在,则判断其位置与当前元素是否满足条件k
如果满足,则返回true
否则,把新的元素的键值对存入哈希表中——可能覆盖之前的等值但>k的
遍历结束后,没有找到,则返回false
这里用了网上某个大佬的代码,知道了C语言有个优秀的开源代码
#include “uthash.h”
利用它只需要再定义一个结构体,就可以实现哈希的相关操作
typedef struct Hash_Tag {
int key; //键
int value; //值
UT_hash_handle hh; //内部句柄
} *pHASH;
bool
containsNearbyDuplicate(int* nums, int numsSize, int k) {
pHASH now = NULL, hashtable = NULL;
for (int i = 0; i < numsSize; i++) {
if (hashtable) HASH_FIND_INT(hashtable, &(nums[i]), now); //如果哈希表内有数据,则寻找当前元素是否在表中
if (now && (i - now->value) <= k) return true; //找到了且满足条件则返回
now = (pHASH)malloc(sizeof(*now)); //找不到就把新元素加入哈希表中
now->key = nums[i];
now->value = i;
HASH_ADD_INT(hashtable, key, now);
} return false;
}
.
.
.
给定一个整型数组,在数组中找出由三个数组成的最大乘积,并输出这个乘积
示例 1:
输入: [1,2,3]
输出: 6
示例 2:
输入: [1,2,3,4]
输出: 24
思路:排序后求最值
排序后,分析可能出现的情况:
数组有正有负,最大值可能是两个最小的负数相乘,再乘个最大正数
也可能是最大的三个正数相乘——没有其它情况了
或者说其它情况不会是最大值
#define MAX(a,b) (a)>(b)?(a):(b)
int
cmpfun(const void* a, const void* b) {
return *(int*)a - *(int*)b;
}
int
maximumProduct(int* nums, int numsSize) {
qsort(nums, numsSize, sizeof(int), cmpfun);
return MAX(nums[0] * nums[1] * nums[numsSize - 1],
nums[numsSize - 1] * nums[numsSize - 2] * nums[numsSize - 3]);
}
.
.
.
假设你有一个很长的花坛,一部分地块种植了花,另一部分却没有。
可是,花卉不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给定一个花坛(0表示没种植花,1表示种植了花)和一个数 n 。
能否在不打破种植规则的情况下种入 n 朵花?
能则返回True,不能则返回False。
示例 1:
输入: flowerbed = [1,0,0,0,1], n = 1
输出: True
示例 2:
输入: flowerbed = [1,0,0,0,1], n = 2
输出: False
思路1:模拟+贪心
从头到尾遍历一次,以3个花坛为单位进行判断,
只要能够种,就模拟一次种花过程,把相应位置置1
每次循环判断该位置前、中、后是否都为0,如果是,则种花,计数+1
特殊情况是头和尾,并不需要头有开头0,并不需要尾有结尾0
bool
canPlaceFlowers(int* flowerbed, int flowerbedSize, int n) {
for (int i = 0; i < flowerbedSize; i++) {
if (!flowerbed[i] && \ /*当前位置为0*/
(i == 0 || !flowerbed[i - 1]) && \ /*上一位置也为0*/ /*或者是开头*/
(i == flowerbedSize - 1 || !flowerbed[i + 1])) {
/*下一位置也为0*/ /*或者是结尾*/
flowerbed[i++] = 1, n--; //模拟种花
}
if (n <= 0)return true;
}return false;
}
思路2:模拟+贪心+只纠错不预判
看到一个网友的思路,很秀:
每次只考虑当前位置和上一位置
如果当前位置为0,上一位置也为0,则可种,就种一颗
如果当前位置为1,下一位置也为1,则上次种错了,拔掉上一颗(太秀了)
bool
canPlaceFlowers(int* flowerbed, int flowerbedSize, int n) {
if (!flowerbed[0]) {
flowerbed[0] = 1, n--; } //处理开头,避免-1溢出
for (int i = 1; i < flowerbedSize; i++) {
//模拟种花过程
if (!flowerbed[i] && !flowerbed[i - 1]) {
//当前为0,上一位也为0
flowerbed[i] = 1, n--; //种花
} else if (flowerbed[i] && flowerbed[i - 1]) {
//当前为1,上一位也为1
flowerbed[i - 1] = 0, n++; //拔花
} //其它情况就不做操作,直接i++继续后移
} return n <= 0;
}
.
.
.
给定一个二进制矩阵 A,我们想先水平翻转图像,然后反转图像并返回结果
水平翻转图片就是将图片的每一行都进行翻转,即逆序。
例如,水平翻转 [1, 1, 0] 的结果是 [0, 1, 1]。
反转图片的意思是图片中的 0 全部被 1 替换, 1 全部被 0 替换。
例如,反转 [0, 1, 1] 的结果是 [1, 0, 0]。
示例 1:
输入: [[1,1,0],[1,0,1],[0,0,0]]
输出: [[1,0,0],[0,1,0],[1,1,1]]
解释: 首先翻转每一行: [[0,1,1],[1,0,1],[0,0,0]];
然后反转图片: [[1,0,0],[0,1,0],[1,1,1]]
示例 2:
输入: [[1,1,0,0],[1,0,0,1],[0,1,1,1],[1,0,1,0]]
输出: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
解释: 首先翻转每一行: [[0,0,1,1],[1,0,0,1],[1,1,1,0],[0,1,0,1]];
然后反转图片: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
思路:模拟+翻转的同时顺便反转
一次循环遍历,在翻转的同时顺便把结果反转,而不应该分成两次循环
这里要注意二维数组返回参数的方法!
*returnSize = ASize;
returnColumnSizes[0] = AColSize;
int** flipAndInvertImage(int** A, int ASize, int* AColSize, int* returnSize, int** returnColumnSizes) {
*returnSize = ASize;
returnColumnSizes[0] = AColSize;
for (int i = 0; i < ASize; i++) {
for (int j = 0; j < (*AColSize + 1) / 2; j++) {
int temp = A[i][j];
A[i][j] = A[i][*AColSize - 1 - j] ^ 1;
A[i][*AColSize - 1 - j] = temp ^ 1;
}
} return A;
}
.
.
.
给你一份[词汇表](字符串数组) words 和一张[字母表](字符串) chars
假如你可以用 chars 中的[字母]拼写出 words 中的某个[单词],
那么我们就认为你掌握了这个单词
注意:每次拼写(指拼写词汇表中的一个单词)时,
chars 中的每个字母都只能用一次
返回词汇表 words 中你掌握的所有单词的 长度之和
示例 1:
输入:words = [“cat”,“bt”,“hat”,“tree”], chars = “atach”
输出:6
解释: 可以形成字符串 “cat” 和 “hat”,所以答案是 3 + 3 = 6
示例 2:
输入:words = [“hello”,“world”,“leetcode”], chars = “welldonehoneyr”
输出:10
解释:可以形成字符串 “hello” 和 “world”,所以答案是 5 + 5 = 10
提示:
1 <= words.length <= 1000
1 <= words[i].length, chars.length <= 100
所有字符串中都仅包含小写英文字母
思路:两张哈希表分别记录存储量和消耗量
两个哈希表,一个用来存储目前掌握的chars的单词字母的数量,
一个用来记录消耗字母数量。
当消耗大于储备量,则无效,否则则为一次有效单词,添加长度
每个单词判断完后把消耗表清空
int
countCharacters(char** words, int wordsSize, char* chars) {
int len = 0;
int hashtable[26] = {
0 }, usetable[26] = {
0 };
for (int i = 0; chars[i]; hashtable[chars[i++] - 'a']++); //入货
for (int i = 0; i < wordsSize; i++) {
int j = 0;
for (; words[i][j]; j++) {
//出货wordsSize次
if (usetable[words[i][j] - 'a']
< hashtable[words[i][j] - 'a']) {
//库存大于需求
usetable[words[i][j] - 'a']++; //每次消耗量++
} else {
break; }
} if (j == strlen(words[i])) len += strlen(words[i]); //库存充足
for (int k = 0; k < j; usetable[words[i][k++] - 'a'] = 0); //消除消耗
} return len;
}
.
.
.
在一排座位( seats)中,1 代表有人坐在座位上,0 代表座位上是空的。
至少有一个空座位,且至少有一人坐在座位上。
小明希望坐在一个能够使他与离他最近的人之间的距离达到最大化的座位上
返回他到离他最近的人的最大距离。
示例 1:
输入:[1,0,0,0,1,0,1]
输出:2
解释:
如果小明坐在第二个空位(seats[2])上,他到离他最近的人的距离为 2
如果小明坐在其它任何一个空位上,他到离他最近的人的距离为 1
因此,他到离他最近的人的最大距离是 2
示例 2:
输入:[1,0,0,0]
输出:3
解释:
如果小明坐在最后一个座位上,他离最近的人有 3 个座位远
这是可能的最大距离,所以答案是 3
提示:
2 <= seats.length <= 20000
seats 中只含有 0 和 1,至少有一个 0,且至少有一个 1。
思路:求连续0串的最大值
求出连续0串的最大值max后,输出(max+1)/2即为结果
但是我们还得考虑开头和结尾这两种边界情况
如果开头或结尾为0,开头连续0串符合条件的最大值应该为0串长度
#define MAX(a,b) (a)>(b)?(a):(b)
int
maxDistToClosest(int* seats, int seatsSize) {
int max = 0, headzero = 0, tailzero = 0;
for (int i = 0, zeroCnt = 0; i < seatsSize; i++) {
if (seats[i]) {
zeroCnt = 0;
} else {
zeroCnt++;
max = zeroCnt > max ? zeroCnt : max;
}
}
for (int i = 0; i < seatsSize && !seats[i]; headzero++, i++);
for (int i = seatsSize - 1; i > 0 - 1 && !seats[i]; tailzero++, i--);
return MAX(MAX(headzero, tailzero), (max + 1) / 2);
}
.
.
.
斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。
该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1
给定 N,计算 F(N)
思路1:动态规划+记忆化数组
就,从0到N迭代,每次求得结果之后记录到数组相应位置
这样,就是挨个求,一次遍历一次求出来就好了,也符合普通的思考运算顺序
最后输出数组最后一个元素就行了
注意不要用递归,很浪费,因为每次求某个数的结果时,
很多分支都被求了很多次了,完全没必要
int fib(int N) {
int fab[31] = {
0 };
for (int i = 0; i <= N; i++) {
if (i == 0) {
fab[i] = 0;
} else if (i == 1) {
fab[i] = 1;
} else {
fab[i] = fab[i - 1] + fab[i - 2];
}
} return fab[N];
}
思路2:优化版动态规划
我们发现,其实只需要保存上一个和上上个数据,以方便这一次数据的计算即可
比如,当我们求fab[7]时,只需要fab[5]和fab[6]就可以了,前面的不需要再保存了
所以我们只需要建立3个变量分别存储“前次”、“上次”、“这次”的数据即可
int fib(int N) {
if (N <= 1)return N;
int prev = 0, last = 1, now = 0;
for (int i = 2; i <= N; i++) {
now = prev + last;
prev = last;
last = now;
} return now;
}
.
.
.
给定一个矩阵 A, 返回 A 的转置矩阵
矩阵的转置是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引
示例 1:
输入:[[1,2,3],[4,5,6],[7,8,9]]
输出:[[1,4,7],[2,5,8],[3,6,9]]
示例 2:
输入:[[1,2,3],[4,5,6]]
输出:[[1,4],[2,5],[3,6]]
提示:
1 <= A.length <= 1000
1 <= A[0].length <= 1000
int**
transpose(int** A, int ASize, int* AColSize,
int* returnSize, int** returnColumnSizes) {
*returnSize = *AColSize;
int** ret = malloc(sizeof(int*) * AColSize[0]);
for (int i = 0; i < AColSize[0]; i++) {
ret[i] = (int*)malloc(sizeof(int) * ASize);
returnColumnSizes[0][i] = ASize;
for (int j = 0; j < ASize; j++)
ret[i][j] = A[j][i];
} return ret;
}
.
.
.
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,
同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。
思路1:双指针
一开始我想到的是采用双指针,这个方向本身没错,但是我的思路是,分别用头指针和尾指针指向头和尾,头指针找到0值,尾指针找到非0值,然后交换位置,直到头尾相交…
但是这个思路有个问题,它无法确保非零元素的相对顺序——我一开始没审清题意。
这是一开始的错误的代码:
void
moveZeroes(int* nums, int numsSize){
for(int head = 0, tail = numsSize-1; head<tail; ){
if(0!=nums[head]){
head++;
}else if(0==nums[tail]){
tail--;
}else if(0==nums[head]&&0!=nums[tail]){
swap(&nums[head++], &nums[tail--]);
}
}
}
于是有了第二种思路:
思路2:快慢指针
快指针从0开始,每次遍历递增1,遇到非零值则和慢指针对应元素交换
慢指针从0开始,只有在快指针遇到非零值才递增1
即,在没遇到零值时,快慢指针步调一致,如果(快指针)遇到零值,慢指针不动,快指针继续走,直到快指针遇到非零值。
简单来说就是,快指针负责找到非零值然后按照0、1、2、3…的顺序填到慢指针所对应的位置,这里慢指针相当于一个loc变量的作用
void
swap(int* a, int* b) {
int temp;
temp = *a, *a = *b, *b = temp;
}
void
moveZeroes(int* nums, int numsSize){
for(int fast = 0, slow = 0; fast<numsSize; fast++){
if(0!=nums[fast]){
swap(&nums[fast], &nums[slow++]);
}
}
}