字符串

1、字符串基本:

字符串是算法题目中的一个大类,其实c语言对字符的操作并不是很友好,c语言本身并没有封装很多的c语言的函数,但是以下几种基本的函数是我们需要掌握的:

1.1、strcpy函数

原型:strcpy(str1,str2);

功能:将字符串str2复制到字符串str1中,并覆盖str1原始字符串,可以用来为字符串变量赋值

返回:str1

注意:

  1)字符串str2会覆盖str1中的全部字符,

  2)字符串str2的长度不能超过str1,

  3)  拷贝原理:从首元素开始,遇到\0结束

int main(int argc, char const *argv[])
{
    char *str1 = "hello world";
    char *str2;

    // 功能:把str1的内容拷贝到str2,参数为字符数组指针
    strcpy(str2, str1);

    printf("str2 = %s\n", str2);

    char str3[15] = "hello\0 world";
    char str4[15];

    //拷贝原理:从首元素开始,遇到\0结束
    strcpy(str4, str3);

    printf("str4 = %s\n", str4);
    return 0;
}

输出:

str2 = hello world
str4 = hello

1.2、strncpy函数

原型:strncpy(str1,str2,n);

功能:将字符串str2中的前n个字符复制到字符串str1的前n个字符中

返回:str1

注意:

  1)不会清除str1中全部字符串,只会改变前n个字符串,

  2)n不能大于字符串str1、str2的长度

  3)但是如果使用strncpy_s便会清除str1中的全部字符串

int main(int argc, char const *argv[])
{
    char str1[] = "day day up";
    char str2[] = "you are";
    strncpy(str1, str2, strlen(str2));
    printf("%s\n", str1); // you are up
    return 0;
}

1.3、strcat函数

原型:strcat(str1,str2);

功能:将字符串str2添加到字符串str1的尾部,也就是拼接两个字符串

返回:str1

int main(int argc, char const *argv[])
{
    char str1[] = "hello ";
    char str2[] = "world";
    strcat(str1, str2); // hello world
    printf("%s\n", str1);
    return 0;
}

1.4、strncat函数

原型:strncat(str1,str2,n);

功能:将字符串str2的前n个字符添加到字符串str1的尾部

返回:str1

注意:拼接之后的长度不能超过字符串数组str1的长度

int main(int argc, char const *argv[])
{
    char str1[] = "hello ";
    char str2[] = "world";
    strncat(str1, str2, 2); // hello wo
    printf("%s\n", str1);
    return 0;
}

1.5、strlen函数

原型:strlen(str1);

功能:计算字符串str1的长度

返回:一个int值

注意:字符串的长度不包括字符'\0'

{
    char *str1 = "hello world";
    char *str2 = "hello,\0 kite";
    int l1 = strlen(str1);
    int l2 = strlen(str2);
    printf("l1 = %d\n", l1); // 11
    printf("l2 = %d\n", l2); // 6
    return 0;
}

1.6、strcmp函数

原型:strcmp(str1,str2);

功能:比较两个字符串,如果两个字符串相等,则返回0;若str1大于str2(对于大于的理解,是指从两个字符串的第一个字符开始比较,若两个字符相同,则继续比较,若发现两个字符不相等,且str1中该字符的ASCII码大于str2中的,则表示str1大于str2),返回一个正数(这个正数不一定是1);若str1小于str2,返回一个负数(不一定是-1);若字符串str1的长度大于str2,且str2的字符与str1前面的字符相同,则也相对于str1大于str2处理

原型2:strncmp(str1,str2,n);

功能2:比较两个字符串的前n个字符

原型3:stricmp(str1,str2); (在Windows中使用stricmp,在Linux中使用strcasecmp)

功能3:忽略两个字符串中的大小写比较字符串,也就是对大小写不敏感

char str1[] = "Wearecsdn!";
char str2[] = "Wearecsdn!";
char str3[] = "Wearea!";
char str4[] = "Wearef!";
char str5[] = "Weare";
char str6[] = "weAreCsdn!";
 
int cmp1 = strcmp(str1, str2);        //cmp1=0
int cmp2 = strcmp(str1, str3);        //cmp2=1
int cmp3 = strcmp(str1, str4);        //cmp3=-1
int cmp4 = strcmp(str1, str5);        //cmp4=1
 
