刷题心得01 前缀和、差分数组、双指针

目录

前言

一、前缀和

1.核心思想

2.用处

3.代码模板

4.例题分析

例一        303. 区域和检索 - 数组不可变

例二        最后 K 个数的乘积

例三        1314. 矩阵区域和

二、差分数组 

1.核心思想

2.用处

3.代码模板

4.例题分析

例一        370. 区间加法

例二        1094. 拼车

三、双指针

1.核心思想

2.用处

3.例题分析

例一        876. 链表的中间结点

例二        141. 环形链表

例三        142. 环形链表 II

例四        234. 回文链表

例五        26. 删除排序数组中的重复项

例六        80. 删除排序数组中的重复项 II

例七        283. 移动零

例八        76. 最小覆盖子串

例九        567. 字符串的排序

例十        3. 无重复字符的最长子串

例十一        21. 合并两个有序链表

例十二        23. 合并K个排序链表

例十三        264. 丑数 II

例十四        19. 删除链表的倒数第N个节点

例十五        160.相交链表

例十六        704. 二分查找

例十七        74. 搜索二维矩阵

例十八        875. 爱吃香蕉的珂珂

例十九        658. 找到 K 个最接近的元素

例二十        378.有序矩阵中第K小的元素

例二十一        15. 三数之和

例二十二        151. 翻转字符串里的单词

例二十三        42. 接雨水

例二十四        5.最长回文子串



前言

        该系列博客旨在记录我的刷题心得和一些解题技巧,题目全部来源于力扣,一些技巧和方法参考过力扣上的题解和labuladong大佬的文章。所有代码使用C语言,一些辅助函数(比如ADT的基本操作集)的具体实现在此省略,但会保留最核心的辅助函数的代码。虽然说这些内容主要是写给我自己看的,但也欢迎大家发表自己新颖的解法和不一样的观点。


一、前缀和

1.核心思想

        把数组nums索引前的累加和保存在一个新的presum数组中。presum[i]为nums[0...i-1]的和。

2.用处

        提高对某个索引区间内的元素求和的效率,把时间复杂度从O(N)降为O(1)。

3.代码模板

        

//假设需对长度为N的nums数组在[i,j]区间内求和
int presum[N+1];
presum[0]=0;          //避免对边界的讨论
for(int i=1;i<N+1;i++){
    presum[i]=presum[i-1]+nums[i-1];
}

int sumRange(int i, int j){
    return presum[j+1]-presum[i];
}

4.例题分析

例一        303. 区域和检索 - 数组不可变

直接考察前缀和,套模板就行了。

typedef struct {
    int *presum;
    int size;
} NumArray;

NumArray* numArrayCreate(int* nums, int numsSize) {
    NumArray *NArray=malloc(sizeof(NumArray));
    NArray->size=numsSize;
    NArray->presum=malloc(sizeof(int)*(numsSize+1));
    NArray->presum[0]=0;
    for(int i=1;i<numsSize+1;i++){
        NArray->presum[i]=NArray->presum[i-1]+nums[i-1];
    }
    return NArray;
}

int numArraySumRange(NumArray* obj, int left, int right) {
    return obj->presum[right+1]-obj->presum[left];
}

例二        最后 K 个数的乘积

本题只不过把前缀和改为了前缀积而已,对0做特殊处理即可。

typedef struct {
    int num[40000];
    int premult[40001];
    int rear;
} ProductOfNumbers;


ProductOfNumbers* productOfNumbersCreate() {
    ProductOfNumbers *obj;
    obj=malloc(sizeof(ProductOfNumbers));
    obj->rear=0;
    obj->premult[0]=1;
    return obj;
}

void productOfNumbersAdd(ProductOfNumbers* obj, int num) {
    if(num==0){
        obj->rear=0;        //遇0清空premult数组
    }else{
        obj->num[obj->rear++]=num;
        obj->premult[obj->rear]=obj->premult[obj->rear-1]*obj->num[obj->rear-1];
    }
}

