程序员编程艺术第三十~三十一章:字符串转换成整数,通配符字符串匹配

2023-05-16

第三十~三十一章:字符串转换成整数,带通配符的字符串匹配


前言

    之前本一直想写写神经网络算法和EM算法,但写这两个算法实在需要大段大段的时间,而平时上班,周末则跑去北大教室自习看书(顺便以时间为序,说下过去半年看过的自觉还不错的数学史方面的书:《数理统计学简史》《微积分概念发展史》《微积分的历程:从牛顿到勒贝格》《数学恩仇录》《数学与知识的探求》《古今数学思想》《素数之恋》),故一直未曾有时间写。

     然最近在负责一款在线编程挑战平台:http://hero.pongo.cn/(简称hero,通俗理解是中国的topcoder,当然,一直在不断完善中,与一般OJ不同点在于,OJ侧重为参与ACM竞赛者提供刷题练习的场所,而hero则着重为企业招聘面试服务),在上面出了几道编程面试题,有些题目看似简单,但一coding,很多问题便立马都在hero上给暴露出来了,故就从hero上的编程挑战题切入,继续更新本程序员编程艺术系列吧。

    况且,前几天与一朋友聊天,他说他认识的今年360招进来的三四十人应届生包括他自己找工作时基本都看过我的博客,则更增加了更新此编程艺术系列的动力。

    OK,本文讲两个问题:

  • 第三十章、字符串转换成整数,从确定思路,到写出有瑕疵的代码,继而到microsoft & linux的atoi实现,再到第一份比较完整的代码,最后以Net/OS中的实现结尾,看似很简单的一个问题,其实非常不简单
  • 第三十一章、字符串匹配问题
    还是这句老话,有问题恳请随时批评指正,感谢。


第三十章、字符串转换成整数

    先看题目:

    输入一个表示整数的字符串,把该字符串转换成整数并输出,例如输入字符串"345",则输出整数345。
给定函数原型int StrToInt(const char *str) ,完成函数StrToInt,实现字符串转换成整数的功能,不得用库函数atoi(即便准许使用,其对于溢出情况的处理也达不到题目的要求,详情请参看下文第7节末)。

    我们来一步一步分析(9小节,重点在下文第8小节及后续内容),直至写出第一份准确的代码:

1、本题考查的实际上就是字符串转换成整数的问题,或者说是要你自行实现atoi函数。那如何实现把表示整数的字符串正确地转换成整数呢?以"345"作为例子:

  1. 当我们扫描到字符串的第一个字符'3'时,由于我们知道这是第一位,所以得到数字3。
  2. 当扫描到第二个数字'4'时,而之前我们知道前面有一个3,所以便在后面加上一个数字4,那前面的3相当于30,因此得到数字:3*10+4=34。
  3. 继续扫描到字符'5','5'的前面已经有了34,由于前面的34相当于340,加上后面扫描到的5,最终得到的数是:34*10+5=345。

    因此,此题的思路便是:每扫描到一个字符,我们便把在之前得到的数字乘以10,然后再加上当前字符表示的数字

    2、思路有了,有一些细节需要注意,如zhedahht所说:

  1. 由于整数可能不仅仅之含有数字,还有可能以'+'或者'-'开头,表示整数的正负。因此我们需要把这个字符串的第一个字符做特殊处理。如果第一个字符是'+'号,则不需要做任何操作;如果第一个字符是'-'号,则表明这个整数是个负数,在最后的时候我们要把得到的数值变成负数。
  2. 接着我们试着处理非法输入。由于输入的是指针,在使用指针之前,我们要做的第一件是判断这个指针是不是为空。如果试着去访问空指针,将不可避免地导致程序崩溃。
  3. 另外,输入的字符串中可能含有不是数字的字符。每当碰到这些非法的字符,我们就没有必要再继续转换。
  4. 最后一个需要考虑的问题是溢出问题。由于输入的数字是以字符串的形式输入,因此有可能输入一个很大的数字转换之后会超过能够表示的最大的整数而溢出。
    比如,当给的字符串是如左边图片所示的时候,有考虑到么?当然,它们各自对应的正确输出如右边图片所示( 假定你是在32位系统下,且编译环境是VS2008以上):
    3、很快,可能你就会写下如下代码:
//copyright@zhedahht 2007  
enum Status {kValid = 0, kInvalid};
int g_nStatus = kValid;

// Convert a string into an integer
int StrToInt(const char* str)
{
	g_nStatus = kInvalid;
	long long num = 0;

	if(str != NULL)
	{
		const char* digit = str;

		// the first char in the string maybe '+' or '-'
		bool minus = false;
		if(*digit == '+')
			digit ++;
		else if(*digit == '-')
		{
			digit ++;
			minus = true;
		}

		// the remaining chars in the string
		while(*digit != '\0')
		{
			if(*digit >= '0' && *digit <= '9')
			{
				num = num * 10 + (*digit - '0');

				// overflow  
				if(num > std::numeric_limits<int>::max())
				{
					num = 0;
					break;
				}

				digit ++;
			}
			// if the char is not a digit, invalid input
			else
			{
				num = 0;
				break;
			}
		}

		if(*digit == '\0')
		{
			g_nStatus = kValid;
			if(minus)
				num = 0 - num;
		}
	}
	return static_cast<int>(num);
}
    run下上述程序,会发现当输入字符串是下图中红叉叉部分所对应的时候,程序结果出错:  
    

    两个问题:

  1. 当输入的字符串不是数字,而是字符的时候,比如“1a”,上述程序直接返回了0(而正确的结果应该是得到1):
    // if the char is not a digit, invalid input
    				  else
    				  {
    					  num = 0;
    					  break;
    				  }
  2. 处理溢出时,有问题。因为它遇到溢出情况时,直接返回了0:
    // overflow  
    				if(num > std::numeric_limits<int>::max())
    				{
    					num = 0;
    					break;
    				}

    4、把代码做下微调,如下( 注:库函数atoi规定超过int值,按最大值maxint:2147483647来,超过-int按最小值minint:-2147483648来):