int cmp5 = strncmp(str1, str2, 5);    //cmp5=0
int cmp6 = strncmp(str1, str3, 5);    //cmp6=0
int cmp7 = strncmp(str1, str4, 5);    //cmp7=0
int cmp8 = strncmp(str1, str5, 5);    //cmp8=0
 
int cmp9 = _stricmp(str1, str6);      //cmp9=0

1.7、strchr函数

原型:strchr(str,c);

功能:在str字符串中查找首次出现字符c的位置(从字符串的首地址开始查找)

原型2:strrchr(str,c);

功能2:在字符串str中从后向前开始查找字符c首次出现的位置

原型3:strstr(str1,str2);

功能3:在字符串str1中查找字符串str2的位置,若找到,则返回str2第一个字符在str1中的位置的指针,若没找到,返回NULL

返回:字符c的位置的指针,若没有查找到字符c,则返回空指针NULL

{
    // strchr查询字符 和 strstr查询字符串函数
    char *str = "no one can help you";
    // 在字符串中查询字符 h ,如果存在,返回 h 的地址
    // 如果不存在,返回NULL
    char *s = strchr(str, 'h');
    if(s){
        printf("存在字符:%c\n", *s);
    } else {
        printf("不存在字符\n");
    }

    // 如果存在,返回字符串所在位置的首地址
    // 否则返回NULL
    char *st = strstr(str, "help");
    if(st){
        printf("存在字符串:%p\n", *st); 
        //注意:此处返回字符串所在元素首地址,也就是一个字符串数组,所以不用取地址值*st
        // 因为对一个字符串(字符数粗)取地址值 *buf 是没有用的,它本来就是一个指针,取到的
        // 会是指针的值,而非指针所指向的值
        // 关于指针类型变量:&a取地址是值所在的地址值(指针变量),*st表示指针变量所指向的值,和定义时一样
        // 这两个的操作刚好是相反的
    } else {
        printf("不在在字符串\n");
    }
    return 0;
}

1.8、strpbrk函数

 原型:strpbrk(str1,str2);

功能:依次检验字符串 str1 中的字符,当被检验字符在字符串 str2 中也包含时,则停止检验,并返回该字符位置

返回:第一个两个字符串中都包含的字符在str1中的位置的指针

char str1[] = "We12are34csdn!";
char str2[] = "32";
 
char* ret1;
 
ret1 = strpbrk(str1, str2);   //*ret1 = 2
 
int r1 = ret1 - str1;         //r1 = 3
 
printf("%c\n", *ret1);
printf("%d\n", r1);

1.9、字符串转数字atoi、atof、atol函数

atoi(str);    //字符串转换到int整型

atof(str);    //字符串转换到double浮点数

atol(str);    //字符串转换到long整形

{
    // 函数1,字符串转整型,atoi()
    // 说明:扫描字符串,跳过前面的空格,直到遇到数字或正负号则开始转换,遇到非数字或 \0 结束转换,返回转换的值
    // 来源:stdlib.h头文件
    // 扩展:相似的有 atof, atol 顾名思义,就是转float, long
    // 注意:必须本身就是数字的字符串才能被正确转换
    // 举例:
    char *str = "125";

    int i = atoi(str);
    printf("i = %d\n", i);

    // 数字转字符串
    int j = 10005;
    char arr[20];
    sprintf(arr, "%d", j);  //c语言里面不能直接 + 连接,需要这么格式化
    printf("arr = %s\n", arr);

    return 0;
}

1.10、从基础函数库的思考----暴力比较

       其实上面的基础函数库都是提供的非常简单的函数,我们自己手动也能实现而且必须能很快的自己手动实现出来,这是你的基本能力!我们就基于上面的类库来做自己的扩展,现在我们要找出一个字符在字符串中出现的所有下标:

// 找出字符在字符串中的所有出现位置
// @Param index:找出的出现位置全部放入这个数组
// @Param str:传入的字符串
// @Param ch:需要查找的字符串
// @return :返回值
int find_all(int *index,char * str,char ch){
    // 给个错误提示,请真正的理解上c语言的值传递和引用传递,如果我们给
    // 一个指针进入,想要修改指针的内容可以,但是指针的引用修改了是无用的
    // 必须是:
    // find_all(int **index,....){
    // *index = (int *)malloc(sizeof(int *)*strlen(str));
    // }
    // 上面的这种写法是合格的,简单来说就是参数的指针如果高一个档次
    // 那么就能够修改传入的东西,比如外界是 int a
    // 那么参数是 int *index就行了,如果外界是 int *a
    // 那么传入的参数必须是int **index,如果外界的是int **a
    // 那么传入的就必须是int ***index
    // error------>>int *index = (int *)malloc(sizeof(int *)*strlen(str));

    int count = 0;
    for(int i = 0;i < strlen(str);i++)
        if(str[i] == ch) index[count++] = i;
    return count;
}

ok上面这个我相信对于你来说是小case,下面我们来加大难度,现在我们来实现一个函数,我们要找出一个字符串是否包含另外一个字符串:

函数目标:

        参数传入两个字符串str1和str2,我们要判断str1中是否包含str2 ?

从本小节的题目其实就能看出来,我们直接暴力比较就可以实现,直接两个字符串从头开始比较,如果第一个字符匹配上了,那么打开一个开关!!!(因为接下来的匹配不一定成功,如果不成功那么还需要回来接着比较!!),其实本题可以用递归来实现!请你实现递归版本的!!!

// 我们要判断的是字符串是否包含字符串2!!注意
// @Param str1:字符串1
// @Param str2:字符串2
bool contains(char *str1,char *str2){
    bool flag = false;
    int length1 = strlen(str1);
    int length2 = strlen(str2);
    int remember;
    if(length1<length2) return false;
    int i=0,j=0;
    while(i < length1 && j < length2){
        if(str1[i] == str2[j]){
            flag = true;
            // 后面可能需要会退,记住下标
            remember = i;
            i++;j++;
        }else if(flag){
            flag = false;
            // 不匹配了,那么j需要从头开始匹配,i需要会退
            j = 0;
            I = remember+1;
        }else
            i++; // 就是普通的不匹配,那么str1往后面走一位
    }
    // 走到最后了就说明匹配上了
    return j == length2;
}

ok你肯定会觉得上面的也是小case,别急,等会你看字符串的后续题型就会逐渐具有挑战性了。

上面这种基本就是考的你基本功,不涉及太难的算法, 但是你必须得会,现在布置一个题目如下:

题目1:(Hard)

让我们来实现上面的atoX函数的加强版本,我们只要给的人以的字符串,都能将其转化成响应的数字,其中包括科学记数法:

例如:

"1.234"--------->输出:1.234

"-232.2310000000"-------->输出:-232.231

"+000062313"---------->输出:62313

"00.9342"---------->输出:0.9342

“123.123+-123”------->输出:0(非法的)

“+-123213”-------->输出:0(非法的)

"12*10^3"--------->输出:12000

“+1.987*10^-1”------->输出:0.1987

"++3.627*10^-3"------->输出:0.003627

上面这种题目叫基本功,你得很快的能写出来!!!是考察你思维缜密,细心程度的题目。

TIPS:

那这个题目会涉及到一下的几个方面:

1、前导0的去除,后导0的去除

2、前导加减号的去除

3、非法判断

4、科学记数法的处理(简单来说就是字符串的转化,从一种表示形式转换成另外一种表示形式)

题目2、字符串的压缩:(middle)

字符串压缩。利用字符重复出现的次数,编写一种方法,实现基本的字符串压缩功能。比如,字符串aabcccccaaa会变为a2b1c5a3。若“压缩”后的字符串没有变短,则返回原先的字符串。你可以假设字符串中只包含大小写英文字母(a至z)。

输入:"aabcccccaaa"

输出:"a2b1c5a3"

输入:"abbccd"

输出:"abbccd"    解释:因为压缩后的字符串比原来的还长所以返回原来的字符串

TIPS:

凡是涉及到和字符串的操作相关的,记住几种特殊的边界情况:

1、空字符串

2、就一个字符的情况

3、aa、aaa、aaaa这种重复的特殊形式

4、是否有开头或者结尾出现的特殊形式导致访问越界等问题

 

题目3: 罗马数字和字符串的转化(middle)

TIPS:

类似这类将一种形式转换成另外一种形式的题目,在做题前一定要自己在题目给出的测试用例下再尽量举几组特殊的数据出来