int productOfNumbersGetProduct(ProductOfNumbers* obj, int k) {
    if(obj->rear<k)        //不足k个数,说明相乘所有数中有0,返回0
        return 0;
    else
        return obj->premult[obj->rear]/obj->premult[obj->rear-k];
}

例三        1314. 矩阵区域和

如何把一个一维的前缀和推广为二维形式呢?

如下图,红色区域的和可以等价为4个从(0,0)开始的区域和相加减。因此建立一个presum[row+1][col+1]数组,presum[i][j]代表从原点(0,0)到(i-1,j-1)区域内所有元素的和。当计算presum[i][j](相当于绿色区域)的值时,把橙、蓝、紫三个区域移到等号左边即可。

int** matrixBlockSum(int** mat, int matSize, int* matColSize, int k, int* returnSize, int** returnColumnSizes){
    int m=matSize;
    int n=matColSize[0];
    int presum[m+1][n+1];
    for(int i=0;i<m+1;i++){
        presum[i][0]=0;
    }
    for(int j=0;j<n+1;j++){
        presum[0][j]=0;
    }
    for(int i=1;i<m+1;i++){
        for(int j=1;j<n+1;j++){
            //绿=蓝+橙+红-紫
            presum[i][j]=presum[i-1][j]+presum[i][j-1]+mat[i-1][j-1]-presum[i-1][j-1];
        }
    }
    int **answer;
    answer=malloc(sizeof(int *)*m);
    returnColumnSizes[0]=malloc(sizeof(int)*m);
    for(int i=0;i<m;i++){
        answer[i]=malloc(sizeof(int)*n);
        returnColumnSizes[0][i]=n;
        int row1=i-k;
        int row2=i+k;
        if(row1<0)
            row1=0;
        if(row2>=m)
            row2=m-1;
        for(int j=0;j<n;j++){
            int col1=j-k;
            int col2=j+k;
            if(col1<0)
                col1=0;
            if(col2>=n)
                col2=n-1;
            answer[i][j]=presum[row2+1][col2+1]-presum[row1][col2+1]-presum[row2+1][col1]+presum[row1][col1];
        }
    }
    *returnSize=m;

    return answer;
}

二、差分数组 

1.核心思想

把nums数组前后两个数的差保存在diff中,diff[i]=nums[i]-nums[i-1],nums[i]=diff[0...i]的累加

2.用处

提高对nums数组某个区间内元素共同增减的效率,把for循环的O(N)时间复杂度将为O(1)

3.代码模板

//假设需对长度为N的nums数组在[i,j]区间内加val 
int diff[N];
diff[0]=0;          
for(int i=1;i<N;i++){
    diff[i]=nums[i]-nums[i-1];
}

void changeRange(int i, int j){
    diff[i]+=val;
    if(j+1<N)
    	diff[j+1]-=val;
}
//返回结果数组
int *result(){
	int res[N];
    //res也可看为diff的前缀和
	res[0]=diff[0];
	for(int i=1;i<N;i++){
		res[i]=res[i-1]+diff[i];
	}
	return res;
}

4.例题分析

例一        370. 区间加法

直接套模板即可,注意一开始数组内所有元素为0,因此diff的所有元素初始化为0

int* getModifiedArray(int length, int** updates, int updatesSize, int* updatesColSize, int* returnSize){
    int diff[length];
    for(int i=0;i<length;i++){
        diff[i]=0;
    }
    for(int i=0;i<updatesSize;i++){
        diff[updates[i][0]]+=updates[i][2];
        if(updates[i][1]+1<length){
            diff[updates[i][1]+1]-=updates[i][2];
        }
    }
    int *ans=malloc(sizeof(int)*length);
    ans[0]=diff[0];
    for(int i=1;i<length;i++){
        ans[i]=ans[i-1]+diff[i];
    }
    *returnSize=length;
    return ans;
}

 例二        1094. 拼车

