数组
二分查找
二分查找有两种写法:
1.左闭右闭:[left, right]
- 在这种写法中,【left,right】在left==right依然有意义。所以while循环条件写成(left<=right)
- 然后用三种条件的if/else语句写。if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1
2.左闭右开:[left, right)
- 循环条件就要写成while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的
- if (nums[middle] > target) right 更新为 middle。因为下一个查询区间不会去比较nums[middle](左闭右开)
查找成功的条件放最后,时间复杂度低一点
数组创建
二维数组
int**可以理解为二维数组。在创建int** 类型的变量时,需要用malloc分配内存,同时对每一行数组分配内存。以int[6][3]为例:
int** ans = (int**)malloc(sizeof(int*) * 6);
for(int m = 0; m < 6; m++){
ans[m] = (int *)malloc(sizeof(int) * 3);
}
char**数组可以理解为字符串数组。例如
tokens = ["2","1","+","3","*"]
这个数组 tokens
中的每个元素都是字符指针(char*
),指向一个以 null 结尾的字符串。虽然有些元素可能只包含一个字符,但在 C 语言中,字符也被表示为以 null 结尾的字符串。因此,在这个数组中,每个元素都是一个指向字符数组的指针,表示一个字符串。
对于该数组,"2"
是一个指向以字符 '2' 和 null 终止符组成的字符数组的指针(即指向字符串 "2" 的指针)。后面以此类推。
calloc函数
calloc函数用法:
int arraySize = 10; // 数组大小
char *charArray; // 指向char数组的指针
// 使用calloc分配内存,并自动初始化为零
charArray = (char *)calloc(arraySize, sizeof(char));
和malloc函数的区别是,calloc函数会将所有元素初始化为0,这导致在大块内存分配时,calloc要慢于malloc。
无论使用哪种函数,记得在使用完内存后使用free
来释放分配的内存,以避免内存泄漏问题。
char数组
快速排序
qsort
qsort
是 C 标准库中提供的一个快速排序函数。函数用法如下:
qsort(nums, numsSize, sizeof(int), cmp);
nums
:指向要排序的数组的第一个元素的指针(数组名)。numsize
:数组中的元素数量。sizeof
:每个元素的大小(以字节为单位)。cmp
:辅助函数,比较函数的指针,用于指定元素之间的比较规则。
cmp函数如下(升序写法):
int cmp(const void* a, const void* b){
return *((int*)a) > *((int*)b);
}
如果 compar
返回一个负值,表示第一个元素小于第二个元素;如果返回零,表示两个元素相等;如果返回一个正值,表示第一个元素大于第二个元素。
数组长度
在C语言中:
int类型数组s,可以用sizeof(s) / sizeof(int)来计算。int数组不以 '\0' 结尾。
char类型数组s,可以用strlen(s)来计算(不包括‘\0’)。因为char数组以 '\0' 结尾。在使用malloc函数为char数组分配内存时,还需要考虑加一个 '\0' 的大小。
在C++中:
提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用'\0'来判断是否结束。
例如这段代码:
string a = "asd";
for (int i = 0; i < a.size(); i++) {
}
在LeetCode中数组返回长度用*returnSize来传递,注意通过循环递增*returnSize的时候,要写成:
(*returnsize)++
不然会报错stack-buffer-overflow
链表
虚拟头节点(增删元素)
删除链表中的第一个元素和中间的元素逻辑是不一样的。因为删除第一个元素还要改变链表的头节点。
为了统一逻辑,需要虚拟一个头节点。如果删除的是第一个元素,在代码最后改变头节点的位置为虚拟头节点的下一个即可。
struct ListNode* removeElements(struct ListNode* head, int val){
typedef struct ListNode ListNode;
ListNode* shead = (struct ListNode*)malloc(sizeof(struct ListNode));
shead->next = head;
ListNode* cur = shead;
while(cur->next != NULL){
if(cur->next->val == val){
ListNode* tmp = cur->next;
cur->next = cur->next->next;
free(tmp);
}else{
cur = cur->next;
}
}
head = shead->next;
free(shead);
return head;
}
哈希表
哈希表和链表的联系
- 理想情况下,插入、删除和查找操作的时间复杂度都是 O(1)。
- 一般哈希表都是用来快速判断一个元素是否出现集合里。
- 遍历哈希表其实可以理解成遍历链表。
哈希表结构
struct HashTable{
int key;
int val;
UT_hash_handle hh;
};
我的个人习惯,为了后续方便,可以在最前面加上:
typedef struct HashTable hashtable;
哈西表处理常用的宏
在使用这些宏之前需要引入‘uthash’库函数
#include "uthash.h"
1.HASH_FIND_INT:查找具有整数键的元素(参数解释如下)
HASH_FIND_INT(hashTable, &searchKey, result);
- 哈希表的指针(在本例中是
hashTable
)。 - 指向要查找的整数键的指针(在本例中是
&searchKey
)。 - 一个指针变量,用于存储查找结果。
2.HASH_ADD_INT:添加
HASH_ADD_INT(hashTable, key, newElement);
3.HASH_ITER:用于迭代遍历整个哈希表,执行自定义操作。
struct HashTable *currentElement, *tmp;
HASH_ITER(hh, hashTable, currentElement, tmp) {
// 对当前元素执行操作
}
4.HASH_DEL:用于从哈希表中删除一个元素。
HASH_DEL(hashTable, elementToDelete);
三数之和
关键:
- 使用快排+双指针
- 遍历排序后的数组,确定第一个数,移动指针确定后两个数
- 最终结果的几种可能可以看作排列组合,n个里面选3个,但不是Cn3,而是Cn2,因为在确定前两个数值后,最后一个数值是固定的。而这里申请内存的时候理论值应该是 :numsSize * (numsSize -1);代码中的设计:
int **ans = (int **)malloc(sizeof(int *) * numsSize *numsSize);
去重的写法:
a:
问题在于,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同
如果写法是前者,以{-1, -1 ,2} 这组数据为例,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。
我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!
所以写法应该是后者,我们判断前一位是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到结果集里。
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
b、c:
while(middle < right && nums[middle] == nums[++middle]);
while(middle < right && nums[right] == nums[--right]);
字符串
反转字符串 ||
给定一个字符串 s
和一个整数 k
,从字符串开头算起,每计数至 2k
个字符,就反转这 2k
字符中的前 k
个字符。
- 如果剩余字符少于
k
个,则将剩余字符全部反转。 - 如果剩余字符小于
2k
但大于或等于k
个,则反转前k
个字符,其余字符保持原样。
思路:
因为要找的也就是每2 * k 区间的起点,所以在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。
当需要固定规律一段一段去处理字符串的时候,要想想在for循环的表达式上做做文章。
替换空格
很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后再从后向前进行操作。
这就需要确定扩充后数组的长度。
优点:
- 不用申请新数组。
- 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。
KMP算法
i:后缀末尾, j:最长共同前缀末尾
以下为next数组不统一减1的代码:
void GetNext(int* next, char* p){
next[0] = 0;
int j = 0;
int len = strlen(p);
for(int i = 1; i < len; i++){
j = next[i - 1];
//不断递归判断是否存在子对称,k=0说明不再有子对称,Pattern[i] != Pattern[k]说明虽然对称,但是对称后面的值和当前的字符值不相等,所以继续递推
while(p[i] != p[j] && j > 0){
j = next[j - 1]; //继续递归
}
if(p[i] == p[j]){
next[i] = j + 1; //找到了这个子对称,j又表示最大共同前后缀长度,在此基础上长度+1
}else{
next[i] = 0; //如果遍历了所有子对称都无效,说明这个新字符不具有对称性,清0
}
}
}
对应的kmp代码(返回第一次出现p的第一个字母下标):
int strStr(char * s, char * p){
int lens = strlen(s);
int lenp = strlen(p);
int* next = (int*)malloc(sizeof(int) * lenp);
GetNext(next, p);
int i = 0, j = 0;
for(; i < lens; i++){
while(j > 0 && s[i] != p[j]){ //同样的循环条件
j = next[j - 1]; //一直往前找到最大相同前后缀
}
if(s[i] == p[j]){
j++;
}
if(j == lenp) return i - j + 1;
}
return -1;
}
常见错误及解决办法:
1.control reaches end of non-void function
解决方法:没有默认返回值,在函数末尾添加返回值如return 0;