1、参考 523. 连续的子数组和中,C语言中也可以HASH, 详细使用方法
uthash底层实现的数据结构就是一个双向链表
C语言哈希表uthash的使用方法详解(附下载链接) - 知乎
使用HASH_FIND_INT(g_users, &key, node) 查找node结点时,HASH_COUNT(node) 返回的是整个hash 中的元素总个数(并非当前node->key的子项的重复数),等价于HASH_COUNT(g_users)
2、背包问题总结:力扣
3、qsort 支持字符串指针的排序,eg: leetcode 524
char *dic[] = {"aaa", "ntgcykxhdfescxxypyew"}; // 字符串指针数组定义方法
qsort (dic, dictionarySize, sizeof (char *), comp);
// 长度长的优先,相等时字典序
int comp (const void* a, const void *b) {
char *p1 = *(char**)a; // 注意comp中的实现中需要二维指针
char *p2 = *(char**)b;
int s1 = strlen(p1);
int s2 = strlen(p2);
if (s1 != s2)
return s2-s1;
return strcmp (p1, p2);
}
3、统计最大值为10000的数字中各个数字的下标,leetcode 1202, 避免申请超大内存的方法,采用2次遍历
4、接口strtok的使用时第二次访问时第一个参数是NULL, 因此这个接口不能并行处理多个串, 参考C library function - strtok()
5、leetcode 629 介绍了记忆化搜索和动态规划的关系(两者不等价),详见力扣
6、myLowerBound和myUpperBound实现上的差异在于data[mid]==k时的差异,参考实现lower_bound和upper_bound算法_yang20141109的专栏-CSDN博客
leetcode 1231, 对于序列 9,8,8,7,6,6,5,5,4, 如果找最后一个6,那么上面二分法的框架第一个大于等于2 或者第一个大于2 均不符合要求,因此可以考虑将mid+1进行确认。(思考:需要找出一个mid值,按照mid值能分K+1份,而mid+1值不能分K+1份的最大mid值,显然mid+1值就是首个大于的UpperBound场景)
Leetcode 475 中二分法的右边界界是right = nums.length - 1
7、技巧:答案中每个坐标的visit元素是多个值,确保仅排除在相同位置且速度一致的重复状态,参考 力扣
8、Leetcode 1202
a) 在并查集查找后额外进行路径压缩,从而保证相同集合的均使用同一代表值
b) 使用二次遍历法统计每个集合的元素 (第一次统计各个集合元素个数,使用前缀和下标预留位置 第二次遍历将各个数字存入到相应的位置)
9、LeetCode Offer 42 连续子数组的最大和 并不是使用划窗,而是应该贪心
划窗解决的问题是 子区间 和不能超越每个阈值时的最长区间类似的,也就是目标值区间和是给定的;显然,划窗类问题中左右边界将对答案有重要影响,而本题并不关心左右边界
10、部分场景中双端队列可以简化模拟优先队列,但两者是不同的。优先队列中存储所有压入的数值,而双端队列为了保证单调性,必须去重部分结点
Leetcode 407 接雨水中每个最小值还关联坐标信息,也就是相应的最小值仅对当前坐标周围点起效,因此不能简化为双端队列
Leetcode 239 滑动窗口中的最大值显然很容易通过优先队列实时获取(常规思路),为什么能简化为双端队列呢?就是因为非最大值的丢失不会影响计算结果,因此双端队列中单调性来模拟优先队列获取最大值
11、实时的票数最大值为上次的最大值和当前新更新的值比较,参考leetcode 911力扣
if (top < 0 || voteCounts[persons[i]] >= voteCounts[top]) {
top = persons[i];
}
obj->tops[i] = top;
12、双指针将枚举的时间复杂度从 O(N^2)减少至 O(N)O(N), 适用场景:当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,参考leetcode 14力扣,可以替代hash操作
13、Leetcode 415中两个数相加,由于很难事先确认相加后结果的有效位数,因此可以采取的方法是先下标小的位存放低位的结果,完成计算后数据进行翻转即可
14、Leetcode 630 中使用qsort模拟实现大小堆,力扣
void push_q(int val, int *q, int *tail) {
q[(*tail)++] = val;
// 剪枝, 仅在压入更大值时进行排序,减少排序次数
if ((*tail) >= 2 && val < q[(*tail) - 2]) {
qsort(q, (*tail), sizeof(int), _cmp);
}
}
int pop_q(int *q, int *tail) {
if ((*tail) == 0) {
return 0;
}
return q[--(*tail)];
}
int top_q(int *q, int *tail) {
if ((*tail) == 0) {
return 0;
}
return q[(*tail) - 1];
}
15、146. LRU 缓存机制 利用uthash 双向链表的性质,访问之前先删除再添加,保证哈希表元素是按照访问时间从旧到新的,这样在删除“最久未使用”元素时只需要迭代哈希表,然后删除第一个元素即可
16、字符串的hash计算方法,参考力扣
我们可以用一个质数 p ,比如 31 当作底数; 将字符串转化为 sub[0]*p^{m-1}+sub[1]*p^{m-2}...+sub[m-1]sub[0]∗p ^m−1+sub[1]∗p^m−2...+sub[m−1] 。 这其实基本上就是 JDK 中对 string 哈希的默认做法。
而这个哈希计算在滑动过程中,我们也不需要每次都重新计算一遍,可以用上一位置的状态转移过来,如果将hash值看成31进制数的话就是所有位数都左移一位,再去头加尾即可,相信很好理解:hash = hash * prime - prime^m * (s[i-len] - charCode.at(a)) + (s[i] - charCode.at(a))hash=hash∗prime−prime^m∗(s[i−len]−charCode.at(a))+(s[i]−charCode.at(a));
这就是滑动窗口的威力。正常来说这个值会很大可能会导致溢出,所以RK算法应该还要对这个数取模,这样会导致hash冲突;不过用 unsigned long long 存储相当于自动取模了
17、约瑟夫环,参考力扣
假设 f(n-1, m) = x, 显然 f(n, m) = (x+m)%n; 边界f(1,m)=0 表示只有一个元素的圈时,最后剩下的必然是下标0
18、排列数和组合数的差异参考力扣
比如爬楼梯中,如果楼梯3格,2+1和1+2 我们认为是两者不同的方法,则是排列数;而硬币面额 2+1 和1+2我们认为是相同的情况,则属于组合
先枚举金额,代表对于金额来说每次硬币循环都是可以的,1+2和2+1代表了两种方案,是排列数,也就是爬楼梯方案,但是先枚举硬币,那么只是对相应的金额进行方案添加,之前的硬币使用后不能再次使用1+2可行,但是2+1就不会再出现,是组合数,也就是零钱兑换的方案
19、双指针和滑动窗口的概念区分
计算过程仅与「两端点」相关的称为「双指针」,将计算过程与「两端点表示的区间」相关的称为「滑动窗口」,参考力扣
20、对于16进制的编码,数字 0-9 的编码[48,58], 也就是荣通过 value |= 0x20 后仍旧属于[48,58], 这样就可以将 0x12aAd 统一为0x12aad 这样的字符串,仍旧是十六进制值
21、线段树(类似字典树)参考力扣,关键在于区间和的计算
1.上述代码构建的树应是1棵完全二叉树,配图是错的。叶子节点存储原始nums 2.根节点的索引是1 3.左子树节点的索引为偶数 右子树节点的索引为奇数 4.叶子节点的索引范围[n,2n-1]
int sumRange(int left, int right) {
int sum = 0;
left += len;
right += len;
while (left <= right) { // 左右区间更新规则在奇、偶上相反
if (left % 2 == 1) {
sum += segmentTree[left++];
}
if (right % 2 == 0) {
sum += segmentTree[right--];
}
left >>= 1;
right >>= 1;
}
return sum;
}
22、LeetCode 308,将每行看成一个子区间,则更新一个值时,仅仅更新相应行的前缀和(不需要更新整体二维区间的前缀和),问题简化成304
23、使用#define printf(...)屏蔽打印信息,确认超时是否因为printf引起