把本题与上一题对照着看,numPassengersi相当于要加的val,fromi和toi相当于要加的区间的两端,当数组内某个元素的值大于capacity时,说明超载了。这里有个细节:乘客在fromi上车,在toi下车,因此真正的区间为[fromi,toi-1]

bool carPooling(int** trips, int tripsSize, int* tripsColSize, int capacity){
   int diff[1000];
   for(int i=0;i<1000;i++){
       diff[i]=0;
   } 
   for(int i=0;i<tripsSize;i++){
       int num=trips[i][0];
       int from=trips[i][1];
       int to=trips[i][2]-1;
       diff[from]+=num;
       if(to+1<1000)
        diff[to+1]-=num;
   }
   int res[1000];
   res[0]=diff[0];
   if(res[0]>capacity)
    return 0;
    for(int i=1;i<1000;i++){
        res[i]=res[i-1]+diff[i];
        if(res[i]>capacity)
            return 0;
    }
    return 1;
}

三、双指针

1.核心思想

        双指针顾名思义,就是同时使用两个指针,在序列、链表结构上指向的是位置,在树、图结构中指向的是节点,通过或同向移动,或相向移动来维护、统计信息。

2.用处

        使用双指针技巧的题目的一大特征为有序(单调),只要数据是有序的,就可以考虑双指针。思维导图如下:

3.例题分析

例一        876. 链表的中间结点

使用两个指针,开始指向链表的虚拟头结点,每次让fast指针走两步,slow指针走一步,当fast走到链表结尾时,slow走到链表中间位置,当链表长度(不算虚拟头结点)为偶数时,slow走到靠前的那个结点

typedef struct ListNode* List;
struct ListNode* middleNode(struct ListNode* head){
    List list,slow,fast;
    list=malloc(sizeof(struct ListNode));
    list->next=head;
    slow=fast=list;
    while(fast!=NULL&&fast->next!=NULL){
        slow=slow->next;
        fast=fast->next->next;
    }
    return slow;
}

例二        141. 环形链表

同样用快慢指针,若快慢指针能相遇,则包含环

typedef struct ListNode* List;
bool hasCycle(struct ListNode *head) {
    List list,fast,slow;
    list=malloc(sizeof(struct ListNode));
    list->next=head;
    fast=slow=list;
    while(fast!=NULL&&fast->next!=NULL){
        slow=slow->next;
        fast=fast->next->next;
        if(fast==slow)
            return 1;
    }
    return 0;
}

例三        142. 环形链表 II

 本题要找环的起点,延续上题思路,用快慢指针。假设从虚拟头结点开始走x步到环的起点,环长为c,两指针第一次相遇时慢指针一共走了k步,则快指针走了2k步,于是慢指针在环上走了k-x步,快指针走了2k-x步,快指针比慢指针多走一圈:2k-x=k-x+c,所以k=c,把慢指针移到虚拟头结点,让两者一步一步走,当慢指针走x步时,快指针在环上共走2k-x+x=2c步,刚好回到环的起点与慢指针相遇。

typedef struct ListNode* List;
struct ListNode *detectCycle(struct ListNode *head) {
    List list,fast,slow;
    list=malloc(sizeof(struct ListNode));
    list->next=head;
    fast=slow=list;
    while(fast&&fast->next){
        slow=slow->next;
        fast=fast->next->next;
        if(fast==slow)
            break;
    }
    if(fast==NULL||fast->next==NULL)
        return NULL;
    slow=list;
    while(fast!=slow){
        slow=slow->next;
        fast=fast->next;
    }
    return slow;
}

例四        234. 回文链表

 本题涉及两步操作,第一步寻找链表中点的后一个结点(偶数情况下就是靠后的那个结点),第二步反转该点以后的整条链表。先来看看如何反转整条链表,即力扣206题