还有就是这类题目往往是可以采用递归的方式解题的,因为我们每次的转换一般来说都是基于局部的,然后再解决局部问题之后又需要递归

解决后续的问题。

这道题目的难点关键在于哪些字母应该组合再一起。

 

题目4、拉丁文转换(easy)

TIPS:

递归,没什么好说的,细心一点按照规则转换。还有就是动手前举几个自己认为特殊的案例,确认自己的解法没问题了再动手!

 

题目5、竖直打印

TIPS:

做这类题目请先举出特殊情况在动笔!!!比如就一个字符怎么办?空字符串怎么办?就两个单词怎么办?就一个单词怎么办?

然后再是找规律!!!

 

做上面打印类题目,一般来说思路上不会太难,非常重要的解题技巧就是举几个你认为很特殊的例子,然后再写程序,如果程序实在是出现了bug,那么请用printf打印出来每次循环的情况!!然后根据打印的东西确认哪些边界条件错了!!!!!

2、字符串题目的技巧

2.1、HashMap

       对它来了,它叫做HashMap,这个数据结构很好用,在一些字符串的题目中如果使用HashMap将会带来极大的便利,我举个简单的例子,假设现在我们有10个待判断字符,和一个长字符串,我们现在要判断长的字符串中是否包含这些字符,每个字符包含了几次?那么常规的做法就是每次字符都去便利一次字符串,如下:

int findTimes(char * str, char ch){

    int count = 0;
    for(int I = 0;i < stelen(str);I++){
        if(str[I] == ch) count++; 
    }
    return count;
}

函数很简单,但是如果判断10次那么就要调用10次,时间复杂度上面非常的不美妙,但是如果我采取HashMap的方式:

// 假设全部都是小写字母
int HashMap[26];


// 只需要构建一次即可
void creatHashMap(char *str){
    
    for(int I = 0;i < strlen(str),I++){
        int number =  HashMap[str[I]='a'];
        HashMap[str[I]='a'] = number >= 1?number+1:0;
    }
}


// 无论多少字符HashMap一旦构建完毕都是一次查询到,
// 时间复杂度大大优化
int findTimes(char ch){
    return HashMap[ch-'a'];
}

同时HashMap出了上述的优点,在字符串的题目中还天然带上了顺序,我简单举例说明:

题目:

      对于输入的字符进行重新的排序,要求按照字母表顺序从小到大,比如输出:

sdaasdasdhiad

输出:

aaaaddddhisss

如果是正常写得怎么半呢?是不是我们得利用排序算法,时间复杂度是O(n^2),那么利用HashMap可以进行如下的编写:

int HashMap[26]


void sort(char *str){

    for(int I = 0;i < strlen(str);I++){
        int number = HashMap[str[I]-'a'];
        HashMap[str[I]-'a'] = number==1?number+1:0;
    }
    // 因为HashMap的下标是利用a-z,所以天然有序,然后数组对应房间的内容就是该字母重复的次数
    for(int I = 0;i < 26;i++){
        int j = 0;
        for(j = 0;j < HashMap[I];j++)
            printf("%d",('a'+I));
    }
}

你可能会说上面的题目太简单了,完全没体现出这个HashMap的好处,那么现在我给你一个问题:

题目1、输入一串字符串,输出这串字符串中出现次数最多的字符(如果有两个或者多个字符数量一样且是最多的,那么按照字母顺序表输出最小的那个)

例如:dadadagdahj

输出:a

例如:hhhjjjkkkiiibbbdddeee

输出:d

 

题目2、字符串的压缩:

字符串压缩。利用字符重复出现的次数,编写一种方法,实现基本的字符串压缩功能。比如,字符串aabcccccaaa会变为a2b1c5a3。若“压缩”后的字符串没有变短,则返回原先的字符串。你可以假设字符串中只包含大小写英文字母(a至z)。

输入:"aabcccccaaa"

输出:"a2b1c5a3"

输入:"abbccd"

输出:"abbccd"    解释:因为压缩后的字符串比原来的还长所以返回原来的字符串

 

 

题目3、输入两个字符串,str1和str2,现在我们想要获取两个字符串的相似度,现在规定如下:

两个字符串的相似度计算 =  相同字符的个数除以两个字符串中长度较长的字符串的长度

输入:abcdefg   和   gfedcbb

