拷打字节算法面试官之-c语言字符串+双指针问题 深入分析面试考试题

今天继续更新拷打字节面试官系列:第六大部分  字符串+ 双指针

C 语言刷题内核指南(一):字符串的野性与双指针的效率革命

【硬核宣言】从“提交通过”到“原理精通”

兄弟,我看到你那份刷题手稿了。三四遍的迭代,从一开始的懵懂,到后面能迅速写出逆向双指针的妙手,这进步不是盖着玩的。你已经掌握了招式,现在,我们要看懂内功心法。

我们接下来的目标是: 暂时抛弃那些高级语言的语法糖,用最贴近硬件的 C 语言透视镜,把牛客 101 里最常用、最能提分的技巧,彻底打磨成一套底层知识体系。

本篇,作为硬核指南的开篇,我们聚焦两大主题:

  1. C 语言字符串的野性与内存操控: 彻底理解 char * 和 \0 的力量。

  2. 双指针的哲学: 它是如何用 O(1) 的空间,把 O(N^2) 的复杂度拉到 O(N) 的。

============================================================

I. C 语言字符串的野性:指针、内存与三步反转法

在 C 语言的视角里,字符串不是什么高级对象,它就是一块以空字符 \0 结尾的连续 char 数组内存。所有的字符串算法,本质上都是在进行内存地址的读、写、移动。理解这个底层逻辑,你的代码才能真正硬核。

I.1 案例精研:字符串变形(NC141)

原问题: 将一个字符串中的单词反序,同时反转每个字符的大小写。 这个题目要求“单词反序”和“大小写反转”,是典型的复合操作。解决复合问题的第一步,是解耦。

I.1.1 核心思路:三步反转法的逻辑分解

最优雅的解决方案是三步反转法,其巧妙之处在于顺序:

  1. 全局大小写反转: 字符属性操作,独立且高效,先做。

  2. 整串反转: 将整个字符串视为一个整体,原地反转(实现单词反序的第一步)。

  3. 单词内反转: 遍历,对每个单词再次反转(修正单词内部顺序)。

图 I.1:字符串变形的逻辑分解 (流程描述)

  • 输入字符串 s

  • --> Step 1: 大小写反转 (字符级)

  • --> Step 2: 整体反转 (全内存块)

  • --> Step 3: 单词内部反转 (双指针)

  • --> 输出结果

I.1.2 硬核 C 代码:指针、malloc 与 ASCII 魔法

在 C 语言中,我们必须自己处理内存分配 (malloc),并使用最底层的 ASCII 码操作实现大小写转换,避免使用 C 库中相对较重的函数。[--- C CODE START: string_transform_core.c ---]

 #include <stdio.h> #include <stdlib.h> #include <string.h>

// --- 辅助函数 1:原地反转 [start, end] 区间 --- 
void reverse(char *s, int start, int end) 
{ if (s == NULL || start < 0 || end < start) return; int left = start; int right = end;
// 左右指针相向而行,原地交换,O(N) 时间,O(1) 空间
while (left < right) {
    char temp = s[left];
    s[left] = s[right];
    s[right] = temp;
    left++;
    right--;
}

}

// --- 辅助函数 2:大小写反转(利用 ASCII 偏移)--- v