typedef struct ListNode* List;
struct ListNode* reverseList(struct ListNode* head){
    if(head==NULL)             //空链表不需要反转
        return head;
    List preNode=NULL;
    List curNode=head;
    List nextNode=head->next;
    while(nextNode!=NULL){
        curNode->next=preNode;
        preNode=curNode;
        curNode=nextNode;
        nextNode=nextNode->next;
    }
    //退出时preNode与curNode之间是断开的,要连上
    curNode->next=preNode;
    return curNode;
}

以上是迭代版本,递归版本 点这里

bool isPalindrome(struct ListNode* head) {
    List slow,fast,list;
    list=malloc(sizeof(struct ListNode));
    list->next=head;
    slow=fast=list;
    while(fast!=NULL&&fast->next!=NULL){
        slow=slow->next;
        fast=fast->next->next;
    }
    slow=slow->next;
    slow=reverseList(slow);        //把slow指向反转后链表的头结点
    while(slow){
        if(head->val!=slow->val)
            return 0;
        head=head->next;
        slow=slow->next;
    }
    return 1;
}

例五        26. 删除排序数组中的重复项

同样用快慢指针,[0,slow)区间内无重复元素,[fast,numsSize)内存在重复元素,让fast指针往后遍历,若无重复,则移动slow扩大区间。开始令slow=1,使区间内有一个元素,slow-1为区间内最后一个元素的位置,循环退出时slow的大小即为数组的长度。

int removeDuplicates(int* nums, int numsSize){
    int fast,slow;
    slow=1;
    fast=1;
    while(fast<numsSize){
        if(nums[slow-1]!=nums[fast]){
            nums[slow]=nums[fast];
            slow++;
        }
        fast++;
    }
    return slow;
}

 例六        80. 删除排序数组中的重复项 II

 这题是上一题的升级版,允许一个元素重复2次。同样假设[0,slow)为符合条件的区间,让fast向后遍历。但注意每次要让fast与slow-2做比较,如果相等,说明所指的元素已经出现了两次。

int removeDuplicates(int* nums, int numsSize){
    int slow=2,fast=2;
    while(fast<numsSize){
        if(nums[slow-2]!=nums[fast]){
            nums[slow]=nums[fast];
            slow++;
        }
        fast++;
    }
    if(numsSize<2){
        return numsSize;
    }else{
        return slow;
    }
}

例七        283. 移动零

看到这道题我原本的反应是想如何把0移到后面,用swap吗?但其实不需要这么麻烦,把问题稍微转换一下。

void moveZeroes(int* nums, int numsSize){
    int slow=0,fast=0;                 //维护一个[0,slow)区间,区间内无0元素
    while(fast<numsSize){              //删除0元素
        if(nums[fast]!=0){
            nums[slow]=nums[fast];
            slow++;
        }
        fast++;
    }                                  
    for(int i=slow;i<numsSize;i++){   //循环退出时slow指向第一个0元素,把slow之后的值赋为0即可
        nums[i]=0;
    }
}

 例八        76. 最小覆盖子串

先简单介绍下滑动窗口技巧

1.核心思想

用左右指针维护窗口内的信息,新增一个或着删去一个元素后都很方便更新信息

2.用处

主要用来解决子串问题

3.代码模板

int left=0,right=0         //[0,left)为窗口区间,初始窗口内无元素
while(right<strlen(s)){
    char c1=s[right];
    right++;
    //信息更新
    while(窗口需要收缩){
        //若满足条件,信息更新
        char c2=s[left];
        left++;
    }
    //若满足条件,信息更新
}

回到题目本身,对于s中的每个字符c,如何判断c出现在t中,且已经出现了几次呢?这里有另一个处理字符串问题的常用技巧:哈希表。建立一个map[256]的整形数组,数组下标代表字符,值代表出现次数。该题不限制顺序和间距,因此当t的每个字符都被找到时收缩窗口。

