编程总结
每每刷完一道题后,其思想和精妙之处没有地方记录,本篇博客用以记录刷题过程中的遇到的算法和技巧
13. 罗马数字转整数
罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。
字符 数值
I 1
V 5
X 10
L 50
C 100
D 500
M 1000
例如, 罗马数字 2 写做 II ,即为两个并列的 1。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。
通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况:
I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。
X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。
C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。
给定一个罗马数字,将其转换成整数。输入确保在 1 到 3999 的范围内。
struct hash
{
int interger;
char roman;
};
// 哈希表
struct hash Hash[13] = {
1000, 'M',
500, 'D',
100, 'C',
50, 'L',
10, 'X',
5, 'V',
1, 'I'
};
int charToInt(char c)
{
for (int i = 0; i < 7; i++)
{
if (Hash[i].roman == c) {
return Hash[i].interger;
}
}
return 0;
}
int romanToInt(char * s)
{
int len = strlen(s);
int value = 0;
int cur, next;
for (int i = 0; i < len - 1; i++) // 先计算 [0, len-1)
{
cur = charToInt(s[i]);
next = charToInt(s[i + 1]);
value = (cur < next) ? value - cur : value + cur; // 小技巧,如果cur < next 则减掉cur
}
value += charToInt(s[len - 1]); // 补上 len-1
return value;
}
两种方法都使用哈希表来实现。
散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表
整数转罗马,整数可以根据更为详细的哈希表来转换,但罗马转数字不行,得挨个查找,因为,仅看字符无法知道是需要减的情况还是增的情况,所以上下都使用了哈希表,但其表是不一样的。
struct hash
{
int interger;
char roman[4];
}Hash[13] = { // 这也是一种初始化的方法
1000, "M",
900, "CM",
500, "D",
400, "CD",
100, "C",
90, "XC",
50, "L",
40, "XL",
10, "X",
9, "IX",
5, "V",
4, "IV",
1, "I",
};
char *intToRoman(int num) {
char *cRoman = NULL;
unsigned short uIndex = 0;
cRoman = (char *)malloc(16 *sizeof(char));;
memset(cRoman, 0, 16);
for (uIndex = 0; uIndex <= (13 - 1); uIndex++)
{
while (num >= Hash[uIndex].interger)
{
num = num - Hash[uIndex].interger;
strcat(cRoman, Hash[uIndex].roman); // 注意此时的Hash和之前的不一样,更为详细
}
}
return cRoman;
}
注意下面直接分配栈空间地址回去在leetcode是不允许的,会报错,应该使用上面的 malloc 使用堆地址空间:
char *intToRoman(int num){
char cRoman[16] = {0x0}; // 申请栈空间
unsigned short uIndex = 0;
for(uIndex = 0; uIndex <= (13-1); uIndex++)
{
while(num >= Hash[uIndex].interger)
{
num = num - Hash[uIndex].interger;
strcat_s(cRoman, Hash[uIndex].roman); // 注意此时的Hash和之前的不一样,更为详细
}
}
return cRoman; // 返回栈空间
}
409. 最长回文串
给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。
在构造过程中,请注意区分大小写。比如 “Aa” 不能当做一个回文字符串。
#define G_LENGTH 57 // ASCII编码中,A~z共有57个字符
// 65~90为26个大写英文字母,97~122号为26个小写英文字母
int longestPalindrome(char *s)
{
int g_string[G_LENGTH] = {0};
int length = strlen(s);
int value = 0;
int ans = 0;
memset(g_string, 0, G_LENGTH);
for(int i = 0; i < length; i++) {
g_string[(s[i] - 'A') % 57]++; // 哈希存储
}
for(int j = 0; j < G_LENGTH; j++) {
if (g_string[j] !=0 ) {
value = g_string[j]; // 每个字符出现的次数
ans = ans + value/2 * 2; // 左右都出现一次 v / 2
if (value % 2 == 1 && ans % 2 == 0) { // 发现第一个出现次数为奇数加1,其他情况不加
++ans;
}
}
}
return ans;
}
剑指 Offer 62. 圆圈中最后剩下的数字
- 0,1,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字。求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
// 递归
int lastRemaining(int n, int m){
return f(n, m);
}
int f(int n, int m)
{
if (n == 1)
return 0;
int x = f(n - 1, m);
return (m + x) % n;
}
// 另一种方法
int lastRemaining(int n, int m){
if (n == 1) {
return 0;
}
int ans = 0;
for (int i = 2; i <= n ;i++) {
ans = (ans + m) % i;
}
return ans;
}
推导公式参考:https://leetcode-cn.com/problems/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-lcof/solution/yuan-quan-zhong-zui-hou-sheng-xia-de-shu-zi-by-lee/
矩阵相乘:
https://blog.csdn.net/qq_41649694/article/details/81432663
136. 只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
int singleNumber(int *nums, int numsSize)
{
int i = 0;
int tmp = 0;
// 任何数和 00 做异或运算,结果仍然是原来的数,即 a \oplus 0 = aa⊕0 = a
// 任何数和其自身做异或运算,结果是 00,即 a \oplus a = 0a⊕a = 0
// 异或运算满足交换律和结合律,即 a \oplus b \oplus a = b
for (i = 0; i < numsSize; i++) { // 数组中两两出现的数字都会异或成原来的数,剩下的0和单独出现数字异或得数字本身
tmp = tmp ^ nums[i];
}
return tmp;
}
680. 验证回文字符串 Ⅱ
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串
// 判断从 i - j 是否为回文字符串
bool isPalindrome(char *s, int i, int j)
{
for (int k = i; k <= i + (j - i)/2; k++) {
if (s[k] != s[j - k + i]) {
return false;
}
}
return true;
}
// 验证回文字符串Ⅱ -- 贪心算法
bool validPalindrome(char *s)
{
int j = strlen(s) - 1; // 计算的是真实大小
int i = 0;
while (i < j) {
// 起始字符不相同,那只能删除头尾字符形成回文,否则就不会是回文
if (s[i] != s[j]) {
return (isPalindrome(s, i + 1, j) || isPalindrome(s, i, j - 1));
}
// 起始字符相同,继续往里面判断
i++; // 如果s[i] == s[j], 则判断s[i+1] 和 s[j-1]
j--;
}
return true; // 如果 i >= j, 说明 s 本身就是回文字符串
}
611 有效三角形的个数
用了比较笨的排序方法,其实还可以使用双指针,复杂度要小些的方法。
int CmpInt(const void* a, const void* b)
{
return *(const int*)a - *(const int*)b;
}
int triangleNumber(int *nums, int numsSize)
{
int count = 0;
qsort(nums, numsSize, sizeof(int), CmpInt);
for (int i = 0; i < numsSize - 2; i++) {
for (int j = i + 1; j < numsSize - 1; j++) {
for (int k = j + 1; k < numsSize; k++) {
if (nums[i] + nums[j] > nums[k])
count++;
}
}
}
return count;
}
344. 反转字符串
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
void swap(char *a, char *b)
{
char *t = NULL;
*t = *a;
*a = *b;
*b = *t;
}
void reverseString(char *s, int sSize)
{
int right = sSize - 1;
int left = 0;
for (; left < right; right--, left++) {
swap(s + left, s + right);
}
}
.338.比特位计数
给定一个非负整数 num。对于 0 ≤ i ≤ num 范围中的每个数字 i ,计算其二进制数中的 1 的数目并将它们作为数组返回。
/* 获取非负数其二进制数中的 1 的个数 */
int getNumOfOne(int num)
{
int cnt = 0;
while(num) {
cnt += num & 1; // 判断 num 最右一位是否为 11 ,根据结果计数
num = num >> 1;
}
return cnt;
}
10: 0x1010
9: 0x1001
10 & 9 == 0x1000 – 把n 最右边1变成0;
int getNumOfOne(int num)
{
int cnt = 0;
while(num) {
cnt++;
num = num & (num - 1); // 二进制数字 nn 最右边的 11 变成 00 ,其余不变
}
return cnt;
}
48.旋转图像
给定一个 n × n 的二维矩阵表示一个图像。
将图像顺时针旋转 90 度。
你必须在原地旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要使用另一个矩阵来旋转图像。
73. 矩阵置零
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
进阶:
一个直观的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。
一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。
你能想出一个仅使用常量空间的解决方案吗?
思路:
第一次遍历时,使用标记数组,记录哪行哪列出现0值
第二次遍历时,根据标记数组清0
void setZeroes(int **matrix, int matrixSize, int *matrixColSize) {
int m = matrixSize; // matrixSize 是该矩阵的行数
int n = matrixColSize[0]; // matrixColSize[0] 是该矩阵的列数
int row[m], col[n];
memset(row, 0, sizeof(row));
memset(col, 0, sizeof(col));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (!matrix[i][j]) { // 使用标记数组,记录行列值
row[i] = 1;
col[j] = 1;
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (row[i] || col[j]) {
matrix[i][j] = 0;
}
}
}
}
789. 逃脱阻碍者
思路:此题看上去像是一道 DFS 的题,但实际上由于ghosts也能动,所以会有点想不明白具体怎么解。
其实只需要比较 “我”和 Ghosts 各自到 target 的距离即可,如果是都是小于就可以在ghosts之前出去。
这个距离称之为曼哈顿距离。。。虽然名字有点奇怪。
题目中给出了target为一维有两个的数组;
ghosts为二维数组,每个也有两个;
int manhattanDistance(int *point1, int *point2)
{
return fabs(point1[0] - point2[0]) + fabs(point1[1] - point2[1]);
}
bool escapeGhosts(int **ghosts, int ghostsSize, int *ghostsColSize, int *target, int targetSize) {
int source[2] = { 0, 0 };
int distance = manhattanDistance(source, target);
for (int i = 0; i < ghostsSize; i++) {
int ghostDistance = manhattanDistance(ghosts[i], target);
if (ghostDistance <= distance) {
return false;
}
}
return true;
}
390. 消除游戏
列表 arr 由在范围 [1, n] 中的所有整数组成,并按严格递增排序。请你对 arr 应用下述算法:
从左到右,删除第一个数字,然后每隔一个数字删除一个,直到到达列表末尾。
重复上面的步骤,但这次是从右到左。也就是,删除最右侧的数字,然后剩下的数字每隔一个删除一个。
不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。
给你整数 n ,返回 arr 最后剩下的数字。
int lastRemaining(int n)
{
int head = 1;
int cnt = n, step = 1;
bool dir = true; // true表示正向
while (cnt > 1) {
// 从左边开始移除 or(从右边开始移除,数列总数为奇数)
if (dir || cnt % 2 == 1) { // dir为false表示反向,只有反向且 cnt % 2 == 0 时不更新head
head = head + step;
}
cnt = cnt / 2; // 步长 * 2
step = step * 2; // 总数 / 2
dir = !dir; // 取反移除方向
}
// 每个回合更新和记录head变量,当数组的总数变为1时,head就是最后的一个数
return head;
}
上述过程需要自己以一个实例进行推导几次,才能体会,体会后就是上述的过程。
参考:https://leetcode-cn.com/problems/elimination-game/solution/wo-hua-yi-bian-jiu-kan-dong-de-ti-jie-ni-k2uj/
1185. 一周中的第几天
题目有个知识应该被告知:
1970年最后一天是周四 – 已知的先验知识
题目保证日期是在 1971 到 2100 之间,我们可以计算给定日期距离 1970 的最后一天(星期四)间隔了多少天,从而得知给定日期是周几。
具体的,可以先通过循环处理计算年份在 [1971, year - 1] 时间段,经过了多少天(注意平年为 365,闰年为 366);然后再处理当前年 yearyear 的月份在 [1, month - 1] 时间段 ,经过了多少天(注意当天年是否为闰年,特殊处理 2 月份),最后计算当前月 month 经过了多少天,即再增加 day 天。
得到距离 1970 的最后一天(星期四)的天数后进行取模,即可映射回答案。
// 题目保证日期是在 1971 到 2100 之间,我们可以计算给定日期距离 1970 的最后一天(星期四)间隔了多少天,从而得知给定日期是周几
char *dayOfTheWeek(int day, int month, int year)
{
const char *Day[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
int Month[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
int res = 4; // 1970年最后一天是周四 -- 已知的先验知识
// 可以先通过循环处理计算年份在 [1971, year - 1] 时间段,经过了多少天(注意平年为 365,闰年为 366)
for (int i = 1971; i < year; i++) {
bool isLeap = (i % 4 == 0 && i % 100 != 0) || i % 400 == 0;
res += isLeap ? 366 : 365;
}
// 然后再处理当前年 year 的月份在 [1,month−1] 时间段
// 经过了多少天(注意当天年是否为闰年,特殊处理 2 月份),最后计算当前月 month 经过了多少天,即再增加 day 天。
for (int i = 1; i < month; i++) {
res += Month[i - 1];
if (i == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) {
res++;
}
}
res += day;
res %= 7;
return (char *)Day[res];
}
6368. 找出字符串的可整除数组
错误答题:
想这种题目,大概率会溢出,需要数学推演来找规律:
// 6368. 找出字符串的可整除数组
long long judge_num(long long num, long long m)
{
long long res_s = num % m;
return res_s == 0;
}
/**
* Note: The returned array must be malloced, assume caller calls free().
*/
int *divisibilityArray(char *word, int m, int *returnSize)
{
int right = 0, left = strlen(word);
int i;
int res = 0;
char *tmp = (char *)malloc(sizeof(char) * strlen(word) + 1);
int *cnt = (int *)malloc(sizeof(int) * strlen(word) + 1);
memset(tmp, 0, sizeof(char) * strlen(word) + 1);
memset(cnt, 0, sizeof(int) * strlen(word) + 1);
*returnSize = strlen(word);
for (i = right; i < left; i++) {
memset(tmp, 0, sizeof(char) * strlen(word) + 1);
memcpy((char *)tmp, (char *)word, (i + 1) * sizeof(char));
tmp[strlen(tmp)] = '\0';
res = judge_num(atoll(tmp), (long)m);
if (res == 1) {
cnt[i] = 1;
}
}
return cnt;
}
如上为错误答案,会报:(测试用例数字太大了)
借鉴如下思路,每次向后增加一位时, 我们只考虑前面的余数部分, 这样是不会影响结果的。
int *divisibilityArray(char *word, int m, int *returnSize)
{
int right = 0, left = strlen(word);
long long res = 0;
int result ,i = 0;
int *cnt = (int *)malloc(sizeof(int) * strlen(word) + 1);
memset(cnt, 0, sizeof(int) * strlen(word) + 1);
*returnSize = strlen(word);
for (i = right; i < left; i++) {
res = res * 10 + (word[i] - '0');
result = (res % m == 0);
if (result == 1) {
cnt[i] = 1;
}
// 每次向后增加一位时, 我们只考虑前面的余数部分, 这样是不会影响结果的
// 只保留余数部分
res = res % m;
}
return cnt;
}
1247. 交换字符使得字符串相同
数学推演,找规律~
因此进行计数,数出s1[i],s2[i]的(‘x’, ‘y’) 和(‘y’, ‘x’) 的数量,分别计入cntxy和cntyx变量中。 按贪心思路:
(1)cntxy和cntyx均是偶数,则分别各自成对交换完成,返回:cntxy//2 + cntyx//2
(2)cntxy和cntyx均是奇数,则剩余一个(‘x’, ‘y’) 和一个(‘y’, ‘x’)需要交换两次,返回:cntxy//2 + cntyx//2 + 2
(3)cntxy和cntyx一奇一偶,则最后会剩余一个(‘x’, ‘y’) 或一个(‘y’, ‘x’)无法完成交换,返回:-1
cntxy + cntyx == 奇数一定不行,返回 -1;
int minimumSwap(char * s1, char * s2){
short cntxy = 0, cntyx = 0;
for (short i = 0; s1[i]; ++ i)
if (s1[i] == 'x' && s2[i] == 'y') ++ cntxy;
else if (s1[i] == 'y' && s2[i] == 'x') ++ cntyx;
return (cntxy & 1) == (cntyx & 1) ? (cntxy >> 1) + (cntyx >> 1) + ((cntxy & 1) << 1) : -1;
}
面试题 05.02. 二进制数转字符串
#define MAX(a, b) ((a) > (b) ? (a) : (b))
char *printBin(double num)
{
char *res = (char *)malloc(sizeof(int) * 33);
int pos = 0;
int digit;
pos += sprintf(res, "%s", "0.");
while (pos <= 32 && num != 0) {
num *= 2;
digit = num;
res[pos++] = digit + '0';
num -= digit;
}
res[pos] = '\0';
if (pos > 32) {
free(res);
return "ERROR";
}
return res;
}