//copyright@SP_daiyq 2013/5/29
int StrToInt(const char* str)
{
	int res = 0; // result
	int i = 0; // index of str
	int signal = '+'; // signal '+' or '-'
	int cur; // current digit

	if (!str)
		return 0;

	// skip backspace
	while (isspace(str[i]))
		i++;

	// skip signal
	if (str[i] == '+' || str[i] == '-')
	{
		signal = str[i];
		i++;
	}

	// get result
	while (str[i] >= '0' && str[i] <= '9')
	{
		cur = str[i] - '0';

		// judge overlap or not
		if ( (signal == '+') && (cur > INT_MAX - res*10) )
		{
			res = INT_MAX;
			break;
		}
		else if ( (signal == '-') && (cur -1 > INT_MAX - res*10) )
		{
			res = INT_MIN;
			break;
		}

		res = res * 10 + cur;
		i++;
	}

	return (signal == '-') ? -res : res;
}
    此时会发现,上面第3小节末所述的第1个小问题( 当输入的字符串不是数字,而是字符的时候)解决了:
    
    但, 上文第3小节末所述的第2个小问题:溢出问题却没有解决。即当给定下述测试数据的时候,问题就来了:
需要转换的字符串                           代码运行结果    理应得到的正确结果
    
    什么问题呢?比如说用上述代码转换这个字符串:"    10522545459",它本应得到的正确结果应该是2147483647,但程序实际得到的结果却是:1932610867。故很明显,程序没有解决好上面的第2个小问题:溢出问题。原因是什么呢?咱们来分析下代码,看是如何具体处理溢出情况的:
// judge overlap or not
		if ( (signal == '+') && (cur > INT_MAX - res*10) )
		{
			res = INT_MAX;
			break;
		}
		else if ( (signal == '-') && (cur -1 > INT_MAX - res*10) )
		{
			res = INT_MIN;
			break;
		}
    接着上面的例子来,比如给定字符串"    10522545459",除去空格有11位,而MAX_INT,即2147483647是10位数,当扫描到最后一个字符‘9’的时候,程序会比较  92147483647 - 1052254545*10的大小。
     问题立马就暴露出来了,因为此时让res*10,即让1052254545*10 > MAX_INT,溢出无疑,程序已经出错,再执行下面这行代码已无意义:
cur > INT_MAX - res*10
    也就是说, 对于字符串"10522545459",  当扫描到最后一个字符‘9’时,根据上文第1小节的字符串转换成整数的思路:“每扫描到一个字符,我们便把在之前得到的数字乘以10,然后再加上当前字符表示的数字”,为了得到最终的整数,我们得如此计算:

1052254545*10 + 4,

    然实际上当程序计算到1052254545*10时,

1052254545*10 >

2147483647

    此时已经溢出了,若再执意计算,则程序逻辑将出错,故此后也就不能再判断字串的最后一位4是否大于2147483647%10了(耐不得烦想尽快看到最终正确代码的读者可以直接跳到下文第8节)。

  
    5、上面说给的程序没有“ 很好的解决溢出问题。由于输入的数字是以字符串的形式输入,因此有可能输入一个很大的数字转换之后会超过能够表示的最大的整数而溢出”。那么,到底代码该如何写呢?
    像下面这样?:
//copyright@fuwutu 2013/5/29
int StrToInt(const char* str)
{
    bool negative = false;
    long long result = 0;
    while (*str == ' ' || *str == '\t')
    {
        ++str;
    }
    if (*str == '-')
    {
        negative = true;
        ++str;
    }
    else if (*str == '+')
    {
        ++str;
    }

    while (*str != '\0')
    {
        int n = *str - '0';
        if (n < 0 || n > 9)
        {
            break;
        }

        if (negative)
        {
            result = result * 10 - n;
            if (result < -2147483648LL)
            {
                result = -2147483648LL;
            }
        }
        else
        {
            result = result * 10 + n;
            if (result > 2147483647LL)
            {
                result = 2147483647LL;
            }
        }
        ++str;
    }

  return result;
}
    run下程序,看看运行结果:
    
     上图所示程序貌似通过了,然实际上它还是未能处理数据溢出的问题,因为它只是做了个取巧,即把返回的值esult定义成了long long,如下所示:
long long result = 0;
    故严格说来,我们依然未写出准确的规范代码。
    6 那到底该如何解决这个数据溢出的问题呢?咱们先来看看Microsoft是如何实现atoi的吧:
//atol函数
//Copyright (c) 1989-1997, Microsoft Corporation. All rights reserved.
long __cdecl atol(
	const char *nptr
	)
{
	int c; /* current char */
	long total; /* current total */
	int sign; /* if ''-'', then negative, otherwise positive */

	/* skip whitespace */
	while ( isspace((int)(unsigned char)*nptr) )
		++nptr;

	c = (int)(unsigned char)*nptr++;
	sign = c; /* save sign indication */
	if (c == ''-'' || c == ''+'')
		c = (int)(unsigned char)*nptr++; /* skip sign */

	total = 0;

	while (isdigit(c)) {
		total = 10 * total + (c - ''0''); /* accumulate digit */
		c = (int)(unsigned char)*nptr++; /* get next char */
	}

	if (sign == ''-'')
		return -total;
	else
		return total; /* return result, negated if necessary */
}
    其中,isspace和isdigit函数的实现代码为:
isspace(int x)  
{  
	if(x==' '||x=='/t'||x=='/n'||x=='/f'||x=='/b'||x=='/r')  
		return 1;  
	else   
		return 0;  
}  