char * minWindow(char * s, char * t){
    int left=0,right=0;
    int map[256]={0};
    int cnt=0;                //记录满足条件的字符数目
    int len=0,min=strlen(s);
    int start=0;
    for(int i=0;i<strlen(t);i++){      //建立哈希表
        map[t[i]]++;
    }
    while(right<strlen(s)){
        if(map[s[right++]]-->0)        //把模板里三步合为一步
            cnt++;
        while(cnt==strlen(t)){
            //更新信息,[left,right)区间为满足条件的区间
            len=right-left;
            if(min>len){
                min=len;
                start=left;
            }
            if(++map[s[left++]]>0)
                cnt--;
        }
    }
    if(len==0)
        return "";
    char *c;
    c=malloc(sizeof(char)*(min+1));
    for(int i=start;i<start+min;i++){
        c[i-start]=s[i];
    }
    c[min]='\0';
    return c;
}

 例九        567. 字符串的排序

依然用滑动窗口和哈希表,但此题不要求字串顺序,但要无间隔,因此当窗口的长度等于strlen(s1)时收缩(大于的话肯定不满足条件了)。

bool checkInclusion(char * s1, char * s2){
    int map[256]={0};
    int left=0,right=0;
    int cnt=0;
    for(int i=0;i<strlen(s1);i++){
        map[s1[i]]++;
    }
    while(right<strlen(s2)){
        if(map[s2[right++]]-->0)
            cnt++;
        while(right-left==strlen(s1)){
            if(cnt==strlen(s1))
                return 1;
            if(++map[s2[left++]]>0)
                cnt--;
        }
    }
    return 0;
}

 例十        3. 无重复字符的最长子串

本题要求找最长的子串,应当在窗口内元素不满足条件(有重复元素)时收缩,外层while中窗口是始终满足条件的,因此在外层循环更新max

int lengthOfLongestSubstring(char * s){
    int left=0,right=0;
    int len=0,max=0;
    int map[256]={0};
    while(right<strlen(s)){
        map[s[right++]]++;
        while(map[s[right-1]]>1){
            map[s[left++]]--;
        }
        len=right-left;
        if(max<len)
        max=len;
    }
    return max;
}

例十一        21. 合并两个有序链表

每次比较l1,l2两个指针所指的结点的值的大小,把较小的结点接到新的结果链表中

typedef struct ListNode* List;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2){
    List res,rear,l1,l2;
    res=malloc(sizeof(struct ListNode));
    res->next=NULL;
    rear=res;
    l1=list1;
    l2=list2;
    while(l1!=NULL||l2!=NULL){
        List t;
        t=malloc(sizeof(struct ListNode));
        t->next=NULL;
        if(l2!=NULL&&(l1==NULL||l2->val<l1->val)){
            t->val=l2->val;
            rear->next=t;
            l2=l2->next;
        }else{
            t->val=l1->val;
            rear->next=t;
            l1=l1->next;
        }
        rear=rear->next;
    }
    return res->next;
}

 例十二        23. 合并K个排序链表

本题更多的还是考查对优先队列(堆)的使用,把K个链表的所以结点放入堆中,依次弹出即可

struct ListNode* mergeKLists(struct ListNode** lists, int listsSize){
    MinHeap H=CreatHeap(listsSize*500);
    List res,rear;
    res=malloc(sizeof(struct ListNode));
    res->next=NULL;
    rear=res;
    for(int i=0;i<listsSize;i++){
        List l=lists[i];
        while(l!=NULL){
            Insert(H,l);
            l=l->next;
        }
    }
    while(rear!=NULL){
        rear->next=Delete(H);
        rear=rear->next;
    }
    return res->next;
}

例十三        264. 丑数 II

本题官方的解答用到了动态规划,其实这种解法依然可以理解为合并K个升序链表。详细内容见我的题解

 例十四        19. 删除链表的倒数第N个节点

双指针还提供了一种只要一次遍历就能找到倒数第N个节点的方法。先让p1从虚拟头结点开始走N步,此后p1,p2同时走,直到p1走到链表结尾,p2所指的位置就是答案