输出: 0.85(小数点后面就保留2位就行了)

输入:bsgdadas 

 

 

题目4、去除字符串中重复的字母

输入:abdsEDAsd

输出:abdsEDA

输入:abndddaAjhAj

输出:abndAjh

(出现重复了总是删除后面的)

 

题目5、同类词查找

题目6、单词拼凑

我们现在将上面的题目进行修改,因为拼凑固定的字符串太简单了,我们现在要求单词从键盘输入,那么代表每次的输入单词可能不一样

请求出在text中可以拼凑出的单词数量!

 

题目7、重复文件

 

题目8、无重读的最长子串

 

HashMap类题目的总结:

1、和字符串个数相关的题目

2、和重复性相关的题目

3、需要加快检索速度的题目(因为HashMap的检索速度非常的快)

具有以上三个特点的题目,一般都是能使用HashMap的!

# 带大写和小写字母的HashMap的构建
int a[200]

for(int I = 0;i < strlen(str);I++){
    // 这块的逻辑你想怎么实现都行,但是请都减去大写的A
    // 因为大写A的ASCII码较小,相减不会出现负数的情况
    // 中间没用的房间就空着,浪费的这点空间对于时间的提升
    // 是相当划算的。
    a[str[I]-'A'] = a[str[I]-'A'] == 1?1:0;
}

 

2.2、HashMap的扩展

        上述的HashMap中我们可以看见实际上都是将一个字符映射为数组的一个下标,那么我们可能会有这种疑问:能不能将一个字符串映射成下标呢?答案是肯定的!,我简单的举个例子,我们可以将字符串的所有字符求和作为字符串的下标,你看这样不就行了吗?但是你肯定会有以下一个疑问:

1、那两串字符串如果求和结果一样怎么办?

2、我的数组设置成多长合适呢?毕竟单个字母一共就26个,算上大写也就52个。

例如字符串:abc和字符串bbb求和肯定是一样的,那么其实我们可以采取如下的措施:

        那么对于这个办法你可能会问有什么用呢?其实这就是大名鼎鼎的RK字符串匹配算法,假设我现在要在一篇文章中查找指定的词语,如果采用之前的暴力滑动,那么时间复杂度事比较高的,像我们的百度搜索呀还有其他的软件的关键字搜索呀,肯定不可能在整个文章中采用暴力匹配,现在来说一般公司大一点的公司数据了都是TB为单位了。下面看我利用HashMap对字符串的匹配算法进行优化:

例题:

输出长文本字符串text和需要匹配的字符串str

输出第一次匹配到的下标

解题思路:

1、循环便利text,每次取str长度的字符串计算HashMap中的下标,如果计算的HashMap下标和str字符串计算出来的下标不一样肯定不相等,但是如果一样,说明可能相等,需要进行细致的匹配

2、细致的匹配就是暴力滑动匹配,这时候字符串的长度极大的减小了,因此时间复杂度事很低的。(如果不进行步骤1的处理那么处理速度就会很慢)

// 假设这么长够用!!!
int HashMap[100000]


// 计算一个字符串的HashMap的下标
// @Param start:开始计算的下标
// @Param end:结束计算的下标
int getHashMapIndex(char *start,char *end){
    int sum = 0;
    for(int *i = start;i <= *end;i++) sum+=*i;
    return sum;
}


// 判断两个字符串是否相等,相等返回true
// 不相等返回false
// @Param text_start:文本开始匹配的地方
// @Param text_end:文本结束的地方
bool equals(char *text_start,char *text_end,char *str2){
    int length1 = text_end-text_start+1;
    int length2 = strlen(str2);
    if(length1!=length2) return false;
    for(int i = 0;i < length1;i++)
        if(text_start[i] != str2[i]) return false;
    return true;
}


// @return -1代表没有查找到
int find_string(char * text, char *str){
    int str_index = getHashMapIndex(str);
    

    // 遍历每一个元素
    int length = strlen(str);
    for(int i = 0;i < strlen(text);i++){
        // HashMap值的匹配
        if(getHashMapIndex(text+i,text+i+length) == str_index)
            if(!equals(text+i,text+i+length)) return -1;
            else
                return i;                // match
    }
    
}

如果你想挑战自己:
题目:

输入一堆单词(单词间的输入用逗号隔开,并且只含有小写字母),请找出单词中出现频率第k多的单词

输入:

第一行代表输入的单词

第二行事输入的k

good,student,school,ning,good,good,good,person,school,student,student

3

输出:

school

2.3、字符串的递归

思前想后还是决定把这个写上,首先说明这个递归并不是字符串题目中一个常见的思路,实际上更多的可能只是借助了字符串的外套,其实考察的是全排列的问题,所以下次遇见了纯字符串的匹配问题或者打印问题一般不用往递归上面想,只有出现字符串的组合,分类,子集等等又可能会使用上递归。

题目1:

给定一组单词words,编写一个程序,找出其中的最长单词,且该单词由这组单词中的其他单词组合而成。若有多个长度相同的结果,返回其中字典序最小的一项,若没有符合要求的单词则返回空字符串。

输入: ["cat","banana","dog","nana","walk","walker","dogwalker"]
输出: "dogwalker"
解释: "dogwalker"可由"dog"和"walker"组成。

题目2:

一条包含字母 A-Z 的消息通过以下方式进行了编码:

'A' -> 1
'B' -> 2
...
'Z' -> 26

给定一个只包含数字的非空字符串,请计算解码方法的总数。

题目3:

特殊的二进制序列是具有以下两个性质的二进制序列:

0 的数量与 1 的数量相等。
二进制序列的每一个前缀码中 1 的数量要大于等于 0 的数量。
给定一个特殊的二进制序列 S,以字符串形式表示。定义一个操作 为首先选择 S 的两个连续且非空的特殊的子串,然后将它们交换。(两个子串为连续的当且仅当第一个子串的最后一个字符恰好为第二个子串的第一个字符的前一个字符。)

在任意次数的操作之后,交换后的字符串按照字典序排列的最大的结果是什么?

 

输入: S = "11011000"
输出: "11100100"
解释:
将子串 "10" (在S[1]出现) 和 "1100" (在S[3]出现)进行交换。
这是在进行若干次操作后按字典序排列最大的结果。

 

总结:

上面的题目实际上就是排列组合的问题,和我们之前讲解的给定几个数字求解全排列是一个意思,递归加回溯就搞定了。

 

 

2.4、字符串的匹配(重点、重灾区)

2.4.1、字符串匹配----暴力美学

你现在做题就多用这个就好了,你们的题目还考不到那么难,这个方法是最好想的,很简单,在这类型的题目中通常是匹配两个字符串,那么我以下面的题目为例进行讲解:

例题1:

解题思路:这道题目的难点在于我们需要弄清楚什么叫做模式匹配?实际上这个中括号的意思就是我们可以从中括号中选取1个字符加入我们的模式字符串(必须且仅能加入一个字符!!)

那我们再写程序的时候只需要顺序的匹配两个字符串的部分就行了,在匹配到待用[模式匹配的部分]采用分支+递归的方法就好了!因为这道题目没有说有多少个中括号,但是我明显可以就解决局部的中括号然后后面是一样的解决手法:

请你实现上述思路的代码部分:

 

题目1:

总结:类似这种带有分支的情况,那么恶需要在包里匹配的基础上进行回溯,如果没有分支的情况那么直接暴力就行了,暴力的复杂度通常来说是O(m*n)m和n代表的是两个字符串的长度!!!

 

2.4.2、正则匹配

这个相当于是调用语言的字符串匹配的类库,c语言不知道是否支持,没使用过,不过你在后续的其他语言学习中肯定会学到这个,比如c++语言,Java语言等。

 

2.4.3、初步加速--RK算法

该算法就是上面的提到的HashMap扩展章节,我们平时的论文查重的算法可能大部分都是基于这个算法的。

 

2.4.4、KMP算法(*****非常的重要)

留坑,等你学了数据结构再给你补上,目前不会这么难

 

2.4.5、BM算法(速度比KMP快)

记事本的查找通常就是这个算法

留坑,等你考试完了可以给你补上,这个考试肯定不会考

 

2.4.6、字典树(前缀树)

url的域名解析过滤,多模式的字符串快速匹配都是用的这个

等你学了树这个数据结构我给你普及

 

2.4.7、Manlacher算法

这个是和回文判断相关的算法,速度很快,等你开始完我给你介绍。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页