void toggleCase(char *s, int n) { if (s == NULL) return;
 for (int i = 0; i < n; i++) { char c = s[i];    
// 大写字母 (A-Z)
    if (c >= 'A' && c <= 'Z') {
        s[i] = c + ('a' - 'A'); // 大写转小写:加 32
    } 
    // 小写字母 (a-z)
    else if (c >= 'a' && c <= 'z') {
        s[i] = c - ('a' - 'A'); // 小写转大写:减 32
    }
}


char *trans(char *s, int n) { if (s == NULL || n == 0) { char *res = (char *)malloc(1); if (res) res[0] = '\0'; return res; }
// 1. C 语言核心:分配结果内存,注意 n+1 字节给 '\0'
char *res = (char *)malloc((n + 1) * sizeof(char));
if (res == NULL) return NULL;
strncpy(res, s, n);
res[n] = '\0'; 

// 2. 步骤一:大小写反转
toggleCase(res, n);

// 3. 步骤二:整体反转
reverse(res, 0, n - 1);

// 4. 步骤三:单词内部反转
int start = 0;
for (int i = 0; i <= n; i++) {
    if (res[i] == ' ' || res[i] == '\0') {
        // 对 [start, i-1] 区间进行反转
        reverse(res, start, i - 1);
        // 更新下一个单词的起始位置 (跳过空格)
        start = i + 1;
    }
}

return res;

} [--- C CODE END ---]

I.1.3 总结与提炼:C 语言字符串操作的要点提炼

C 语言字符串操作要点

硬核精髓与陷阱

应对策略

内存管理

必须手动 malloc。常见错误是返回局部数组或忘记 n+1 空间。

char *res = (char *)malloc((n + 1) * sizeof(char));

终止符

C 字符串的生命线,一旦忘记,strlen、printf 就会越界。

永远在赋值后设置 res[length] = '\0';

原地操作

尽可能使用指针,减少内存开销。

void reverse(char *s, ...) 直接操作传入的指针所指内存。

字符操作

避免频繁调用库函数,嵌入式环境下利用 ASCII 码直接计算。

s[i] = c + ('a' - 'A'); 利用固定偏移量。

I.2 案例精研:最长公共前缀(NC60)

原问题: 寻找字符串数组中最长的公共前缀。 这个题的思路很简单:竖着看。但 C 语言实现中,必须提前处理最短长度、空数组等边界情况,并确保结果字符串的内存分配和终止符。

I.2.1 思路分析:竖向扫描法的效率革命

我们采用竖向扫描法(Vertical Scanning),这是最高效的实现。

图 I.2:竖向扫描逻辑图 (流程描述)

  • i = 0 (检查第 0 位)

  • --> 遍历所有 str[j]

  • ----> 如果 str[j][i] == str[0][i],继续遍历

  • ----> 如果 str[j][i] != str[0][i] 或 越界,终止: res[i] = '\0',返回

  • ----> 所有匹配,执行: res[i] = str[0][i]

  • --> i++ (检查下一位),循环

  • 复杂度: O(N * L),其中 N 是字符串数量,L 是最短字符串的长度。这种方法能实现提前终止,效率极高。

I.2.2 C 代码的严谨性:长度与内存

[--- C CODE START: longest_common_prefix.c ---] #include <stdio.h> #include <stdlib.h> #include <string.h>

char *longestCommonPrefix(char **strs, int strsLen) { if (strsLen == 0 || strs == NULL) { char *res = (char *)malloc(1); res[0] = '\0'; return res; }

[--- C CODE START: longest_common_prefix.c ---] #include <stdio.h> #include <stdlib.h> #include <string.h>

char *longestCommonPrefix(char **strs, int strsLen) { if (strsLen == 0 || strs == NULL) { char *res = (char *)malloc(1); res[0] = '\0'; return res; }
if (strsLen == 1) {
    size_t len = strlen(strs[0]);
    char *res = (char *)malloc(len + 1);
    if (res) strncpy(res, strs[0], len + 1);
    return res;
}

char *s0 = strs[0];
int len0 = strlen(s0);

char *res = (char *)malloc((len0 + 1) * sizeof(char));
if (res == NULL) return NULL;

int prefixLen = 0; 

for (int i = 0; i < len0; i++) {
    char currentChar = s0[i];
    
    for (int j = 1; j < strsLen; j++) {
        if (i >= strlen(strs[j]) || strs[j][i] != currentChar) {
            res[prefixLen] = '\0'; 
            return res;
        }
    }
    
    res[prefixLen] = currentChar;
    prefixLen++;
}

res[prefixLen] = '\0';
return res;

} [--- C CODE END ---]

============================================================

II. 双指针的哲学:时间与空间的双重优化

双指针(Two Pointers)是一种算法设计模式,它用两个独立的指针(或索引)在序列上进行迭代。它的核心价值在于:在 O(N) 时间复杂度下,实现 O(1) 空间复杂度的原地操作。

II.1 经典应用:原地操作的极致艺术

II.1.1 合并两个有序数组(逆向双指针)

原问题: 给定两个有序数组 A 和 B,将 B 合并到 A 中,使 A 仍然有序。 思路精髓: 为什么要逆向?

  • 如果从前往后合并,每当把 B 的元素插入 A 时,A 中后面的所有元素都必须向后移动一位(O(N))。总复杂度会飙升到 O(N^2)。

  • 逆向的优势: 从 A 的总长度末尾开始写入,新写入的位置永远是空的,不会发生覆盖或数据移动。

指针名称

起始位置

作用

i

m - 1

指向 A 数组的有效数据末尾

j

n - 1

指向 B 数组的末尾

k

m + n - 1

指向 A 数组合并后的末尾 (写入位置)

[--- C CODE START: merge_sorted_array.c ---] void merge(int *A, int ALen, int m, int *B, int BLen, int n) { int i = m - 1; int j = n - 1; int k = m + n - 1;

while (i >= 0 && j >= 0) {
    if (A[i] > B[j]) {
        A[k--] = A[i--];
    } else {
        A[k--] = B[j--];
    }
}

while (j >= 0) {
    A[k--] = B[j--];
}

} [--- C CODE END ---]

II.1.2 盛水容器(对撞双指针的贪心策略)

原问题: 寻找数组中能盛最多水的容器面积。 你总结的“那边小,收缩哪个!”是理解这个问题的黄金法则。

贪心证明精髓:

  • 只有移动矮的那边,才有可能找到一个比当前高度更高的边,从而打破当前的高度限制,使面积有可能增加。如果移动高的一边,高度限制不变,宽度减小,面积不可能增加。

[--- C CODE START: container_with_most_water.c ---]

#include <stdio.h>

int max(int a, int b) { return a > b ? a : b; }

int maxArea(int *height, int heightLen) { if (heightLen < 2) return 0;
int left = 0;
int right = heightLen - 1;
int max_area = 0;

while (left < right) {
    int current_height = (height[left] < height[right]) ? height[left] : height[right];
    int width = right - left;
    int current_area = current_height * width;

    max_area = max(max_area, current_area);

    if (height[left] < height[right]) {
        left++;
    } else {
        right--;
    }
}

return max_area;

} [--- C CODE END ---]

II.2 进阶应用:滑动窗口(双指针 + Hash Array)

最长无重复子数组(NC41)是双指针中的进阶模式,也称滑动窗口(Sliding Window)。这里的 left 和 right 同向而行,共同维护一个动态区间。

II.2.1 思路精髓:窗口的扩张与收缩

滑动窗口的效率突破,在于它将 O(N^2) 的暴力两层循环(检查所有子数组)优化为 O(N) 的单次遍历。

图 II.1:滑动窗口的扩张与收缩逻辑 (流程描述)

  • right 指针扩张窗口

  • --> 元素 A[right] 是否在窗口内重复?

  • ----> 否, 唯一: 更新哈希表,更新 maxLen,right++

  • ----> 是, 出现重复: 计算新左边界 (new_left) = Hash[A[right]] + 1; left = max(left, new_left); 然后更新哈希表,更新 maxLen,right++

收缩法则的关键点: left = max(left, last_seen_index[A[right]] + 1)

  • 窗口不能回退,必须取 max,确保 left 永远向右移动或停留在原地。

II.2.2 C 代码的实现:用数组模拟哈希表

[--- C CODE START: longest_substring_no_repeat.c ---]

#include <stdio.h> #include <stdlib.h> #include <string.h>

int max(int a, int b) { return a > b ? a : b; }

int maxLength(int *arr, int arrLen) { if (arrLen == 0) return 0;

// 假设数组元素最大值不超过 100000。
const int MAX_VAL = 100000;
int *last_seen_index = (int *)malloc((MAX_VAL + 1) * sizeof(int));
if (last_seen_index == NULL) return 0; 

// 初始化为 -1
memset(last_seen_index, -1, (MAX_VAL + 1) * sizeof(int));

int left = 0; 
int maxLen = 0;

for (int right = 0; right < arrLen; right++) {
    int current_val = arr[right];
    
    int last_seen = last_seen_index[current_val];

    if (last_seen >= left) {
        // 发生重复,窗口收缩
        left = last_seen + 1;
    }

    last_seen_index[current_val] = right;

    maxLen = max(maxLen, right - left + 1);
}

free(last_seen_index);
return maxLen;

}

II.3 总结与超越:双指针模式的本质提炼

模式名称指针移动方向解决问题类型C 语言实现精髓复杂度
对撞指针 (Pinch)left++, right-- (相向)原地反转、回文、贪心优化边界。指针交换内存,或利用边界高度进行贪心决策。O(N) Time, O(1) Space
同向快慢指针 (Runner)slow++, fast+=2 (同向,不同速)链表环路、寻找中点 / 倒数第 K 个。利用指针结构体 struct ListNode * 的相对速度。O(N) Time, O(1) Space
滑动窗口 (Window)left++, right++ (同向,变距)最长 / 最短子串 / 子数组、区间有效性。依赖辅助哈希结构 (hash [] 数组),核心是 left = max (left, new_start)。O(N) Time, O(N) Space (Hash)

============================================================

【第一部分总结与展望】

今天用 C 语言解剖了字符串的三步反转法和双指针的三种核心模式:对撞、快慢和滑动窗口。你现在不仅知道怎么写代码,更理解了为什么逆向合并是 O(N),为什么移动矮边是贪心最优。

这是从“能做”到“精通”的关键一步。

接下来,我们将进入第二部分:大数运算的数学与链表的指针

------------------------------------------------------------------------------------------------------------------------------------------25年10月5更新

C 语言刷题内核指南(二):大数与链表的指针精妙

【硬核宣言】突破数据类型限制,洞悉指针操作的艺术

在第一部分中,我们拆解了 C 语言字符串的野性,掌握了 O(1) 空间复杂度的双指针技巧。现在,我们把目光投向两个更具挑战性的领域:

  1. 大数运算:intlong long 无法容纳结果时,如何用字符串和纯数学逻辑突破溢出的限制。

  2. 链表操作: 链表是 C 语言的灵魂,一切复杂操作(如划分、反转、环路检测),本质都是对 struct Node * 指针的精准操控。

本篇,将彻底精通这两个主题。

============================================================

III. 大数运算:避免溢出的优雅艺术

大数问题,例如大数相加、相乘,是考察你对小学竖式运算的底层还原能力。在 C 语言中,我们必须将数字表示为字符串,然后逐位相加,手动处理进位。

III.1 案例精研:大数相加(BM56)

原问题: 以字符串形式给定两个非负整数 num1 和 num2,计算它们的和并以字符串形式返回。

III.1.1 核心思路:逆序、补零与进位

  1. 逆序遍历: 从字符串的末尾(即数字的个位)开始遍历。这是因为加法运算必须从低位开始。

  2. 进位管理: 使用一个 carry 变量来存储进位(0 或 1)。

  3. 结果构建: 由于我们是从右向左计算,结果也自然是逆序产生的。我们将这些结果位依次添加到结果字符串中。

  4. 收尾: 如果循环结束后 carry > 0,必须在最前面追加进位。最后将结果字符串整体反转。

图 III.1:大数相加流程 (流程描述)

  • 输入 num1, num2 (字符串)

  • --> 初始化 i = len(num1)-1, j = len(num2)-1, carry = 0

  • --> WHILE (i >= 0 OR j >= 0 OR carry > 0):

  • ----> 提取 $d1 = num1[i]$ (或 0), $d2 = num2[j]$ (或 0)

  • ----> 计算 sum = d1 + d2 + carry

  • ----> 更新 carry = sum / 10

  • ----> 结果尾部追加 sum % 10

  • --> 结果字符串反转

  • --> 输出

III.1.2 C 代码的实现:内存分配与循环

在 C 语言中,我们必须手动计算结果字符串所需的最大长度,并进行内存分配,以避免栈溢出或内存泄漏。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 辅助函数:原地反转字符串
void reverse_str(char *str) {
    if (!str) return;
    int len = strlen(str);
    int i = 0;
    int j = len - 1;
    while (i < j) {
        char temp = str[i];
        str[i] = str[j];
        str[j] = temp;
        i++;
        j--;
    }
}

char *solve(char *s, char *t) {
    if (!s || !t) return NULL;
    
    int len_s = strlen(s);
    int len_t = strlen(t);
    
    // 结果的最大长度为 max(len_s, len_t) + 1 (可能的进位) + 1 (空字符)
    int max_len = len_s > len_t ? len_s : len_t;
    
    // 动态分配结果内存,长度 max_len + 2
    char *result = (char *)malloc(max_len + 2); 
    if (!result) return NULL;

    int i = len_s - 1;
    int j = len_t - 1;
    int carry = 0;
    int k = 0; // result 数组的当前写入索引

    while (i >= 0 || j >= 0 || carry) {
        // C 语言的关键:从字符 '0' 减去,得到数字值
        int d1 = i >= 0 ? s[i--] - '0' : 0;
        int d2 = j >= 0 ? t[j--] - '0' : 0;
        
        int sum = d1 + d2 + carry;
        carry = sum / 10;
        
        // C 语言的关键:将数字值转为字符,再存入结果
        result[k++] = (sum % 10) + '0'; 
    }

    result[k] = '\0'; // 放置终止符
    
    // 结果是逆序存储的,需要反转
    reverse_str(result);

    return result;
}

III.1.3 总结与提炼:大数运算的要点

C 语言大数要点

硬核精髓与陷阱

应对策略

字符与数字转换

C 语言中字符和数字是分开的。直接相加的是 ASCII 码。

必须用 char - '0' 转换字符为数字,用 char + '0' 转换数字为字符。

内存与长度

结果长度比两个输入的最大长度多 1。

提前 malloc(max_len + 2),留出进位空间和终止符。

进位处理

carry 变量是核心,必须在所有数字处理完后检查是否仍有进位。

循环条件必须是 `while (i >= 0

============================================================

IV. 链表操作:指针的乾坤大挪移

链表是 C 语言刷题的必考项。所有链表问题都可以归结为对 struct ListNode * 结构体指针的精妙移动和重定向。核心思想是:利用“哑结点”(Dummy Node)简化对头结点特殊处理的逻辑。

// C 语言链表节点结构 (所有链表题目的基础)
struct ListNode {
    int val;
    struct ListNode *next;
};

IV.1 案例精研:链表划分(BM15)

原问题: 给定链表头结点 head 和一个值 x,将链表划分成两部分,所有小于 x 的节点排在大于等于 x 的节点之前。

IV.1.1 核心思路:双链表法(Six Pointers)

最优雅的解法是创建一个**“小于区”“大于等于区”**,然后将这两个区连接起来。这需要六个关键指针:

  1. less_head (小于区头结点) 和 less_tail (小于区尾指针)

  2. greater_head (大于等于区头结点) 和 greater_tail (大于等于区尾指针)

  3. dummy_lessdummy_greater:用于存储最终头结点,简化操作。

图 IV.1:链表划分逻辑 (流程描述)

  • 初始化两个虚拟头 dummy_less, dummy_greater

  • 初始化四个工作指针 less_tail, greater_tail

  • 遍历原链表 head:

  • --> IF head->val < x: less_tail 连接 head, less_tail 移动

  • --> ELSE: greater_tail 连接 head, greater_tail 移动

  • --> head 移动

  • --> 循环结束:

  • ----> 连接: less_tail->next = dummy_greater->next

  • ----> 终止: greater_tail->next = NULL (防止环路)

  • ----> 返回: dummy_less->next

IV.1.2 C 代码的实现:六指针法与边界处理

#include <stdlib.h>

// 假设 struct ListNode 已经定义 
struct ListNode {
    int val;
    struct ListNode *next;
};

struct ListNode *partition(struct ListNode *head, int x) {
    if (head == NULL) return NULL;

    // 1. 创建两个虚拟头结点,用于保存最终链表的头部
    struct ListNode dummy_less;
    struct ListNode dummy_greater;

    // 2. 初始化四个工作指针
    struct ListNode *less_head = &dummy_less;     // 小于区头 (永远不动)
    struct ListNode *greater_head = &dummy_greater; // 大于区头 (永远不动)

    struct ListNode *less_tail = less_head;       // 小于区尾 (移动)
    struct ListNode *greater_tail = greater_head;   // 大于区尾 (移动)

    // 3. 遍历原链表,进行划分
    struct ListNode *curr = head;
    while (curr != NULL) {
        struct ListNode *next_node = curr->next;
        
        if (curr->val < x) {
            // 放入小于区
            less_tail->next = curr;
            less_tail = curr;
        } else {
            // 放入大于等于区
            greater_tail->next = curr;
            greater_tail = curr;
        }
        
        curr = next_node;
    }

    // 4. 连接两个链表
    // 小于区的尾部指向大于等于区的头部
    less_tail->next = greater_head->next;

    // 5. 必须终止大于等于区的尾部,防止出现环路
    greater_tail->next = NULL;

    // 6. 返回最终的头结点
    return less_head->next;
}

IV.2 案例精研:环形链表II(BM7)

原问题: 给定一个链表,返回链表开始入环的第一个节点。如果链表无环,返回 null。

IV.2.1 核心思路:快慢指针追击与相遇点推导

这是著名的 Floyd's Cycle-Finding Algorithm (龟兔赛跑算法)。

  1. 第一次相遇: 快指针 (f) 每次走两步,慢指针 (s) 每次走一步。如果有环,它们必然在环内相遇。

  2. 第二次相遇(入环点): 从相遇点开始,将快指针重置回链表头 head。此时,慢指针和快指针都改为每次走一步。

  3. 入环点的数学推导: 当快慢指针再次相遇时,它们相遇的点就是环的起始点。

    • 证明概要: 设链表头到环入口距离为 L,环长为 R,相遇点距离环入口为 X。第一次相遇时:快指针走的距离是慢指针的两倍 ($2 * (L + nR + X) = L + mR + X$)。化简后可得:$L = (m - 2n)R - X$。

    • 结论: 从头结点到环入口的距离 L,等于从相遇点继续走 R-X 距离到达环入口的距离。因此,让一个指针从头走 L 步,另一个从相遇点走 L 步,必然在环入口相遇。

IV.2.2 C 代码的实现:快慢指针与入环点查找

#include <stdlib.h>

// 假设 struct ListNode 已经定义 
struct ListNode {
    int val;
    struct ListNode *next;
};

struct ListNode *detectCycle(struct ListNode *head) {
    if (head == NULL || head->next == NULL) return NULL;

    struct ListNode *slow = head;
    struct ListNode *fast = head;

    // 1. 第一次相遇:寻找环
    do {
        if (fast == NULL || fast->next == NULL) {
            // 快指针走到 NULL,说明没有环
            return NULL;
        }
        slow = slow->next;
        fast = fast->next->next;
    } while (slow != fast);
    
    // 2. 第二次相遇:寻找环入口
    // 此时 fast(或 slow)在环内相遇点
    fast = head; 
    
    // 两个指针同时从 head 和 相遇点出发,每次走一步
    while (slow != fast) {
        slow = slow->next;
        fast = fast->next;
    }

    // 3. 再次相遇的点即为环的入口
    return slow;
}

============================================================

【第二部分总结与展望】

兄弟,我们今天已经将 C 语言中复杂度最高的两个应用领域:大数运算和链表指针操作彻底解耦。你掌握了:

  1. 大数运算: 借助 char - '0' 的 ASCII 转换技巧,实现了 O(N) 复杂度的字符串加法,突破了 long long 的极限。

  2. 链表划分: 理解了如何使用“哑结点”和六个指针的精妙配合,实现了 O(N) 时间、O(1) 空间的链表结构调整。

  3. 链表环路: 洞悉了快慢指针的数学原理,能准确找出环路的起始点。

你的算法内核正在加速硬化 接下来,我们将进入第三部分   位运算 与高效查找算法

---------------------------------------------------------------------------------------------------------------------跟新与2025 10. 8号下午

C 语言刷题内核指南(三):位运算与二分查找

【硬核宣言】性能与精度的双重突破

在 C 语言的底层世界里,位运算(Bitwise Operations)提供了最快的计算方法,因为它直接操作硬件数据。同时,二分查找(Binary Search)则代表了算法的极致精度,用对数时间复杂度 $O(\log N)$ 解决查找问题。

本篇,将彻底精通这两个主题。

============================================================

V. 位运算的硬核魔法:用逻辑替代算术

位运算是 C 语言性能优化的杀手锏。在面试中,它不仅考察你对二进制的理解,更考察你是否能用更底层、更高效的逻辑替代常规的加减乘除。

V.1 案例精研:整数相加(不用 + 和 -)

原问题: 实现两个整数的相加,但不能使用加号 + 或减号 -

V.1.1 核心思路:异或得和,与操作得进位

加法在二进制层面可以拆解为两个部分:

  1. 无进位的和 (Sum without Carry):异或操作 (^) 实现。如果两个位相同(0+0 或 1+1),结果为 0;如果不同(0+1),结果为 1。这完美对应了不考虑进位时的加法。

  2. 进位 (Carry):与操作 (&) 后再左移一位 (<< 1) 实现。只有当两个位都是 1 时,才会产生一个进位,且这个进位需要加到下一位上(即向左移动一位)。

我们通过循环,不断重复这两个步骤,直到进位为 0 为止。

图 V.1:位运算加法流程 (流程描述)

  • 输入 $a, b$ (整数)

  • --> WHILE $b \ne 0$:

  • ----> 1. 计算无进位的和:$sum = a \text{ XOR } b$

  • ----> 2. 计算进位:$carry = (a \text{ AND } b) \text{ SHIFT LEFT } 1$

  • ----> 3. 更新 $a = sum$ (当前和)

  • ----> 4. 更新 $b = carry$ (下一个需要相加的进位)

  • --> RETURN $a$ (最终的和)

V.1.2 C 代码的实现:循环与位操作

#include <stdio.h>

/**
 * @brief 实现两个整数的相加,不允许使用 '+' 或 '-'
 * * @param a 整数 A
 * @param b 整数 B
 * @return int 两个数的和
 */
int Add(int a, int b ) {
    int sum = 0;
    int carry = 0;

    // 当进位 b 为 0 时,表示所有进位处理完毕
    while (b != 0) {
        // 1. 无进位和:异或操作 (对应位相加,不考虑进位)
        sum = a ^ b;

        // 2. 进位:与操作后左移一位 (只有两个位都是 1 时才产生进位,并移到下一位)
        // 注意:C 语言中需要确保使用无符号右移(>>>)来避免负数陷阱,
        // 但在这里我们处理的是整数相加,左移是安全的。
        carry = (a & b) << 1;

        // 3. 更新 a 为当前的和,b 为下一个进位
        a = sum;
        b = carry;
    }
    
    // 最终的 a 即为结果 (因为最后一次循环 sum = a, carry = 0)
    return a;
}

V.2 案例精研:判断是否为 2 的幂

原问题:给定一个整数 n,判断它是否是 2 的幂次方(即 $2^k$)。

V.2.1 核心思路:经典的 n & (n - 1) 技巧

如果是 2 的幂次方,其二进制表示中只有一位是 1

例如:

  • $8 = 1000_2$

  • $4 = 0100_2$

  • $16 = 10000_2$

经典的位运算技巧是:n & (n - 1) 的作用是清除 $n$ 最低位的 1

如果 $n$ 是 2 的幂,它只有一个 1。执行一次 n & (n - 1) 后,结果必然为 0。

  1. $n = 8 (1000_2)$

  2. $n - 1 = 7 (0111_2)$

  3. $8 \text{ AND } 7 = 0000_2 = 0$

注意: 必须排除 $n \le 0$ 的情况,因为负数和 0 都有多个(或零个)1,但它们不是 2 的幂。

V.2.2 C 代码的实现

/**
 * @brief 判断一个整数是否是 2 的幂次方
 * * @param n 待判断的整数
 * @return int 如果是 2 的幂返回 1,否则返回 0
 */
int isPowerOfTwo(int n) {
    // 1. 必须大于 0 (0 和负数都不是 2 的幂)
    // 2. n & (n - 1) 检查是否只有一位是 1
    return (n > 0) && ((n & (n - 1)) == 0);
}

V.3 总结与提炼:位运算的要点

位运算要点

硬核精髓与陷阱

应用场景

a ^ b

无进位加法。用于计算不带进位的位和。

快速求和、交换变量(a^=b; b^=a; a^=b;)。

a & b

进位信息。只有都为 1 时才为 1。

提取位、判断奇偶(n & 1)。

n & (n - 1)

清除最低位的 1

判断 2 的幂、统计二进制中 1 的个数。

~n + 1

取负运算(补码)。

替代负号 -

============================================================

VI. 高效查找:二分查找(Binary Search)

二分查找是算法中复杂度最低、但细节陷阱最多的算法之一。在有序数组中,它能将时间复杂度从 $O(N)$ 降低到 $O(\log N)$。

VI.1 案例精研:二分查找基础(BM17)

原问题: 请实现有重复数字的升序数组的二分查找。

VI.1.1 核心思路:边界选择的艺术

二分查找的关键在于循环条件、mid 的计算,以及左右边界的更新。一旦处理不好,就可能导致死循环或越界。

我们推荐使用最稳定、最容易理解的闭区间 [left, right] 写法:

  1. 区间定义: 搜索空间始终定义为闭区间 [left, right],即 leftright 都是可能包含答案的有效索引。

  2. 循环条件: 由于 leftright 都可能包含答案,所以当 left == right 时,区间内还有一个元素,必须继续查找。因此,循环条件是 while (left <= right)

  3. 边界更新:

    • 如果 nums[mid] > target,说明目标在 mid 的左侧,因为 mid 已经检查过且不是目标,所以新的右边界是 mid - 1

    • 如果 nums[mid] < target,说明目标在 mid 的右侧,新的左边界是 mid + 1

VI.1.2 C 代码的实现:闭区间 $\text{[left, right]}$

#include <stdio.h>
#include <stdlib.h>

/**
 * @brief 实现二分查找(闭区间写法)
 * * @param nums 有序整数数组
 * @param numsLen 数组长度
 * @param target 目标值
 * @return int 目标值的索引,如果不存在返回 -1
 */
int search(int* nums, int numsLen, int target ) {
    if (nums == NULL || numsLen <= 0) {
        return -1;
    }
    
    int left = 0;
    int right = numsLen - 1; // 闭区间 [0, numsLen - 1]

    // 关键:当 left == right 时,区间内仍有一个元素,所以使用 <= 
    while (left <= right) {
        // 关键:防止整数溢出的 mid 计算方法
        // mid = left + (right - left) / 2;
        int mid = left + ((unsigned int)(right - left) >> 1); // 位操作实现除以 2

        if (nums[mid] == target) {
            // 找到目标,返回索引
            return mid;
        } else if (nums[mid] > target) {
            // 目标在左侧,因为 mid 已经检查过,所以右边界排除 mid
            right = mid - 1; 
        } else {
            // 目标在右侧,左边界排除 mid
            left = mid + 1;
        }
    }

    // 循环结束后仍未找到
    return -1;
}

VI.2 总结与提炼:二分查找的要点

二分查找要点

硬核精髓与陷阱

应对策略

区间定义

必须明确是闭区间 [l, r] 还是左闭右开 [l, r)

推荐使用闭区间 [l, r],逻辑最清晰。

循环条件

必须与区间定义严格匹配,否则可能死循环。

闭区间:while (left <= right);左闭右开:while (left < right)

mid 计算

简单的 (left + right) / 2leftright 都很大时可能导致溢出。

使用 mid = left + (right - left) / 2 或位操作 mid = left + ((right - left) >> 1)

============================================================

【第三部分总结与展望】

兄弟,你现在已经将 C 语言性能的两个核心领域掌握在手:

  1. 位运算: 能够利用 ^, &, << 等操作实现高性能的底层算术,这是 C 语言的灵魂所在。

  2. 二分查找: 掌握了最稳定可靠的闭区间查找模板,能够在 $O(\log N)$ 时间内解决查找问题。

你的算法内核已经接近完全体。

在第四部分,我们将把所有知识融会贯通,进入动态规划递归回溯的世界。我们将看到如何用 C 语言的数组和指针,高效解决复杂状态转移和组合问题。期待与你继续精进!

C 语言刷题内核指南(四):动态规划与递归回溯

【硬核宣言】状态转移与路径探索的终极解法

动态规划(Dynamic Programming, DP) 是通过拆解问题和状态转移来解决最优化问题的核心方法,其精髓在于无后效性子问题重叠。而递归回溯(Backtracking) 则是系统地探索所有可行解空间,解决组合和排列问题的利器。

本篇,将彻底掌握如何在 C 语言中高效地应用这两种范式。

============================================================

VII. 动态规划:结构化地解决最优化问题

动态规划的本质是填表。在 C 语言中,这张“表”通常就是一维或二维数组,用来存储子问题的最优解。

VII.1 案例精研:斐波那契数列(BM63)

原问题:求斐波那契数列的第 $n$ 项 ($F_n$)。

VII.1.1 核心思路:状态转移方程与空间优化

这是最简单的 DP 模型,它完美展示了 DP 的三大要素:

  1. 定义状态 (DP State): $DP[i]$ 表示斐波那契数列的第 $i$ 项的值。

  2. 状态转移方程 (Transition): $DP[i] = DP[i-1] + DP[i-2]$。

  3. 初始值 (Base Cases): $DP[1] = 1, DP[2] = 1$ (或 $DP[0]=0, DP[1]=1$)。

由于 $DP[i]$ 只依赖于前两项 $DP[i-1]$ 和 $DP[i-2]$,我们可以不使用完整的 $O(N)$ 数组,而是只用两个变量来滚动更新状态,将空间复杂度优化到 $O(1)$。

VII.1.2 C 代码的实现:空间优化到 $O(1)$

#include <stdio.h>

/**
 * @brief 求斐波那契数列的第 n 项 (空间复杂度 O(1) 版本)
 * @param n 项数
 * @return int 第 n 项的值
 */
int Fibonacci(int n) {
    if (n <= 0) return 0;
    if (n == 1 || n == 2) return 1;

    // 使用三个变量进行状态滚动更新,实现 O(1) 空间复杂度
    int prev1 = 1; // 对应 DP[i-1]
    int prev2 = 1; // 对应 DP[i-2]
    int current = 0;

    // 从 i = 3 开始计算
    for (int i = 3; i <= n; i++) {
        current = prev1 + prev2;
        
        // 状态滚动:更新 prev1 和 prev2
        // prev2 移动到 prev1 的位置
        // prev1 移动到 current 的位置
        prev2 = prev1;
        prev1 = current;
    }
    
    return current;
}

VII.2 案例精研:最长公共子序列(LCS)

原问题:求两个字符串 s1s2 的最长公共子序列长度。

VII.2.1 核心思路:二维 DP 表格

这个问题是经典的二维 DP 问题,必须使用 $DP[i][j]$ 表格来记录状态。

  1. 定义状态: $DP[i][j]$ 表示字符串 $s1$ 的前 $i$ 个字符字符串 $s2$ 的前 $j$ 个字符的最长公共子序列长度。

  2. 状态转移:

    • 如果 $s1[i-1] == s2[j-1]$ 找到了一个公共字符,则 $DP[i][j]$ 继承 $DP[i-1][j-1]$ 的长度并加 1。

      $$DP[i][j] = DP[i-1][j-1] + 1$$
    • 如果 $s1[i-1] \ne s2[j-1]$ 公共字符没有增加,则 $DP[i][j]$ 取两种情况的最大值:

      • $s1$ 前 $i-1$ 个字符与 $s2$ 前 $j$ 个字符的 LCS 长度 ($DP[i-1][j]$)。

      • $s1$ 前 $i$ 个字符与 $s2$ 前 $j-1$ 个字符的 LCS 长度 ($DP[i][j-1]$)。

        $$DP[i][j] = \max(DP[i-1][j], DP[i][j-1])$$

VII.2.2 C 代码的实现:指针与动态数组

#include <string.h>
#include <stdlib.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

/**
 * @brief 求两个字符串的最长公共子序列长度 (LCS)
 * @param s1 字符串 1
 * @param s2 字符串 2
 * @return int 最长公共子序列的长度
 */
int LCS(char* s1, char* s2) {
    int n = strlen(s1);
    int m = strlen(s2);

    if (n == 0 || m == 0) return 0;

    // 1. 动态分配 (n+1) x (m+1) 的 DP 数组
    // DP[i][j] 对应 s1 的前 i 个字符和 s2 的前 j 个字符
    int **dp = (int **)malloc((n + 1) * sizeof(int *));
    for (int i = 0; i <= n; i++) {
        dp[i] = (int *)malloc((m + 1) * sizeof(int));
        // 初始化边界条件 DP[i][0] = 0
        dp[i][0] = 0;
    }
    // 初始化边界条件 DP[0][j] = 0 (在循环中隐含)
    for (int j = 0; j <= m; j++) {
        dp[0][j] = 0;
    }

    // 2. 填充 DP 表格
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            // 注意:s1 和 s2 的索引要减 1
            if (s1[i - 1] == s2[j - 1]) {
                // 字符匹配,长度 + 1
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                // 字符不匹配,取左边和上边的最大值
                dp[i][j] = MAX(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }

    int result = dp[n][m];

    // 3. 释放动态分配的内存
    for (int i = 0; i <= n; i++) {
        free(dp[i]);
    }
    free(dp);

    return result;
}

VII.3 总结与提炼:动态规划的要点

DP 核心要素

硬核精髓与陷阱

应对策略

状态定义

必须明确 $DP[i]$ 或 $DP[i][j]$ 的物理含义,通常是“子问题的最优解”。

状态定义必须包含所有必要信息,能通过其值推导出下一状态。

状态转移

正确写出递推关系,确保无后效性(当前状态只依赖于之前已解决的状态)。

仔细分析最后一步:是匹配?是选择?还是继承?

空间优化

如果 $DP[i]$ 只依赖于前 $k$ 个状态,则可以利用滚动数组或变量,将空间从 $O(N)$ 优化到 $O(k)$。

空间优化是锦上添花,但 $O(1)$ 的斐波那契是面试加分项。

============================================================

VIII. 递归回溯:系统地探索解空间

回溯法(Backtracking)是一种深度优先搜索(DFS)的特殊应用,用于在解空间树中搜索满足约束条件的解。

VIII.1 案例精研:组合问题(Subsets, BM57)

原问题:给定一组不含重复元素的整数,返回所有可能的子集(组合)。

VIII.1.1 核心思路:回溯模板与路径记录

回溯法的核心是决策树的遍历。在每个节点,我们有两个选择:

  1. 选择 (Choose): 将当前元素加入路径。

  2. 不选择 (Not Choose): 跳过当前元素,进入下一个元素的决策。

回溯模板包括三个步骤:

  1. 选择 (Selection): 将当前元素加入结果列表(路径)。

  2. 递归 (Recurse): 递归调用下一层,探索基于当前选择的后续可能性。

  3. 撤销选择 (Unselect/Backtrack): 从结果列表撤销当前元素,为同一层的其他选择腾出空间。

C 语言的难点: 在 C 语言中,回溯需要手动管理动态数组(int*int**)和内存,以存储所有路径。

VIII.1.2 C 代码的实现:回溯模板

由于 C 语言实现多维动态数组和内存管理复杂,下面的代码将以伪代码/核心逻辑展示回溯思想,并聚焦于路径的深度优先探索

#include <stdio.h>
#include <stdlib.h>

// 辅助函数:将当前路径添加到最终结果集(需要复杂的内存管理)
void addResult(int** result, int* resultSize, int* path, int pathLen);

/**
 * @brief 回溯核心函数:用于生成子集
 *
 * @param nums 原始数组
 * @param numsLen 数组长度
 * @param start 当前选择的起始索引
 * @param path 当前已选择的子集路径
 * @param pathLen 当前路径的长度
 * @param result 最终结果集
 * @param resultSize 结果集中的子集数量
 */
void backtrack_subsets(int* nums, int numsLen, int start, 
                       int* path, int* pathLen, 
                       int*** result, int* resultSize) {
    
    // ** 1. 达到叶子节点 / 收集结果 **
    // 每次递归进来,path 都是一个合法的子集,将其加入结果集
    // 实际 C 语言实现中,这一步涉及深拷贝和内存分配,这里简化为注释
    // addResult(result, resultSize, path, *pathLen);
    
    // 打印当前路径(仅用于演示):
    printf("{");
    for (int i = 0; i < *pathLen; i++) {
        printf("%d%s", path[i], i == *pathLen - 1 ? "" : ",");
    }
    printf("}\n");

    // ** 2. 探索下一层所有可能性 **
    for (int i = start; i < numsLen; i++) {
        
        // ** (A) 选择 (Choose):将 nums[i] 加入当前路径 **
        path[*pathLen] = nums[i];
        (*pathLen)++;

        // ** (B) 递归 (Recurse):基于当前选择,探索后续可能性 **
        // 从 i + 1 开始,避免重复(即 [1, 2] 和 [2, 1] 视作同一个组合)
        backtrack_subsets(nums, numsLen, i + 1, path, pathLen, result, resultSize);

        // ** (C) 撤销选择 (Unselect/Backtrack):恢复到进入递归前的状态 **
        // 这一步是回溯法的精髓,它将 path 还原,以便 for 循环能尝试下一个元素
        (*pathLen)--; 
    }
}

/**
 * @brief 驱动函数:调用回溯算法
 */
void findSubsets(int* nums, int numsLen) {
    // 最大的子集数量是 2^n,需要预估内存。
    // path 数组最大长度为 numsLen
    int* path = (int*)malloc(numsLen * sizeof(int));
    int pathLen = 0;
    
    // 结果集(实际应用中,需要动态内存分配管理)
    int** result = NULL;
    int resultSize = 0;

    printf("所有子集:\n");
    // 从索引 0 开始探索
    backtrack_subsets(nums, numsLen, 0, path, &pathLen, &result, &resultSize);
    
    free(path); // 释放路径数组
    // 实际应用中,还需要释放 result 及其内部所有子数组
}

/* // 示例运行
int main() {
    int nums[] = {1, 2, 3};
    int len = 3;
    findSubsets(nums, len);
    return 0;
}
// 输出:
// {}
// {1}
// {1,2}
// {1,2,3}
// {1,3}
// {2}
// {2,3}
// {3}
*/

VIII.2 总结与提炼:递归回溯的要点

回溯核心要素

硬核精髓与陷阱

应对策略

路径与状态

路径(Path)是当前已做的选择。回溯必须保证路径可逆转。

使用数组记录路径,在回溯前深拷贝结果,并用栈/数组操作实现撤销。

终止条件

达到树的底部(如 start == numsLen)或满足特定约束(如目标和)。

明确递归什么时候停止,并在停止时记录有效解。

剪枝 (Pruning)

提前发现当前路径不可能得到有效解,从而终止递归。

适用于排列问题(例如:当前和已超目标),能大幅优化性能。

C 语言挑战

动态结果集的内存管理是最大的陷阱,容易造成内存泄漏。

必须在 addResult 阶段为新的子集分配内存(malloc),并在主函数结束时统一释放。

============================================================

【第四部分总结与展望】

恭喜!你现在已经掌握了 C 语言刷题内核指南的全部四大主题:

  1. 数据结构与指针: 字符串、链表、指针操作。

  2. 核心算法基础: 位运算、二分查找。

  3. 高级算法范式: 动态规划(DP)和递归回溯。

这些是构成 C 语言算法面试的基石。如果想进一步提升,请关注之后的博文,我们可以继续刷题 继续提升

####### 最后的附录 源码11* 100行 = 1.4k行 源码:

字符串部分:

1 字符串变形:

思路: 反转 + 大小写+ 单词内部转换!

// /**
//  * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
//  *
//  *
//  * @param s string字符串
//  * @param n int整型
//  * @return string字符串
//  */

// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>
// #include <ctype.h>

// void reverse(char *s, int start, int end)
// {
//     int left = start;
//     int right = end;
//     while (left < right)
//     {
//         char temp = s[left];
//         s[left] = s[right];
//         s[right] = temp;
//         left++;
//         right--;
//     }
// }

// char *trans(char *s, int n)
// {
//     char *res = (char *)malloc((n + 1) * sizeof(char));
//     // write code here
//     // 首先这个字符串中包含着一些空格,就像"Hello World"一样,然后我们要做的是把这个字符串中由空格隔开的单词反序,同时反转每个字符的大小写。
//     // 比如"Hello World"变形后就变成了"wORLD hELLO"
//     // 1 hELLO wORLD 大小写转换
//     // 2 DLROw OLLEh 整个反过来
//     // 3 wORLD hELLO每个单词反过来

//     int i = 0;
//     // 1直接把整个字符大小写替换
//     int len = strlen(s);
//     for (int i = 0; i < len; i++)
//     {
//         // 如果是大写
//         char x = s[i];
//         if (x >= 'A' && x <= 'Z')
//         {
//             printf("原来是%c\n", x);

//             x = tolower(x);
//         }
//         // 小写
//         else
//         {
//             printf("原来是%c\n", x);
//             // printf("现在是%c\n",x);
//             x = toupper(x);
//         }
//         res[i] = x;
//         printf("函数里处理完:<  %c > \n", res[i]);
//         // 小写
//     }
//     // for(int i =0;i<n;i++){
//     //     printf("总的来说 函数里变成:《%s》\n  ",res);
//     // }
//     printf("1 第一步结束 函数里变成:《%s》\n  ", res);

//     // 2把头尾转换
//     reverse(res, 0, n-1);
//     printf("第二部结束,现在是 %s \n",res);

//     // 3 对每个单词反序
//     int left = 0;
//     for(int i =0;i<=n;i++){
//         if(res[i]==' ' || i==n ){
//             reverse(res,left,i-1);
//             left = i+1;
//         }
//     }
//     printf("第三步结束之后:%s \n",res);
//     return res;
// }

// int main()
// {

//     char *juzi = "Caonima Trump Fuck You";
//     int len = strlen(juzi);
//     printf("处理完了!----\n");
//     printf("%s \n", trans(juzi, len));
//     return 0;
// }

// #3刷
// void reverse(char *s, int left, int right)
// {

//     while (left < right)
//     {
//         int x = 0;
//         char *t = &x;
//         *t = s[left];
//         s[left] = s[right];
//         s[right] = *t;
//         left++;
//         right--;
//     }
// }

// char *trans(char *s, int n)
// {
//     // write code here
//     // 反转大小写、顺序、单词顺序
//     char *res = (char *)malloc((n + 1) * sizeof(char));
//     for (int i = 0; i < n; i++)
//     {
//         if (s[i] >= 'a' && s[i] <= 'z')
//         {
//             res[i] = s[i] - 32;
//         }
//         else if (s[i] >= 'A' && s[i] <= 'Z')
//         {
//             res[i] = s[i] + 32;
//         }
//         else
//         {
//             res[i] = s[i];
//         }
//     }
//     // 反转整个字符串
//     reverse(res, 0, n - 1);
//     // 把每个单词反转
//     int start = 0, i = 0;
//     // while (i < n)
//     // {
//     //     while (start != ' ')
//     //     {
//     //         start++;
//     //     }
//     //     reverse(res, i, start);
//     //     while (i < start)
//     //     {
//     //         i++;
//     //     }
//     // }
//     for (int i = 0; i <= n; i++)
//     {
//         if (res[i] == ' ' || res[i] == '\0')
//         {
//             reverse(res, start, i - 1);
//             start = i + 1;
//         }
//     }
//     res[n] = '\0';
//     return res;
// }

/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param s string字符串
 * @param n int整型
 * @return string字符串
 */

//  4刷
#include <stdio.h>
#include <stdlib.h>

void reverse(char *s, int l, int r)
{
    while (l < r)
    {
        char t = s[l];
        s[l] = s[r];
        s[r] = t;
        l++;
        r--;
    }
    return;
}

void funCapital(char *s)
{
    char *x = s;
    while (*x != '\0')
    {
        if (*x >= 'a' && *x <= 'z')
        {
            *x = *x - 32;
        }
        else if (*x >= 'A' && *x <= 'Z')
        {
            *x = *x + 32;
        }
        x++;
    }
}

char *trans(char *s, int n)
{
    // write code here
    reverse(s, 0, n - 1);
    funCapital(s);

    int start = 0, j = 0, i = 0;
    for (; i < n; i++)
    {
        if (s[i] == ' ')
        {
            reverse(s, start, i - 1);
            start = i + 1;
        }
    }

    reverse(s, start, n - 1);
    return s;
}

2 最长公共前缀?

思路:

竖向横向遍历!

代码:

// /**
//  * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
//  *
//  *
//  * @param strs string字符串一维数组
//  * @param strsLen int strs数组长度
//  * @return string字符串
//  */
// #include <string.h>
// char *longestCommonPrefix(char **strs, int strsLen)
// {
//     // write code here
//     if (strsLen == 0)
//     {
//         char *res = (char *)malloc(1 * sizeof(char));
//         res[0] = '\0';
//         return res;
//     }

//     int minLen = 5000;
//     for (int i = 0; i < strsLen; i++)
//     {
//         int tempLen = strlen(strs[i]);
//         // strlen是个函数 求的是实际长度
//         minLen = tempLen < minLen ? tempLen : minLen;
//     }
//     char *res = (char *)malloc((minLen + 1) * sizeof(char));

//     if (strsLen == 1)
//     {
//         // res[0] = strs[0][0];
//         // res[0][1] = strs[0][1];
//         // #!!!!vip
//         // 直接用strncpuy函数

//         strncpy(res, strs[0], strlen(strs[0]));
//         // res[1] = '\0';
//         res[strlen(strs[0]) + 1] = '\0';
//         return res;
//     }
//     res[0] = strs[0][0];

//     int validLen = minLen;
//     int i = 0;
//     for (; i < validLen; i++)
//     {
//         char c = strs[0][i];
//         int j = 1;
//         for (; j < strsLen; j++)
//         {
//             if (strs[j][i] != c)
//             {
//                 break;
//             }
//         }
//         if (j != strsLen)
//         {
//             break;
//         }
//         res[i] = c;
//     }
//     // int len = strlen(res);
//     res[i] = '\0';
//     return res;
// }

// 3刷
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param strs string字符串一维数组
 * @param strsLen int strs数组长度
 * @return string字符串
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *longestCommonPrefix(char **strs, int strsLen)
{
    // write code here

    int maxLen = 10000;
    int count = 0;
    // char res[5001];
    char *res = (char *)malloc(5001 * sizeof(char));
    if (strs == NULL || strsLen == 0)
    {
        return res;
    }
    for (int i = 0; i < strsLen; i++)
    {
        maxLen = maxLen < strlen(strs[i]) ? maxLen : strlen(strs[i]);
    }

    // 最长也不可能超过这个长度
    //   888
    //   88888
    //   8888  图示的这个意思
    for (int i = 0; i < maxLen; i++)
    {
        char temp = strs[0][i];
        for (int j = 1; j < strsLen; j++)
        {
            if (strs[j][i] != temp)
            {
                res[count] = '\0';
                return res;
            }
        }
        res[count++] = strs[0][i];
    }
    res[count] = '\0';
    return res;
}

3 验证ip地址:

未作!!

4 大叔加法?

思路:  从后往前,一个个处理,cnt计数!

代码:

// 2刷
// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>

// char *solve(char *s, char *t)
// {
//     // 1233333 +333333333187689175
//     // len1-1 + len2-1开始,加成n1+n2+flag 必须要( ij都存在 or s[i]+s[j]+flag>0)

//     // if (*s == '\0')
//     // {
//     //     return (*t == '\0') ? strdup("0") : strdup(t);
//     // }
//     // if (*t == '\0')
//     // {
//     //     return strdup(s);
//     // }
//     // int len1 = strlen(s);
//     // int len2 = strlen(t);

//     // if (len2 > len1)
//     // {
//     //     char *temp = s;
//     //     s = t;
//     //     t = temp;
//     // }
//     // len1 = strlen(s);
//     // len2 = strlen(t);
//     // // 让左边这个是大的

//     // // 从末尾开始,出来仅为
//     // int maxLen = len1 > len2 ? len1 : len2;
//     // int flag = 0;
//     // char *res = (char *)malloc((maxLen + 2) * sizeof(char));
//     // int count = 0;
//     // int i = len1 - 1, j = len2 - 1;
//     // for (; j >= 0;)
//     // {
//     //     int curInt = s[i--] - '0' + t[j--] - '0' + flag;
//     //     res[count++] = curInt % 10 + '0';
//     //     flag = curInt / 10;
//     // }
//     // // 处理完了t,较短部分的全部处理了

//     // // 处理剩下的s字符串
//     // while (i >= 0 || flag > 0)
//     // {
//     //     // #self !!!vip 及其容易错@!!!
//     //     int n1 = (i >= 0) ? (s[i] - '0') : 0;
//     //     i--;
//     //     int curInt = n1 + flag;
//     //     res[count++] = curInt % 10 + '0';
//     //     flag = curInt / 10;
//     // }
//     // // 反转
//     // //  res[count ]='\0';

//     // for (int i = 0; i < count / 2; i++)
//     // {
//     //     char x = res[i];
//     //     res[i] = res[count - 1 - i];
//     //     res[count - 1 - i] = x;
//     // }
//     // return res;

//     if (*s == '\0')
//     {
//         return (*t == '\0') ? strdup("0") : strdup(t);
//     }
//     if (*t == '\0')
//     {
//         return strdup(s);
//     }

//     int len1 = strlen(s), len2 = strlen(t);
//     if (len1 < len2)
//     {
//         char *temp = s;
//         s = t;
//         t = temp;
//         len1 = strlen(s);
//         len2 = strlen(t);
//     }

//     int count = 0;
//     int flag = 0;
//     char *res = (char *)malloc((len1 + 2) * sizeof(char));
//     int i = len1 - 1, j = len2 - 1;
//     int sum = 0;
//     while (j >= 0)
//     {
//         sum = s[i] - '0' + t[j] - '0' + flag;
//         res[count] = (sum % 10) + '0';
//         i--;
//         j--;
//         count++;
//         flag = sum / 10;
//     }
//     while (i >= 0 || flag > 0)
//     {
//         if (i < 0)
//         {
//             sum = flag;
//         }
//         else
//         {
//             sum = s[i] - '0' + flag;
//         }
//         i--;
//         res[count++] = (sum % 10) + '0';
//         flag = sum / 10;
//     }
//     res[count] = '\0';
//     for (int i = 0; i < count / 2; i++)
//     {
//         char temp = res[i];
//         res[i] = res[count - 1 - i];
//         res[count - 1 - i] = temp;
//     }
//     return res;
// }

// int main()
// {
//     char *s = "1234567891841";
//     char *t = "9999999999";
//     printf("%s \n", solve(s, t));
// }

//  * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
//  *
//  * 计算两个数之和
//  * @param s string字符串 表示第一个整数
//  * @param t string字符串 表示第二个整数
//  * @return string字符串
//  */

// #include <stdio.h>
// #include <stdlib.h>
// void reverse(char *s, int left, int right)
// {
//     while (left < right)
//     {
//         char temp = s[left];
//         s[left] = s[right];
//         s[right] = temp;
//         left++;
//         right--;
//     }
// }

// char *solve(char *s, char *t)
// {
//     // write code here
//     int len1 = strlen(s);
//     int len2 = strlen(t);
//     int maxLen = len1 > len2 ? len1 : len2;
//     int flag = 0;
//     char *res = (char *)malloc((maxLen + 1) * sizeof(char));
//     int i = len1 - 1, j = len2 - 1;
//     int count = 0;
//     // int k =
//     while (i >= 0 || j >= 0 || flag > 0)
//     {
//         int a = i >= 0 ? s[i] - '0' : 0;
//         int b = j >= 0 ? t[j] - '0' : 0;
//         int sum = a + b + flag;
//         int tempRes = sum % 10;
//         flag = sum / 10;
//         res[count] = tempRes + '0';
//         count++;
//         i--;
//         j--;
//     }
//     reverse(res, 0, count - 1);

//     res[count] = '\0';
//     return res;
// }

// int main(void)
// {

//     char a[] = "9";
//     char b[] = "99999999999999999999999999999999999999999999999999999999999994";
//     printf("%s \n", solve(a, b));
//     return 0;
// }

// 3刷
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 * 计算两个数之和
 * @param s string字符串 表示第一个整数
 * @param t string字符串 表示第二个整数
 * @return string字符串
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

void reverse(char *a, int l, int r)
{
    while (l < r)
    {
        char tmp = a[l];
        a[l] = a[r];
        a[r] = tmp;
        l++;
        r--;
    }
    return;
}

char *solve(char *s, char *t)
{
    // write code here
    // 1 确定最长的可能性  2 倒过来 1234  98765  变成
    //                            4321  56789
    // 3 c / s 进位、和,while循环
    //
    if (strlen(s) == 0 && strlen(t) != 0)
    {
        return t;
    }
    if (strlen(t) == 0 && strlen(s) != 0)
    {
        return s;
    }
    if (!s && !t)
    {
        return NULL;
    }
    // reverse(s, 0, strlen(s) - 1);
    // reverse(t, 0, strlen(t) - 1);

    int maxLen = strlen(s) > strlen(t) ? strlen(s) + 2 : strlen(t) + 2;
    char *res = (char *)malloc((maxLen + 1) * sizeof(char));
    int i = strlen(s) - 1, j = strlen(t) - 1, cnt = 0, carry = 0, sum = 0;
    while (i >= 0 || j >= 0 || carry != 0)
    {
        int a = i >= 0 ? s[i--] - '0' : 0;
        int b = j >= 0 ? t[j--] - '0' : 0;
        sum = (a + b + carry) % 10;

        carry = (a + b + carry) / 10;
        res[cnt++] = sum + '0';
    }
    reverse(res, 0, cnt - 1);
    res[cnt] = '\0';
    return res;
}

双指针部分:

1 合并2个有序数组?

思路:直接往a里面塞,一个个来往里面塞进去!

代码:


/**
 *
 * @param A int整型一维数组
 * @param ALen int A数组长度
 * @param B int整型一维数组
 * @param BLen int B数组长度
 * @return void
 */
void merge(int *A, int ALen, int m, int *B, int BLen, int n)
{
    // write code here
    // 1刷
    //!!!!vip #self 完全错了
    // int *pa = A;
    // int *pb = B;
    // int *test = (int *)malloc(sizeof(int));
    // int *ptr = test;
    // int cnt = 0;
    // int i = 0, j = 0;
    // while (i < m && j < n)
    // {
    //     if (*pa < *pb)
    //     {
    //         p->next = pa;
    //         i++;
    //         pa->next = pa;
    //     }
    //     else
    //     {
    //         p->next = pb;
    //         j++;
    //         pb->next = pb;
    //     }
    //     p = p->next;
    // }
    // if (pa == NULL)
    // {
    //     p->next = pb;
    // }
    // else
    // {
    //     p->next = pa;
    // }
    // return NULL;

    // 应该直接从末尾开始合并
    int i = m - 1;
    int j = n - 1;
    int cnt = m + n - 1;

    while (i >= 0 && j >= 0)
    {
        if (A[i] > B[j])
        {
            A[cnt--] = A[i--];
        }
        else
        {
            A[cnt--] = B[j--];
        }
    }
    while (j >= 0)
    {
        A[cnt--] = B[j--];
    }
}

2 判断是否回文字符?

思路:从start和end一个个开始找!

代码:

/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param str string字符串 待判断的字符串
 * @return bool布尔型
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

bool judge(char *str)
{
    // write code here

    // 2 刷
    int len = strlen(str);
    int l = 0, r = len - 1;
    while (l < r)
    {
        if (str[l++] != str[r--])
        {
            return false;
        }
    }
    return true;
}

2 合并区间?

思路:画一张图!end和start去比较!不停跟新里面的边界条件!

代码:

/**
 * struct Interval {
 *	int start;
 *	int end;
 * };
 */
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param intervals Interval类一维数组
 * @param intervalsLen int intervals数组长度
 * @return Interval类一维数组
 * @return int* returnSize 返回数组行数
 */
// !!!vip
// 3刷
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int cmpFn(const void *a, const void *b)
{
    return ((struct Interval *)a)->start - ((struct Interval *)b)->start;
}

struct Interval *merge(struct Interval *intervals, int intervalsLen, int *returnSize)
{

    if (intervals == NULL || intervalsLen == 0)
    {
        *returnSize = 0;
        return NULL;
    }
    if (intervalsLen == 1)
    {
        *returnSize = 1;
        return intervals;
    }
    // write code here
    // 思路 全部按照第一个位置的元素重新排序:
    // 1,3
    //   2,3
    //  1,4
    //    5,6
    //    2,5
    //    4,5
    //    6,9
    //    7 ,10
    // 1 按照第一个排序,
    // 2 如果i[end]一个个开始往后排序
    // 3 如果set[i][end]>set[i+1][end],直接更新set[i][end ] = set[i+1][end]
    qsort(intervals, intervalsLen, sizeof(intervals[0]), cmpFn);
    // funSort()
    // 遍历整个数组,重复上面整个过程,
    // 3刷的时候欠缺: 后面的思维过程, 如何跟新整个res结果的数组!
    int cnt = 0;
    struct Interval *res = (struct Interval *)malloc(intervalsLen * sizeof(struct Interval));
    res[0] = intervals[0];

    for (int i = 1; i < intervalsLen; i++)
    {
        if (intervals[i].start <= res[cnt].end)
        {
            if (intervals[i].end > res[cnt].end)
            {
                res[cnt].end = intervals[i].end;
            }
        }
        else
        {
            cnt++;
            res[cnt] = intervals[i];
        }
    }
    *returnSize = cnt + 1;
    return res;
}

4 反转字符串?

思路:

弱智题目

代码:

/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 * 反转字符串
 * @param str string字符串
 * @return string字符串
 */
#include <stdlib.h>
#include <string.h>
char *solve(char *str)
{
    // write code here
    if (str == NULL || strlen(str) == 1)
    {
        return str;
    }
    int left = 0, right = strlen(str) - 1;
    while (left < right)
    {
        char x = str[left];
        str[left] = str[right];
        str[right] = x;
        left++;
        right--;
    }
    return str;
}

5 最长无重复字符串?

思路:left right分别维护一个最旧、最新的边界1

代码:

// /**
//  * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
//  *
//  *
//  * @param arr int整型一维数组 the array
//  * @param arrLen int arr数组长度
//  * @return int整型
//  */
// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>

// int max(int a, int b)
// {
//     return a > b ? a : b;
// }
// int maxLength(int *arr, int arrLen)
// {
//     // write code here
//     // 最长的无重复数组

//     int left = 0, right = 0;
//     int maxLen = 0;
//     int hash[100001] = {0};
//     while (right < arrLen)
//     {

//         int cur = arr[right];
//         while (hash[cur] > 0)
//         {
//             hash[arr[left]]--;
//             left++;
//         }
//         hash[cur]++;
//         maxLen = max(maxLen, right - left + 1);
//         right++;
//     }
//     return maxLen;
// }

// int main()
// {

//     int arr[] = {1, 2, 3, 3, 1, 2, 3, 5, 6, 6};
//     int len = sizeof(arr)/sizeof(arr[0]);
//     printf("%d\n", maxLength(arr, len));
//     return 0;
// }

// #2刷
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param arr int整型一维数组 the array
 * @param arrLen int arr数组长度
 * @return int整型
 */

// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>

// int maxLength(int *arr, int arrLen)
// {
//     // write code here
//     int left = 0, right = 0;
//     int *hash = (int *)malloc(100001 * sizeof(int));
//     int maxLen = 0;
//     memset(hash, -1, sizeof(int) * 100001);
//     for (right = 0; right < arrLen; right++)
//     {
//         int t = arr[right];
//         if (hash[t] >= left && hash[t] != -1)
//         {
//             left = hash[t] + 1;
//             // 这里为什么是hash[t]+1,比如[1,2,1] left =0,left = hash[t]+1,t重复了,hash[t]+1,就是新的一个位置
//         }
//         hash[t] = right;

//         int tempLen = right - left + 1;
//         maxLen = maxLen > tempLen ? maxLen : tempLen;
//     }
//     return maxLen;
// }

// # 4刷
//!!!vip 自己做的时候没想到这个好方法
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param arr int整型一维数组 the array
 * @param arrLen int arr数组长度
 * @return int整型
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int maxLength(int *arr, int arrLen)
{
    // write code here
    int *hash = (int *)malloc(100000 * sizeof(int));
    memset(hash, -1, 100000 * sizeof(int));
    int left = 0, right = 0;
    int maxLen = 0;
    for (int right = 0; right < arrLen; right++)
    {
        if (hash[arr[right]] != -1 && hash[arr[right]] >= left)
        {
            left = hash[arr[right]] + 1;
        }
        hash[arr[right]] = right;
        maxLen = maxLen > (right - left + 1) ? maxLen : (right - left + 1);
    }
    return maxLen;
}

6 :盛水容器?

思路:

那边小,收缩哪个!

代码:

// /**
//  * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
//  *
//  *
//  * @param arr int整型一维数组 the array
//  * @param arrLen int arr数组长度
//  * @return int整型
//  */
// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>

// int max(int a, int b)
// {
//     return a > b ? a : b;
// }
// int maxLength(int *arr, int arrLen)
// {
//     // write code here
//     // 最长的无重复数组

//     int left = 0, right = 0;
//     int maxLen = 0;
//     int hash[100001] = {0};
//     while (right < arrLen)
//     {

//         int cur = arr[right];
//         while (hash[cur] > 0)
//         {
//             hash[arr[left]]--;
//             left++;
//         }
//         hash[cur]++;
//         maxLen = max(maxLen, right - left + 1);
//         right++;
//     }
//     return maxLen;
// }

// int main()
// {

//     int arr[] = {1, 2, 3, 3, 1, 2, 3, 5, 6, 6};
//     int len = sizeof(arr)/sizeof(arr[0]);
//     printf("%d\n", maxLength(arr, len));
//     return 0;
// }

// #2刷
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param arr int整型一维数组 the array
 * @param arrLen int arr数组长度
 * @return int整型
 */

// #include <stdio.h>
// #include <stdlib.h>
// #include <string.h>

// int maxLength(int *arr, int arrLen)
// {
//     // write code here
//     int left = 0, right = 0;
//     int *hash = (int *)malloc(100001 * sizeof(int));
//     int maxLen = 0;
//     memset(hash, -1, sizeof(int) * 100001);
//     for (right = 0; right < arrLen; right++)
//     {
//         int t = arr[right];
//         if (hash[t] >= left && hash[t] != -1)
//         {
//             left = hash[t] + 1;
//             // 这里为什么是hash[t]+1,比如[1,2,1] left =0,left = hash[t]+1,t重复了,hash[t]+1,就是新的一个位置
//         }
//         hash[t] = right;

//         int tempLen = right - left + 1;
//         maxLen = maxLen > tempLen ? maxLen : tempLen;
//     }
//     return maxLen;
// }

// # 4刷
//!!!vip 自己做的时候没想到这个好方法
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 *
 * @param arr int整型一维数组 the array
 * @param arrLen int arr数组长度
 * @return int整型
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int maxLength(int *arr, int arrLen)
{
    // write code here
    int *hash = (int *)malloc(100000 * sizeof(int));
    memset(hash, -1, 100000 * sizeof(int));
    int left = 0, right = 0;
    int maxLen = 0;
    for (int right = 0; right < arrLen; right++)
    {
        if (hash[arr[right]] != -1 && hash[arr[right]] >= left)
        {
            left = hash[arr[right]] + 1;
        }
        hash[arr[right]] = right;
        maxLen = maxLen > (right - left + 1) ? maxLen : (right - left + 1);
    }
    return maxLen;
}

贪心算法: 

主持人问题?

思路i:一个个去算,cnt和i j不断往后更新,感性思维!

代码:

// /**
//  * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
//  *
//  * 计算成功举办活动需要多少名主持人
//  * @param n int整型 有n个活动
//  * @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
//  * @param startEndRowLen int startEnd数组行数
//  * @param startEndColLen int* startEnd数组列数
//  * @return int整型
//  */

// #include <stdio.h>
// #include <stdlib.h>
// int cmp(const void *a, const void *b)
// {
//     // return *(int *)a - *(int *)b;
//     int x = *(int *)a;
//     int y = *(int *)b;
//     if (x < y)
//     {
//         return -1;
//     }
//     else if (x > y)
//     {
//         return 1
//     }
//     else
//     {
//         return 0;
//     }
// }

// int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen)
// {
//     // write code here
//     int *start = (int *)malloc(n * sizeof(int));
//     int *end = (int *)malloc(n * sizeof(int));
//     for (int i = 0; i < n; i++)
//     {
//         start[i] = startEnd[i][0];
//         end[i] = startEnd[i][1];
//     }
//     qsort(start, n, sizeof(int), cmp);
//     qsort(end, n, sizeof(int), cmp);

//     int i = 0, j = 0, count = 0, maxLen = 0;
//     while (i < n)
//     {
//         if (start[i] < end[j])
//         {
//             i++;
//             count++;
//             if (count > maxLen)
//             {
//                 maxLen = count;
//             }
//         }
//         else
//         {
//             count--;
//             j++;
//         }
//     }
//     return maxLen;
// }

// 2刷 25.10.19
/**
 * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
 *
 * 计算成功举办活动需要多少名主持人
 * @param n int整型 有n个活动
 * @param startEnd int整型二维数组 startEnd[i][0]用于表示第i个活动的开始时间,startEnd[i][1]表示第i个活动的结束时间
 * @param startEndRowLen int startEnd数组行数
 * @param startEndColLen int* startEnd数组列数
 * @return int整型
 */

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
int funCmp(const void *a, const void *b)
{
    //!!!vip #self 为什么?
    // return *(int *)a - *(int *)b;
    // double x = *((*double)a);
    // double y = *((*double)b);
    // return (int)(*x - *y);
    int x = *(int *)a;
    int y = *(int *)b;
    if (x > y)
    {
        return 1;
    }
    else if (x < y)
    {
        return -1;
    }
    else
    {
        return 0;
    }
}

int minmumNumberOfHost(int n, int **startEnd, int startEndRowLen, int *startEndColLen)
{
    // write code here
    // qsort(startEnd, n, sizeof(startEnd[0]), cmp);
    int *start = (int *)malloc(n * sizeof(int));
    int *end = (int *)malloc(n * sizeof(int));
    for (int i = 0; i < n; i++)
    {
        start[i] = startEnd[i][0];
        end[i] = startEnd[i][1];
    }
    qsort(start, n, sizeof(start[0]), funCmp);
    qsort(end, n, sizeof(end[0]), funCmp);

    int i = 0, j = 0, count = 0, maxNum = 0;
    while (i < n)
    {
        if (start[i] < end[j])
        {
            i++;
            count++;
            if (count > maxNum)
            {
                maxNum = count;
            }
        }
        else
        {
            j++;
            count--;
        }
    }
    return maxNum;
}

!!!目前,25年10月,从3月开始已经完成了牛客热题101 1+2easymiddle题目的全部3-4遍刷!

基本的思路已经成型,可以做i到无答案说出思+70%的代码能20分钟内写出来!

继续努力,
接下来继续完成:

1 笔试101榜单开始做!

2 牛客周赛!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值