typedef struct ListNode* List;
struct ListNode* removeNthFromEnd(struct ListNode* head, int n){
    List list,p1,p2;
    list=malloc(sizeof(struct ListNode));
    list->next=head;
    p1=p2=list;
    while(n--){
        p1=p1->next;
    }
    while(p1->next){        //p2要指到倒数第N个节点的前一个位置,这样才能删除,因此要少走一步
        p1=p1->next;
        p2=p2->next;
    }
    List tmp=p2->next;
    p2->next=tmp->next;
    free(tmp);
    return list->next;
}

例十五        160.相交链表

本题的关键而且难点在于如何让两个指针同时走到交点位置。当指针走到链表结尾时,跳转到另一条链表的头结点继续走,若两个指针能相遇,则有交点,相遇位置即为交点位置

typedef struct ListNode *List;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {
    List pA=headA;
    List pB=headB;
    while(pA!=pB){
        if(pA==NULL)
            pA=headB;
        else
            pA=pA->next;
        if(pB==NULL)
            pB=headA;
        else
            pB=pB->next;
    }
    return pA;
}

例十六        704. 二分查找

先说说我对二分法的感悟,刚开始学算法的小伙伴可能会认为二分法就是在一串有序数据中高效搜索的算法,但二分法的思想却不局限于此,我们可以用它解决很多最值问题。下面给出3大查找模板:

查找某个数:

//假设nums是个升序的数组
int left=0,right=numsSize-1;
while(left<=right){
    int mid=left+(right-left)/2;    //防止溢出
    if(nums[mid]==target){
        return mid;
    }else if(nums[mid]>target){
        right=mid-1;
    }else{
        left=mid+1;
    }
}
return -1;

查找左边界(假设升序):

1.返回的值是数组中>=target的最小元素索引

2.返回的值是target在数组中的插入位置

3.返回的值是数组中<target的元素个数

//假设nums是个升序的数组
int left=0,right=numsSize-1;
while(left<=right){
    int mid=left+(right-left)/2;    //防止溢出
    if(nums[mid]==target){
        right=mid-1;
    }else if(nums[mid]>target){
        right=mid-1;
    }else{
        left=mid+1;
    }
}
return left;       //注意此时left可能等于numSize越界

查找右边界(假设升序):

1.返回的值是数组中<=target的最大元素索引

3.返回的值是数组中<=target的元素个数-1

//假设nums是个升序的数组
int left=0,right=numsSize-1;
while(left<=right){
    int mid=left+(right-left)/2;    //防止溢出
    if(nums[mid]==target){
        left=mid+1;
    }else if(nums[mid]>target){
        right=mid-1;
    }else{
        left=mid+1;
    }
}
return right;       //注意此时right可能等于-1越界

例十七        74. 搜索二维矩阵

对一个m*n的矩阵,可以视为一个长度为m*n的有序一维数组。若一维数组索引为i,对应到矩阵中的行数row=i/n,列数col=i%n

bool searchMatrix(int** matrix, int matrixSize, int* matrixColSize, int target){
    int left=0,right=matrixSize*matrixColSize[0]-1;
    while(left<=right){
        int mid=left+(right-left)/2;
        int row=mid/matrixColSize[0];
        int col=mid%matrixColSize[0];
        if(matrix[row][col]==target){
            return true;
        }else if(matrix[row][col]>target){
            right=mid-1;
        }else{
            left=mid+1;
        }
    }
    return false;
}

例十八        875. 爱吃香蕉的珂珂

这道题就是典型的二分思想的运用。把吃香蕉的速度当作X,所用时间当作关于速度的函数f(X),求使得f(X)=h的最小X,本质就是一个对左边界的搜索。

