【数据结构与算法(十四)】

把基础打牢
画图还是很重要的,现在能一下子想起来的数据结构都是在脑海中有图的

题目

正则表达式

请实现一个函数用来匹配包含'.'‘*’的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符串匹配整个模式。例如,字符串“aaa”与模式"a.a"ab*ac*a匹配,但与aa.aab*a均不匹配。

思路

1、其实一开始对上面这个题目不是很理解,就上面那个例子就已经很迷惑了。说说自己的理解了的题目——首先那个'.'就不说了,就是直接匹配任意一个字符。说说'*'它说可以匹配在它前面出现的那个字符的任意次,其实说得很清楚了,比如'ab*ac*a',在第一个*前面的字符串是b,所以在需要匹配的字符串中,在首字符a出现后可以出现任意个数的b,在b结束之后需要出现的还是一个a,然后又有c*,所以在这之后,可以又任意个数的c,最后要以a结尾,所以这个字符串中至少要有3个a,之外的b和c是穿插在三个a中间,以任意个数出现。再回去看看举的例子大概都能明白了。
2、首先要想想.*可能出现的地方和出现的个数——连在一起出现?并且还在开头?只出现一个,在开头或结尾还是在中间?出现很多个,在开头结尾还是中间?一个都没有出现
3、要一层一层砍掉可能出现的情况,要怎么排序?先判断哪种?
4、首先要分析第一个匹配的字符,如果模式中的字符是'.'那么它可以匹配任意字符;如果不是'.',那就要判断模式中的字符与字符串的第一个字符是否相等,如果匹配则可继续匹配后面的字符,否则就可以返回false了
5、如果第二个字符不是'*',说明第一个字符串不是可以出现任意次数的字符串,因为*总是要和它前面的字符绑在一起才可以用的。那这样的话就直接匹配第一个字符串后面的字符就可以了,就像一个循环的过程吧。
6、但如果第二个字符串是'*',那就有点麻烦了,因为有好多种匹配方法。第一种是判断到第二个字符串时,直到它是*就直接忽略,因为我们能继续判断第二个字符串,说明第一个字符串我们已经判断过了,是匹配的,*又是说可以出现很多次,所以第二个字符串直接就可以跳过了,直接看代码吧
7、输入两个字符串,所以要分别考虑两个是空串的情况,然后输入匹配的、不匹配的,. *出现的地方、次数

bool match(char* str, char* pattern)
{   //模式字符串和输入字符串不存在或者为空串
    if (str == nullptr || pattern == nullptr)
        return false;
    //因为核心的部分是一个递归的过程,所以还是得写两个函数
    return matchCore(str, pattern);
}

bool matchCore(char* str, char* pattern)
{
    //递归退出的条件,模式和字符串都已经到尾部了
    if (*str == '\0' && *pattern == '\0')
        return true;
    //为什么?那如果模式还没完,但是字符串已经完了呢?
    if (*str != '\0'&& *pattern == '\0')
        return false;
    //第二个字符是*的情况
    if (*(pattern + 1) == '*') {
        if (*pattern == *str || (*pattern == '.'&&*str != '\0'))//&&比||优先级高
            //这里是刚好前面一个字符匹配,紧紧跟着的那个字符也匹配【很nice的情况,指匹配一个字符,类似于a*bcd<==>abcd】
            return matchCore(str + 1, pattern + 2)
            //那种情况不成立,那就继续看这里,因为*可以匹配任意长度的字符串,所以str就可以继续往后走啦,类似于.*bcd<==>asfsdbcd
            || matchCore(str + 1, pattern)
            //第二种情况再不成立才会到这里
            || matchCore(str, pattern + 2);
        else
            return matchCore(str, pattern + 2);//竟然第一个都不匹配,那就当他*是0匹配,直接进入下一个匹配
    }
    if (*pattern == *str || (*pattern == '.'&&*str != '\0'))
        return matchCore(str + 1, pattern + 1);

    return false;
}

表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串“+100”、“5e2”、“-123”、“3.1416”及“-1E-16”都表示数值,但“12e”、”1a3.14”、”1.2.3”、”+-5”及”12e+5.4”都不是

思路

1、表示数值的字符串遵循模式A[.[B]][e|EC]或者.B[e|EC],其中A为数值的整数部分,B紧跟着小数点为数值的小数部分,C紧跟着‘e’或者‘E’为数值的指数部分。在小数里可能没有数值的整数部分。例如,.123==0.123,因此A部分不是必须的。如果一个数没有整数部分,那么它的小数部分不能为空。A和C都可以是以‘+’或者’-‘开头的0~9的数位串;B也是0~9的数位串,但是不能有+或者-符号
2、举个例子“123.45e+6”——123==》A整数;45==》B小数;+6==》C指数
3、首先尽可能多地扫描连续的数位串作为A,之后在遇到小数点之后开始扫描小数部分B,如果有遇到e或者E,则开始扫描指数部分C