isdigit(int x)  
{  
	if(x<='9'&&x>='0')           
		return 1;   
	else   
		return 0;  
} 
    然后 atoi调用上面的atol函数,如下所示:
//atoi调用上述的atol
int __cdecl atoi(
	const char *nptr
	)
{
	//Overflow is not detected. Because of this, we can just use
	return (int)atol(nptr);
}

    但很遗憾的是,上述atoi标准代码依然返回的是long:

long total; /* current total */
if (sign == ''-'')
	return -total;
else
	return total; /* return result, negated if necessary */

    再者,下面这里定义成long的total与10相乘,即total*10很容易溢出:

long total; /* current total */
total = 10 * total + (c - ''0''); /* accumulate digit */
   最后,根据本文评论下的读者meiyuli反应:“测试数据是字符串"-21474836480",api算出来的是-2147483648,用上述代码算出来的结果是0”,如此,上述微软的这个atoi源码是有问题的。
     7 microsoft既然不行,读者想必很自然的想到linux。So,咱们接下来便看看 linux内核中是如何实现此字符串转换为整数的问题的。linux内核中提供了以下几个函数:
  1. simple_strtol,把一个字符串转换为一个有符号长整数;
  2. simple_strtoll,把一个字符串转换为一个有符号长长整数;
  3. simple_strtoul,把一个字符串转换为一个无符号长整数;
  4. simple_strtoull,把一个字符串转换为一个无符号长长整数
    相关源码及分析如下。
    首先,atoi调下面的strtol:
//linux/lib/vsprintf.c
//Copyright (C) 1991, 1992  Linus Torvalds
//simple_strtol - convert a string to a signed long
long simple_strtol(const char *cp, char **endp, unsigned int base)
{
	if (*cp == '-')
		return -simple_strtoul(cp + 1, endp, base);

	return simple_strtoul(cp, endp, base);
}
EXPORT_SYMBOL(simple_strtol);
    然后,上面的strtol调下面的strtoul:
//simple_strtoul - convert a string to an unsigned long
unsigned long simple_strtoul(const char *cp, char **endp, unsigned int base)
{
	return simple_strtoull(cp, endp, base);
}
EXPORT_SYMBOL(simple_strtoul);
    接着,上面的strtoul调下面的strtoull:
//simple_strtoll - convert a string to a signed long long
long long simple_strtoll(const char *cp, char **endp, unsigned int base)
{
	if (*cp == '-')
		return -simple_strtoull(cp + 1, endp, base);

	return simple_strtoull(cp, endp, base);
}
EXPORT_SYMBOL(simple_strtoll);
    最后,strtoull调_parse_integer_fixup_radix和_parse_integer来处理相关逻辑:
//simple_strtoull - convert a string to an unsigned long long
unsigned long long simple_strtoull(const char *cp, char **endp, unsigned int base)
{
	unsigned long long result;
	unsigned int rv;

	cp = _parse_integer_fixup_radix(cp, &base);
	rv = _parse_integer(cp, base, &result);
	/* FIXME */
	cp += (rv & ~KSTRTOX_OVERFLOW);

	if (endp)
		*endp = (char *)cp;

	return result;
}
EXPORT_SYMBOL(simple_strtoull);
    重头戏来了。接下来,我们来看上面strtoull函数中的parse_integer_fixup_radix和_parse_integer两段代码。如鲨鱼所说
  • “真正的处理逻辑主要是在_parse_integer里面,关于溢出的处理,_parse_integer处理的很优美,
  • 而_parse_integer_fixup_radix是用来自动根据字符串判断进制的”。
    先来看 _parse_integer函数:
//lib/kstrtox.c, line 39  
//Convert non-negative integer string representation in explicitly given radix to an integer.  
//Return number of characters consumed maybe or-ed with overflow bit.  
//If overflow occurs, result integer (incorrect) is still returned.  
unsigned int _parse_integer(const char *s, unsigned int base, unsigned long long *p)  
{  
    unsigned long long res;  
    unsigned int rv;  
    int overflow;  
  
    res = 0;  
    rv = 0;  
    overflow = 0;  
    while (*s) {  
        unsigned int val;  
  
        if ('0' <= *s && *s <= '9')  
            val = *s - '0';  
        else if ('a' <= _tolower(*s) && _tolower(*s) <= 'f')  
            val = _tolower(*s) - 'a' + 10;  
        else  
            break;  
  
        if (val >= base)  
            break;  
        /* 
         * Check for overflow only if we are within range of 
         * it in the max base we support (16) 
         */  
        if (unlikely(res & (~0ull << 60))) {  
            if (res > div_u64(ULLONG_MAX - val, base))  
                overflow = 1;  
        }  
        res = res * base + val;  
        rv++;  
        s++;  
    }  
    *p = res;  
    if (overflow)  
        rv |= KSTRTOX_OVERFLOW;  
    return rv;  
}
    解释下两个小细节:
  1. 上头出现了个unlikely,其实unlikely和likely经常出现在linux相关内核源码中
    if(likely(value)){
    	//等价于if(likely(value)) == if(value)
    }
    else{
    }
    likely表示value为真的可能性更大,而unlikely表示value为假的可能性更大,这两个宏被定义成:
    //include/linux/compiler.h
    # ifndef likely
    #  define likely(x)	(__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 1))
    # endif
    # ifndef unlikely
    #  define unlikely(x)	(__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 0))
    # endif
  2. 呈现下div_u64的代码:
    //include/linux/math64.h
    //div_u64
    static inline u64 div_u64(u64 dividend, u32 divisor)
    {
    	u32 remainder;
    	return div_u64_rem(dividend, divisor, &remainder);
    }
    
    //div_u64_rem
    static inline u64 div_u64_rem(u64 dividend, u32 divisor, u32 *remainder)
    {
    	*remainder = dividend % divisor;
    	return dividend / divisor;
    }
    最后看下_parse_integer_fixup_radix函数:
//lib/kstrtox.c, line 23
const char *_parse_integer_fixup_radix(const char *s, unsigned int *base)
{
	if (*base == 0) {
		if (s[0] == '0') {
			if (_tolower(s[1]) == 'x' && isxdigit(s[2]))
				*base = 16;
			else
				*base = 8;
		} else
			*base = 10;
	}
	if (*base == 16 && s[0] == '0' && _tolower(s[1]) == 'x')
		s += 2;
	return s;
}
    读者MJN君在我的建议下,对上述linux内核中的atoi函数进行了测试,咱们来看下测试结果如何。

2147483647 : 2147483647
2147483648 : -2147483648
10522545459 : 1932610867
-2147483648 : -2147483648
-2147483649 : -2147483647
-10522545459 : 1932610867  
    如上,根据程序的输出结果可以看出,对于某些溢出的情况,atoi程序的处理并不符合本题的要求
 也就是说,atoi程序对溢出的处理是一个标准,而本题要求对溢出的处理则是另外一个标准,所以说直接用atoi程序达不到本题的要求,但你不能因为本题的标准而否认atoi程序的正确性
  既然直接借用atoi的源码(原理是parseXXX,int i=Integer.parseInt(String str),把str转换成int的方法),不符合题目要求,则咱们另寻他路。
  路漫漫其修远兮,吾等将上下而求索,但与此同时,我们已渐入佳境。
  

   8根据我们第1小节达成一致的字符串转换成整数的思路:“每扫描到一个字符,我们便把在之前得到的数字乘以10,然后再加上当前字符表示的数字,相信读者已经觉察到,在扫描到最后一个字符的时候,如果之前得到的数比较大,此时若再让其扩大10倍,相对来说是比较容易溢出的。

    但车到山前必有路,既然让一个比较大的int整型数括大10倍,比较容易溢出, 那么在不好判断是否溢出的情况下,可以尝试使用除法。即如MJN所说:
  1. 与其将n扩大10倍,,冒着溢出的风险, 再与MAX_INT进行比较(如果已经溢出, 则比较的结果没有意义),
  2. 不如未雨绸缪先用n与MAX_INT/10进行比较: 若n>MAX_INT/10(当然同时还要考虑n=MAX_INT/10的情况), 说明最终得到的整数一定会溢出, 故此时可以当即进行溢出处理,直接返回最大值MAX_INT,从而也就免去了计算n*10这一步骤。
    也就是说, 计算n*10前,先比较n与MAX_INT/10大小,若n>MAX_INT/10,那么n*10肯定大于MAX_INT,即代表最后得到的整数n肯定溢出,既然溢出, 不能 再计算n*10,直接提前返回MAX_INT就行了。
    一直以来,我们努力的目的归根结底是为了更好的处理溢出,但上述做法最重要的是巧妙的规避了计算n*10这一乘法步骤,转换成计算除法MAX_INT/10代替,不能不说此法颇妙。

   他的代码如下,如有问题请指出:

//copyright@njnu_mjn 2013
int StrToDecInt(const char* str)    
{    
    static const int MAX = (int)((unsigned)~0 >> 1);    
    static const int MIN = -(int)((unsigned)~0 >> 1) - 1;    
    unsigned int n = 0;    
    int sign = 1;    
    int c;    
    
    while (isspace(*str))    
        ++str;    
    if (*str == '+' || *str == '-')    
    {    
        if (*str == '-')    
            sign = -1;    
        ++str;    
    }    
    while (isdigit(*str))    
    {    
        c = *str - '0';    
        if (sign > 0 && (n > MAX/10 || (n == MAX/10 && c > MAX%10)))    
        {    
            n = MAX;    
            break;    
        }    
        else if (sign < 0 && (n > (unsigned)MIN/10     
                              || (n == (unsigned)MIN/10 && c > (unsigned)MIN%10)))    
        {    
            n = MIN;    
            break;    
        }    
        n = n * 10 + c;    
        ++str;    
    }    
    return sign > 0 ? n : -n;    
}  

    上述代码从测试结果来看,暂未发现什么问题

输入  输出
10522545459 :     2147483647
-10522545459 :   -2147483648

    咱们再来总结下上述代码是如何处理溢出情况的。对于正数来说,它溢出的可能性有两种:

  1. 一种是诸如2147483650,即n > MAX/10 的;
  2. 一种是诸如2147483649,即n == MAX/10 && c > MAX%10。

   故咱们上面处理溢出情况的代码便是:

c = *str - '0';  
        if (sign > 0 && (n > MAX/10 || (n == MAX/10 && c > MAX%10)))  
        {  
            n = MAX;  
            break;  
        }  
        else if (sign < 0 && (n > (unsigned)MIN/10   
                              || (n == (unsigned)MIN/10 && c > (unsigned)MIN%10)))  
        {  
            n = MIN;  
            break;  
        }  

    不过,即便如此,有些细节是改进的,如他自己所说:

  1. n的声明及定义应该为
    int n = 0;  
  2. 将MAX/10,MAX%10,(unsigned)MIN/10及(unsigned)MIN%10保存到变量中, 防止重复计算
    这样,优化后的代码为:
//copyright@njnu_mjn 2013
int StrToDecInt(const char* str)  
{  
    static const int MAX = (int)((unsigned)~0 >> 1);  
    static const int MIN = -(int)((unsigned)~0 >> 1) - 1;  
    static const int MAX_DIV = (int)((unsigned)~0 >> 1) / 10;  
    static const int MIN_DIV = (int)((((unsigned)~0 >> 1) + 1) / 10);  
    static const int MAX_R = (int)((unsigned)~0 >> 1) % 10;  
    static const int MIN_R = (int)((((unsigned)~0 >> 1) + 1) % 10);  
    int n = 0;  
    int sign = 1;  
    int c;  
  
    while (isspace(*str))  
        ++str;  
    if (*str == '+' || *str == '-')  
    {  
        if (*str == '-')  
            sign = -1;  
        ++str;  
    }  
    while (isdigit(*str))  
    {  
        c = *str - '0';  
        if (sign > 0 && (n > MAX_DIV || (n == MAX_DIV && c >= MAX_R)))  
        {  
            n = MAX;  
            break;  
        }  
        else if (sign < 0 && (n > MIN_DIV   
                                                    || (n == MIN_DIV && c >= MIN_R)))  
        {  
            n = MIN;  
            break;  
        }  
        n = n * 10 + c;  
        ++str;  
    }  
    return sign > 0 ? n : -n;  
}  
    部分数据的测试结果如下图所示:

输入            输出
10522545459  : 2147483647
-10522545459 : -2147483648
2147483648   : 2147483647
-2147483648  : -2147483648  
   是否已是完美?如MJN君本人所说“ 我的实现与linux内核的atoi函数的实现, 都有一个共同的问题: 即使出错, 函数也返回了一个值, 导致调用者误认为自己传入的参数是正确的, 但是可能会导致程序的其他部分产生莫名的错误且很难调试”。

  9最后看下Nut/OS中atoi的实现,同时,本小节内容主要来自参考文献条目9,即MJN的博客:

00077 #include <compiler.h>
00078 #include <stdlib.h>
00079 
00084 
00092 int atoi(CONST char *str)
00093 {
00094     return ((int) strtol(str, (char **) NULL, 10));
00095 }

  上述代码中strtol实现的思想跟上文第7节所述的MJN君的思路类似,也是除法代替乘法。加上测试函数后的具体代码如下:

#include <errno.h>
#include <stdio.h>
#include <ctype.h>
#include <limits.h>

#define CONST      const

long mstrtol(CONST char *nptr, char **endptr, int base)
{
    register CONST char *s;
    register long acc, cutoff;
    register int c;
    register int neg, any, cutlim;

    /*
     * Skip white space and pick up leading +/- sign if any.
     * If base is 0, allow 0x for hex and 0 for octal, else
     * assume decimal; if base is already 16, allow 0x.
     */
    s = nptr;
    do {
        c = (unsigned char) *s++;
    } while (isspace(c));
    if (c == '-') {
        neg = 1;
        c = *s++;
    } else {
        neg = 0;
        if (c == '+')
            c = *s++;
    }
    if ((base == 0 || base == 16) && c == '0' && (*s == 'x' || *s == 'X')) {
        c = s[1];
        s += 2;
        base = 16;
    }
    if (base == 0)
        base = c == '0' ? 8 : 10;

    /*
     * Compute the cutoff value between legal numbers and illegal
     * numbers.  That is the largest legal value, divided by the
     * base.  An input number that is greater than this value, if
     * followed by a legal input character, is too big.  One that
     * is equal to this value may be valid or not; the limit
     * between valid and invalid numbers is then based on the last
     * digit.  For instance, if the range for longs is
     * [-2147483648..2147483647] and the input base is 10,
     * cutoff will be set to 214748364 and cutlim to either
     * 7 (neg==0) or 8 (neg==1), meaning that if we have accumulated
     * a value > 214748364, or equal but the next digit is > 7 (or 8),
     * the number is too big, and we will return a range error.
     *
     * Set any if any `digits' consumed; make it negative to indicate
     * overflow.
     */
    cutoff = neg ? LONG_MIN : LONG_MAX;
    cutlim = cutoff % base;
    cutoff /= base;
    if (neg) {
        if (cutlim > 0) {
            cutlim -= base;
            cutoff += 1;
        }
        cutlim = -cutlim;
    }
    for (acc = 0, any = 0;; c = (unsigned char) *s++) {
        if (isdigit(c))
            c -= '0';
        else if (isalpha(c))
            c -= isupper(c) ? 'A' - 10 : 'a' - 10;
        else
            break;
        if (c >= base)
            break;
        if (any < 0)
            continue;
        if (neg) {
            if ((acc < cutoff || acc == cutoff) && c > cutlim) {
                any = -1;
                acc = LONG_MIN;
                errno = ERANGE;
            } else {
                any = 1;
                acc *= base;
                acc -= c;
            }
        } else {
            if ((acc > cutoff || acc == cutoff) && c > cutlim) {
                any = -1;
                acc = LONG_MAX;
                errno = ERANGE;
            } else {
                any = 1;
                acc *= base;
                acc += c;
            }
        }
    }
    if (endptr != 0)
        *endptr = (char *) (any ? s - 1 : nptr);
    return (acc);
}

int matoi2(CONST char *str)
{
    return ((int) mstrtol(str, (char **) NULL, 10));
}

int mgetline(char* buf, size_t n) {
  size_t idx = 0;
  int c;
  
  while (--n > 0 && (c = getchar()) != EOF && c != '\n') {
    buf[idx++] = c;
  }
  buf[idx] = '\0';
  return idx;
}

#define MAX_LINE 200

int main() {
    char buf[MAX_LINE];
    while (mgetline(buf, MAX_LINE) >= 0) {
        if (strcmp(buf, "quit") == 0) break;
        printf("matoi2=%d\n", matoi2(buf));
    }
    return 0;
}

    同样,MJN对上述实现测试了下,结果如下:


10522545459
matoi2=2147483647
-10522545459
matoi2=-2147483648
  

    程序貌似对溢出的处理是正确的, 真的吗? 再把测试数据换成"10522545454"(与"10522545459"的区别在于最后一个字符


10522545454
matoi2=1932610862
-10522545454
matoi2=-1932610862
  

    症结就在于下面这段代码:

if (neg) {
            if ((acc < cutoff || acc == cutoff) && c > cutlim) {
                any = -1;
                acc = LONG_MIN;
                errno = ERANGE;
            } else {
                any = 1;
                acc *= base;
                acc -= c;
            }
        } else {
            if ((acc > cutoff || acc == cutoff) && c > cutlim) {
                any = -1;
                acc = LONG_MAX;
                errno = ERANGE;

    要想得到正确的输出结果,需要改动两个地方:

    ①其中这行:

if ((acc > cutoff || acc == cutoff) && c > cutlim)

    应该改为:

if ( acc > cutoff ||  (acc == cutoff) && c > cutlim)  )

    ②与此同时,这行:

if ((acc < cutoff || acc == cutoff) && c > cutlim) {

    改为:

if (acc < cutoff || (acc == cutoff && c > cutlim)) {

    为何要这样修改呢?细心的读者相信还是会记得上文第8节中关于正数的两种溢出情况的可能性:对于正数来说,它溢出的可能性有两种:

  1. 一种是诸如2147483650,即n > MAX/10 的;
  2. 一种是诸如2147483649,即n == MAX/10 && c > MAX%10。

    也就是说无论是"10522545459",还是"10522545454",都是属于第1种情况,即“诸如2147483650,即n > MAX/10的”,此时直接返回MAX_INT即可,所以不需要也不能再去判断n == MAX/10的情况。

    这个处理思路类似于上文第8节处理溢出情况的代码:

if (sign > 0 && (n > MAX/10 || (n == MAX/10 && c > MAX%10)))    
        {    
            n = MAX;    
            break;    
        }    
        else if (sign < 0 && (n > (unsigned)MIN/10     
                              || (n == (unsigned)MIN/10 && c > (unsigned)MIN%10)))    
        {    
            n = MIN;    
            break;    
        }    

    So,修改过后的代码测试正常:


10522545459
matoi2=2147483647
-10522545459\
matoi2=-2147483648
10522545454
matoi2=2147483647
-10522545454
matoi2=-2147483648
quit  
    OK,字符串转换成整数这一问题已基本解决。但如果面试官继续问你,如何把整数转换成字符串呢?欢迎于本文评论下或hero上show出你的思路或代码。

第三十一章、带通配符的字符串匹配问题

字符串匹配问题,给定一串字符串,按照指定规则对其进行匹配,并将匹配的结果保存至output数组中,多个匹配项用空格间隔,最后一个不需要空格。

要求:

  1. 匹配规则中包含通配符?和*,其中?表示匹配任意一个字符,*表示匹配任意多个(>=0)字符。
  2. 匹配规则要求匹配最大的字符子串,例如a*d,匹配abbdd而非abbd,即最大匹配子串。
  3. 匹配后的输入串不再进行匹配,从当前匹配后的字符串重新匹配其他字符串。

请实现函数:char* my_find(char  input[],   char rule[])

举例说明

input:abcadefg
rule:a?c
output:abc

input :newsadfanewfdadsf
rule: new
output: new new

input :breakfastfood
rule: f*d
output:fastfood

注意事项:

  1. 自行实现函数my_find,勿在my_find函数里夹杂输出,且不准用C、C++库,和Java的String对象;
  2. 请注意代码的时间,空间复杂度,及可读性,简洁性;
  3. input=aaa,rule=aa时,返回一个结果aa,即可。

    1、本题与上述第三十章的题不同,上题字符串转换成整数更多考察对思维的全面性和对细节的处理,本题则更多的是编程技巧。闲不多说,直接上代码:

//copyright@cao_peng 2013/4/23
int str_len(char *a) {  //字符串长度
	if (a == 0) {
		return 0;
	}
	char *t = a;
	for (;*t;++t)
		;
	return (int) (t - a);
}

void str_copy(char *a,const char *b,int len) {  //拷贝字符串 a = b
	for (;len > 0; --len, ++b,++a) {
		*a = *b;
	}
	*a = 0;
}

char *str_join(char *a,const char *b,int lenb) { //连接字符串 第一个字符串被回收
	char *t;
	if (a == 0) {
		t = (char *) malloc(sizeof(char) * (lenb + 1)); 
		str_copy(t, b, lenb);
		return t;
	}
	else {
		int lena = str_len(a);
		t = (char *) malloc(sizeof(char) * (lena + lenb + 2));
		str_copy(t, a, lena);
		*(t + lena) = ' ';
		str_copy(t + lena + 1, b, lenb);
		free(a);
		return t;
	}
}

int canMatch(char *input, char *rule) { // 返回最长匹配长度 -1表示不匹配 
	if (*rule == 0) { //已经到rule尾端
		return 0;
	}
	int r = -1 ,may;
	if (*rule == '*') {
		r = canMatch(input, rule + 1);  // *匹配0个字符
		if (*input) {
			may = canMatch(input + 1, rule);  // *匹配非0个字符
			if ((may >= 0) && (++may > r)) {
				r = may;
			}
		}
	}
	if (*input == 0) {  //到尾端
		return r;
	}
	if ((*rule == '?') || (*rule == *input)) {
		may = canMatch(input + 1, rule + 1);
		if ((may >= 0) && (++may > r)) {
			r = may;
		}
	}
	return r;
}

char * my_find(char  input[],   char rule[]) {
	int len = str_len(input);
	int *match = (int *) malloc(sizeof(int) * len);  //input第i位最多能匹配多少位 匹配不上是-1
	int i,max_pos = - 1;
	char *output = 0;

	for (i = 0; i < len; ++i) {
		match[i] = canMatch(input + i, rule);
		if ((max_pos < 0) || (match[i] > match[max_pos])) {
			max_pos = i;
		}
	}
	if ((max_pos < 0) || (match[max_pos] <= 0)) {  //不匹配
		output = (char *) malloc(sizeof(char));
		*output = 0;   // \0
		return output;
	}
	for (i = 0; i < len;) {
		if (match[i] == match[max_pos]) { //找到匹配
			output = str_join(output, input + i, match[i]);
			i += match[i];
		}
		else {
			++i;
		}
	}
	free(match);
	return output;
}

     2、本题也可以直接写出DP方程,如下代码所示:

//copyright@chpeih 2013/4/23
char* my_find(char  input[],   char rule[])
{
	//write your code here
	int len1,len2;
	for(len1 = 0;input[len1];len1++);
	for(len2 = 0;rule[len2];len2++);
	int MAXN = len1>len2?(len1+1):(len2+1);
	int  **dp;

	//dp[i][j]表示字符串1和字符串2分别以i j结尾匹配的最大长度
	//记录dp[i][j]是由之前那个节点推算过来  i*MAXN+j
	dp = new int *[len1+1];
	for (int i = 0;i<=len1;i++)
	{
		dp[i] = new int[len2+1];

	}

	dp[0][0] = 0;
	for(int i = 1;i<=len2;i++)
		dp[0][i] = -1;
	for(int i = 1;i<=len1;i++)
		dp[i][0] = 0;

	for (int i = 1;i<=len1;i++)
	{
		for (int j = 1;j<=len2;j++)
		{
			if(rule[j-1]=='*'){
				dp[i][j] = -1;
				if (dp[i-1][j-1]!=-1)
				{
					dp[i][j] = dp[i-1][j-1]+1;

				}
				if (dp[i-1][j]!=-1 && dp[i][j]<dp[i-1][j]+1)
				{
					dp[i][j] = dp[i-1][j]+1;

				}
			}else if (rule[j-1]=='?')
			{
				if(dp[i-1][j-1]!=-1){
					dp[i][j] = dp[i-1][j-1]+1;

				}else dp[i][j] = -1;
			} 
			else
			{
				if(dp[i-1][j-1]!=-1 && input[i-1]==rule[j-1]){
					dp[i][j] = dp[i-1][j-1]+1;
				}else dp[i][j] = -1;
			}
		}
	}

	int m = -1;//记录最大字符串长度
	int *ans = new int[len1];
	int count_ans = 0;//记录答案个数
	char *returnans = new char[len1+1];
	int count = 0;
	for(int i = 1;i<=len1;i++)
		if (dp[i][len2]>m){
			m = dp[i][len2];
			count_ans = 0;
			ans[count_ans++] = i-m;
		}else if(dp[i][len2]!=-1 &&dp[i][len2]==m){
			ans[count_ans++] = i-m;
		}

		if (count_ans!=0)
		{    
			int len = ans[0];
			for (int i = 0;i<m;i++)
			{
				printf("%c",input[i+ans[0]]);
				returnans[count++] = input[i+ans[0]];
			}
			for (int j = 1;j<count_ans;j++)
			{
				printf(" ");
				returnans[count++] = ' ';
				len = ans[j];
				for (int i = 0;i<m;i++)
				{
					printf("%c",input[i+ans[j]]);
					returnans[count++] = input[i+ans[j]];
				}
			}
			printf("\n");
			returnans[count++] = '\0';
		}

		return returnans;
}

     欢迎于本文评论下或hero上 show your code


参考文献及推荐阅读

  1. http://zhedahht.blog.163.com/blog/static/25411174200731139971/
  2. http://hero.pongo.cn/,本文大部分代码都取自左边hero上参与答题者提交的代码,欢迎你也去挑战;
  3. 字符串转换成整数题目完整描述:http://hero.pongo.cn/Question/Details?ID=47&ExamID=45
  4. 字符串匹配问题题目完整描述:http://hero.pongo.cn/Question/Details?ID=28&ExamID=28;
  5. linux3.8.4版本下的相关字符串整数转换函数概览:https://git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/tree/lib/vsprintf.c?id=refs/tags/v3.9.4
  6. 关于linux中的likely和unlikely:http://blog.21ic.com/user1/5593/archives/2010/68193.html
  7. 如果你喜欢编程挑战,除了topcoder和hero,你应该还多去leetcode上逛逛:http://leetcode.com/onlinejudge
  8. atio函数的实现:http://blog.csdn.net/njnu_mjn/article/details/9099405
  9. atoi函数的实现: linux内核atoi函数的测试:http://blog.csdn.net/njnu_mjn/article/details/9104143
  10. Nut/OS中atoi函数的实现:http://www.ethernut.de/api/atoi_8c_source.html
  11. 一读者写的hero上“字符串转换成整数”一题的解题报告(测试正确):http://blog.csdn.net/u011070134/article/details/9116831
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

程序员编程艺术第三十~三十一章:字符串转换成整数,通配符字符串匹配 的相关文章

  • Windows下令QProcess弹出CMD界面

    研究了快一下午 xff0c 来回看了QProcess文档中 xff0c 关于start execute statedDetached相关接口的调用说明 xff0c 然而并没有什么用处 差点就准备调用CreateProcess API的接口
  • Linux aarch64交叉编译之cJSON解析器

    对于cJSON项目的交叉编译 xff0c 该项目难度并不大 xff0c 灵活性也较强 该文章的目标是编译一套aarch64 Linux Debian嵌入式版本上可以运行的版本库 xff0c 基本无坑 老套路 xff0c 先把linux桌面版
  • Linux docker(03)可使用GPU渲染的x11docker实战总结

    该系列文章的目的旨在之前的章节基础上 xff0c 使用x11docker构建一个可以使用GPU的docker容器 该容器可以用于3D图形渲染 XR 等使用GPU渲染的程序调试和运行 0 why docker 为什么非要用x11docker
  • 北斗卫星导航系统介绍

    北斗卫星导航系统 导言 2020年3月9日 xff0c 我国在西昌卫星发射中心用长征三号乙运载火箭 xff0c 成功发射北斗系统第五十四颗导航卫星 距离北斗三号系统建成 xff0c 仅一步之遥 从双星导航定位到54颗北斗嵌满星空 xff0c
  • PyQt vs Tkinter – 更好的 GUI 库

    PyQt 和 Tkinter 的比较 在本文中 xff0c 我将分享我自己使用两个 GUI 库 PyQt 和 Tkinter 的旅程和个人经验 我还对具有相同小部件的 Tkinter 和 PyQt GUI 进行了并排比较 本文比较了两个 P
  • Selenium 中的 XPath

    Selenium 中的 XPath 是什么 xff1f Selenium 中最常用的定位器之一 xff0c XPath xff08 也称为 XML 路径 xff09 xff0c 通过页面的 HTML 格式支持您的指南 使用 HTML DOM
  • Centos7 安装yum源

    一 安装wget的rpm包 xff1a 1 下载wget的rpm包 首先去 http mirrors 163 com centos 7 os x86 64 Packages 下找到wget的rpm包 xff0c 复制链接 xff0c 使用c
  • Redis开启远程连接

    1 开启远程连接 redis默认是不支持远程连接 xff0c 需要手动开启 xff0c 在redis conf文件中 xff0c 找到下方法代码 xff1a bind 127 0 0 1 1 这里只允许127 0 0 1登录 xff0c 注
  • NVIDIA NeMo 简介——教程和示例

    NVIDIA NeMo 是一个用于构建新的最先进对话式 AI 模型的工具包 NeMo 有自动语音识别 ASR 自然语言处理 NLP 和文本转语音 TTS 模型的单独集合 每个集合都包含预构建模块 xff0c 其中包含训练数据所需的一切 每个
  • 2010 Qt开发者大会参会总结

    参加了一天的会议该好好的总结一下 1 QML和Meego会在下一步成为重点 2 Qt和Meego在一段发展时期内会有一些过渡性的库和方案 3 Qt在下一个版本会有可能将模块分解开 4 QML的开发效率会很高 xff0c 也很炫 xff0c
  • 人工智能——你需要知道的一切

    什么是人工智能 xff1f 人工智能 一词是美国数学家和计算机科学家约翰 麦卡锡于 1956 年创造的 人工智能是机器像人类一样学习和工作的能力 人工智能的历史可以追溯到古代 机器展示基本人工智能的第一个记录示例是工程师 Wilhelm S
  • 什么是迭代开发

    移动和 Web 开发行业正在快速发展 xff0c 开发人员可以使用新的工具和方法来创建更好的应用程序 为取得成功 xff0c 企业和开发人员必须紧跟软件开发生命周期和技术的最新发展 软件开发生命周期帮助公司高效地交付高质量的产品并减少错误
  • 如何安全升级 TiDB

    作为一个快速成长的开源 NewSQL 数据库 xff0c TiDB经常发布新特性和改进 如果您是 TiDB 用户 xff0c 您可能会发现很难决定是否升级您的版本 您可能也想知道如何让您的升级之旅更安全 更顺畅 xff0c 甚至不被企业注意
  • 推流是什么,直播为什么要推流

    直播可以快速准确地传递现场信息 xff0c 给大家带去强烈的现场感 xff0c 越来越多的人通过网站和手机来观看直播 在这里 xff0c 我们将通过本文系统地向大家阐述直播中另一个重要的操作环节推流 在搜索引擎上有很多朋友咨询推流是什么 x
  • 科学计算与MATLAB语言课程笔记——专题〇 初识matlab

    一 matlab优势 不需要过多了解各种数值计算方法的具体细节和计算公式 xff0c 也不需要繁琐的底层编程 可以专注与实际问题的分析和设计 xff0c 大大地提高工作效率和质量 二 matlab语言的重要功能 matlab matrix
  • 程序员编程艺术第三十四~三十五章:格子取数问题,完美洗牌算法

    第三十四 三十五章 xff1a 格子取数 xff0c 完美洗牌算法 作者 xff1a July caopengcs 绿色夹克衫 致谢 xff1a 西芹 new xff0c 陈利人 xff0c Peiyush Jain xff0c 白石 xf
  • 机器学习面试150题:不只是考SVM xgboost 特征工程

    前言 本博客曾经在10 13年连续4年整理过各大公司数据结构和算法层面的笔试题 面试题 xff0c 与此同时 xff0c 2012年起 xff0c AI越发火热 xff0c 各大公司开始陆续招AI方面的人才 xff0c 很多同学也会从网上找
  • 通俗理解LDA主题模型

    通俗理解LDA主题模 0 前言 印象中 xff0c 最开始听说 LDA 这个名词 xff0c 是缘于rickjin在2013年3月写的一个LDA科普系列 xff0c 叫LDA数学八卦 xff0c 我当时一直想看来着 xff0c 记得还打印过
  • [汇总III]微软等公司数据结构+算法面试第1-80题[前80题首次集体亮相]

    整理 III 微软等公司数据结构 43 算法面试 第1 80题 汇总 首次一次性汇总 公布 由于这些题 xff0c 实在太火了 所以 xff0c 应广大网友建议要求 xff0c 在此把之前已整理公布的前80题 xff0c 现在 xff0c
  • 微软公司等数据结构+算法面试100题(第1-100题)全部出炉

    微软等公司数据结构 43 算法面试100题 第1 100题 首次完整亮相 作者 July 2010年12月6日 更新 xff1a 现今 xff0c 这100题的答案已经全部整理出来了 xff0c 微软面试100题2010年版全部答案集锦 x

随机推荐