long fhour(int* piles, int pilesSize, int speed){
    long hour=0;
    for(int i=0;i<pilesSize;i++){
        if(piles[i]%speed==0){
            hour+=piles[i]/speed;
        }else{
            hour+=piles[i]/speed+1;
        }
    }
    return hour;
}

int minEatingSpeed(int* piles, int pilesSize, int h){
    int left=1,right=1e9;
    while(left<=right){
        int mid=left+(right-left)/2;
        long hour=fhour(piles,pilesSize,mid);
        if(hour<=h){
            right=mid-1;
        }else{
            left=mid+1;
        }
    }
    return left;
}

例十九        658. 找到 K 个最接近的元素

这题比较暴力的解法用两次排序就行,那么怎么用二分法来优化呢?前面提到左边界的搜索返回的值是数组中>=target的最小元素索引,那么就可以先找到该索引,再让两个指针往两端走

int* findClosestElements(int* arr, int arrSize, int k, int x, int* returnSize){
   int left=0,right=arrSize-1;       
   while(left<=right){
       int mid=left+(right-left)/2;
       if(arr[mid]==x){
           right=mid-1;
       }else if(arr[mid]>x){
           right=mid-1;
       }else{
           left=mid+1;
       }
   }
    left=left-1;             //left可能越界,因此-1确保不越界
    while(right-left-1<k){   /*以(left,right)为搜索区间,确保初始区间内有0个数,且先取值比较、 
                               后改变区间*/
        if(left==-1){
            right++;
        }else if(right==arrSize){
            left--;
        }else if(x-arr[left]>arr[right]-x){
            right++;
        }else{
            left--;
        }
    }
    int *ans=malloc(sizeof(int)*k);
    for(int i=0;i<k;i++){
        ans[i]=arr[i+left+1];
    }
    *returnSize=k;
    return ans;
}

例二十        378.有序矩阵中第K小的元素

本题可以用合并K个链表的思路求解,那么能不能用二分法,也就是说对于一个数X,能否实现一个f(X)返回X在矩阵中是第几小(小于等于X的数有几个),答案是能。这里有两个问题:一是如何实现f(X),二是找到的X是否一定在矩阵中存在,详细解答见官方题解。这里其实也用的是左边界的二分搜索,找到最小的X使得f(X)>=k,f(X)返回的是<=X的元素个数(包括自身)。假如有一个不存在于矩阵的数Y,f(Y)>=K,那么矩阵中必存在比Y小的数X,f(X)=f(Y)>=K,所以最后找到是X而不是Y

int fk(int **matrix, int mid, int n) {
    int i=n-1;
    int j=0;
    int num=0;
    while (i>=0&&j<n) {
        if (matrix[i][j]<=mid) {
            num+=i+1;
            j++;
        }else{
            i--;
        }
    }
    return num;
}