//表示数值的字符串
//str是一个字符串,指向第一个字符
bool isNumberic(const char* str)
{
    if (str == nullptr)
        return false;//字符串是空的就很开心了,完全不用判断
    //为什么是引用?这样当函数调用结束时,str指向的字符串也跟着往后移动了
    bool numeric=scanInteger(&str);
    //如果出现'.',则开始小数部分的匹配
    if (*str == '.') {
        ++str;
        //下面一行代码用||的原因
        //①小数可以没有整数部分
        //②小数点后面可以没有数字
        //③小数点前面和后面都可以有数字。也就是说唯一不可以出现的就是小数点前面和后面都没有数字
        numeric = scanIntegerUnsigned(&str) || numeric;
    }
    //如果出现字符'e''E',则接下来匹配指数部分
    if (*str == 'e' || *str == 'E') {
        ++str;
        //下面一行代码用&&的原因
        //①当e或E前面没有数字时,整个字符串不能为数字,如.e1,e1
        //②当e或E后面没有整数时,也不符合,如12E,12e4.8
        //也就是说,出现了E,那前面就必须有数值,后面必须是整数
        numeric = scanInteger(&str) && numeric;
    }
    return numeric && *str == '\0';
}
//扫描连续的数位串,包括+ -
//str是指向一个字符串的指针
bool scanInteger(const char** str)
{
    if (**str == '+' || **str == '-')
        ++(*str);//指向字符串下一个字符
    return scanIntegerUnsigned(str);
}
//用来匹配B部分
bool scanIntegerUnsigned(const char** str)
{
    const char* before = *str;
    //在数值0~9之间
    while (**str != '\0'&&**str >= '0'&&**str <= '9')
        (*str)++;
    //当存在若干数值0~9时返回true,此时的*str指向的是一个不为数值的字符
    return *str > before;
}

要求先对数值字符的特点进行分析,之后分步骤进行

调整数组顺序使奇数位于偶数前面

输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分

思路

1、最简单的思路,不考虑时间复杂度,从头扫描这个数组,每碰到一个偶数,拿出这个数字,并把位于这个数字后面的所有数字往前挪动一位,挪完之后在数组的末尾就有一个空位,这时把这个偶数放在这个空位。没碰到一个偶数就要挪动O(n),所以总的时间复杂度是O( n2 n 2 )

完成基本功能的算法

2、这道题有点熟悉啊,可以用两个方向同时进行,就是从前面开始扫描找到偶数,后面开始扫描找到奇数,如果扫描到的偶数在奇数前面,就交换。所以又是需要用两个指针解决问题的事
3、第一个指针初始化时指向数组的第一个数字,它只向后移动;第二个指针初始化时指向数组的最后一个数字,它只向前移动。在两个指针相遇之前,第一个指针总是位于第二个指针的前面。如果第一个指针指向的数字是偶数,并且第二个指针指向的数字是奇数,则交换这俩数字
直接看代码

void ReorderOddEven(int* pData, unsigned int length)
{
    //不存在该数组,或者为空数组
    if (pData == nullptr || length == 0)
        return;
    int *pBegin = pData;
    int *pEnd = pData + length - 1;
    while (pBegin < pEnd) {
        //向后移动前面的指针,直到它指向偶数
        while (pBegin < pEnd && (*pBegin & 0x1) != 0)//奇数
            pBegin++;
        //向前移动后面的指针,直到它指向奇数
        while (pBegin < pEnd && (*pEnd & 0x1) == 0)//偶数
            pEnd--;

        if (pBegin < pEnd) {
            int temp=*pBegin;
            *pBegin = *pEnd;
            *pEnd = temp;
        }
    }
}

进阶的解法——解决同类型问题的办法

4、扩展的问题:比如说数组里面有正负数,那要求把负数放在正数前面;比如说要求把能被3整除的数放在不能被3整除的数前面。
5、解决这两个新问题其实我们首先想到的是修改前面代码里面的while循环退出的条件,那我们可以把这个逻辑框架抽象出来,让代码帮我们完成选择用什么判断条件——把判断条件变成一个函数指针,也就是说用一个单独的函数来判断数字是不是符合标准。这样我们就能把整个函数解耦成两部分:一是判断数字应该在数组的前半部分还是后半部分;二是拆分数组的操作。

//问题的扩展,代码的可重用性
void Reorder(int* pData, unsigned int length, bool(*fun)(int))
{
    if (pData == nullptr || length == 0)
        return;

    int *pBegin = pData;
    int *pEnd = pData + length - 1;
    while (pBegin < pEnd) {
        while (pBegin < pEnd && !fun(*pBegin))
            pBegin++;
        while (pBegin < pEnd&&fun(*pEnd))
            pEnd;
        if (pBegin < pEnd) {
            int temp = *pBegin;
            *pBegin = *pEnd;
            *pEnd = temp;
        }
    }
}

//使用上面的代码进行就划分。其实只要改变isEven这个参数就可以进行其他分类了,最终还是回归到isEvent一行代码
void OddEven(int *pData, unsigned int length)
{
    Reorder(pData, length, isEven);
}

bool isEven(int number) {
    return (number & 1) == 0;
}

代码的可重用性——解耦
上面的bool(*func)(int),是指一个名为func的形参,这个形参是一个指向函数的指针,这个函数的参数是int型,返回值是bool

点赞