int kthSmallest(int **matrix, int matrixSize, int *matrixColSize, int k) {
    int left=matrix[0][0];
    int right=matrix[matrixSize - 1][matrixSize - 1];
    while (left<=right) {
        int mid=left+(right - left)/2;
        int tk=fk(matrix,mid,matrixSize);
        if (tk>=k) {
            right = mid-1;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

例二十一        15. 三数之和

先考虑nSum问题的最基础情景n=2,此时使用左右指针向中间走的方达。假设数组升序,两数和大了就让右指针往左走一步,小了则反之。当n=3时,可以把target-nums[i]作为一个新的target,然后再用左右指针。用这种递归思想可以4Sum降为3Sum再降为2Sum

int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){
    int cnt=0;
    int i=0;
    int **ans;
    *returnSize=0;
    ans=malloc(sizeof(int *)*100000);
    qsort(nums,numsSize,sizeof(nums[0]),cmp);
    while(i<numsSize-1){
        int target=-nums[i];
        int left=i+1,right=numsSize-1;
        while(left<right){
            int num=nums[left]+nums[right];
            if(num==target){
                ans[cnt]=malloc(sizeof(int)*3);
                ans[cnt][0]=nums[i];
                ans[cnt][1]=nums[left];
                ans[cnt++][2]=nums[right];
            }
            if(num>target){       
                while(right>left&&nums[right]==nums[--right]);
            }
            else{
                while(right>left&&nums[left]==nums[++left]);
            }
        }
        while(i<numsSize-1&&nums[i]==nums[++i]);    //去重
    }
    *returnSize=cnt;
    *returnColumnSizes = (int *)malloc(sizeof(int) * (*returnSize));
    for (int i = 0; i < (*returnSize); i++)
        (*returnColumnSizes)[i] = 3;
    return ans;
}

例二十二        151. 翻转字符串里的单词

大体思路分三步:1.去空格        2.翻转整条字符串        3.翻转每个单词

void reverse(char *s,int start,int end){
    for(int i=0;i<=(end-start)/2;i++){
        char c=s[start+i];
        s[start+i]=s[end-i];
        s[end-i]=c;
    }
}

char * reverseWords(char * s){
    int fast,slow,length;
    fast=slow=0;
    length=strlen(s);
    while(s[fast]==' ')
        fast++;
    while(fast<length-1){
        if(s[fast]==' '&&s[fast+1]==' '){
            fast++;
        }else{
            s[slow++]=s[fast++];
        }
    }
    if(s[fast]==' '){          //fast指到s最后一个字符就退出了,因此还要对最后一个字符进行处理
        s[slow]='\0';
    }else{                     
        s[slow++]=s[fast++];
        s[slow]='\0';
    }
    reverse(s,0,slow-1);
    for(int i=0;i<slow-1;){
        int j=i;
        while(s[j]!=' '&&s[j]!='\0')
            j++;
        reverse(s,i,j-1);
        i=j+1;
    }
    return s;
}

例二十三        42. 接雨水

这道题要从局部出发开始想,对于任意位置i,它所能接到的雨水等于min(l_max,r_max)-height[i],l_max为左侧柱子最大高度,r_max为右侧柱子最大高度。那么对于位置i,如何找左右最大高度呢?方法一是每次用左右指针向两边移,记录最大值;稍微优化点的是建立一个数组预先存储每个位置的左右最大高度。可以发现以上两种方法都先找后算,重复遍历了height数组,那能否一边找一边算,只遍历一次呢?代码如下:

int trap(int* height, int heightSize){
    int left=0,right=heightSize-1;
    int max_l=0,max_r=0;
    int res=0;
    while(left<=right){
        max_l=max(max_l,height[left]);
        max_r=max(max_r,height[right]);
        if(max_l<max_r){               /*max_r不一定为右侧最大高度(记为maxr),但此时 
                                         min(max_l,max_r)=min(max_l,maxr)*/
            res+=max_l-height[left];
            left++;
        }else{
            res+=max_r-height[right];
            right--;
        }
    }
    return res;
}

例二十四        5.最长回文子串

本题思想很简单,初始左右指针指向同一位置i,然后往两边走,记录回文子串的长度,每次走完更新max的值,记录起止点即可

int palindrome(char *s,int left,int right){
    while(left>=0&&right<strlen(s)&&s[left]==s[right]){
        left--;
        right++;
    }
    return right-left-1;
}

char * longestPalindrome(char * s){
    int l1,l2,maxl;
    maxl=0;
    char *ans;
    int left,right;
    left=right=0;
    for(int i=0;i<strlen(s)-1;i++){
        l1=palindrome(s,i,i);
        l2=palindrome(s,i,i+1);
        maxl=maxl>l1 ? maxl:l1;
        maxl=maxl>l2 ? maxl:l2;
        if(maxl>right-left+1){
            right=i+maxl/2;
            left=right-maxl+1;
        }
    }
    ans=malloc((right-left+1)*sizeof(char *));
    for(int i=left;i<=right;i++){
        ans[i-left]=s[i];
    }
    ans[right-left+1]='\0';
    return ans;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

vio_gram

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值