1. 程式人生 > >程式設計師程式設計藝術第三十~三十一章:字串轉換成整數,萬用字元字串匹配

程式設計師程式設計藝術第三十~三十一章:字串轉換成整數,萬用字元字串匹配

第三十~三十一章:字串轉換成整數,帶萬用字元的字串匹配

前言

    之前本一直想寫寫神經網路演算法和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、很快,可能你就會寫下如下程式碼:
//[email protected] 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來):
//[email protected]_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、上面說給的程式沒有“很好的解決溢位問題。由於輸入的數字是以字串的形式輸入,因此有可能輸入一個很大的數字轉換之後會超過能夠表示的最大的整數而溢位”。那麼,到底程式碼該如何寫呢?    像下面這樣?:
//[email protected] 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原始碼是有問題的。 7microsoft既然不行,讀者想必很自然的想到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代替,不能不說此法頗妙。

   他的程式碼如下,如有問題請指出:

//[email protected]_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儲存到變數中, 防止重複計算
    這樣,優化後的程式碼為:
//[email protected]_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、本題與上述第三十章的題不同,上題字串轉換成整數更多考察對思維的全面性和對細節的處理,本題則更多的是程式設計技巧。閒不多說,直接上程式碼:

//[email protected]_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方程,如下程式碼所示:

//[email protected] 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

參考文獻及推薦閱讀

相關推薦

程式設計師程式設計藝術~字串轉換整數字元字串匹配

第三十~三十一章:字串轉換成整數,帶萬用字元的字串匹配前言    之前本一直想寫寫神經網路演算法和EM演算法,但寫這兩個演算法實在需要大段大段的時間,而平時上班,週末則跑去北大教室自習看書(順便以時間為序,說下過去半年看過的自覺還不錯的數學史方面的書:《數理統計學簡史》《微積

程式設計師程式設計藝術-----續-----Top K演算法問題的實現

                     程式設計師程式設計藝術:第三章續、Top K演算法問題的實現前奏    在上一篇文章,程式設計師面試題狂想曲:第三章、尋找最小的k個數中,後來為了論證類似快速排序中partition的方法在最壞情況下,能在O(N)的時間複雜度內找到最小的k個數,而前前後後update

程式設計師程式設計藝術----------尋找最小的k個數

                    程式設計師程式設計藝術:第三章、尋找最小的k個數作者:July。時間:二零一一年四月二十八日。致謝:litaoye, strugglever,yansha,luuillu,Sorehead,及狂想曲創作組。微博:http://weibo.com/julyweibo。出處

程式設計師程式設計藝術-----五 ~ 二-----全排列、跳臺階、奇偶、第一個出現字元、一致性hash

第十六~第二十章:全排列,跳臺階,奇偶排序,第一個只出現一次等問題作者:July、2011.10.16。出處:http://blog.csdn.net/v_JULY_v。引言    最近這幾天閒職在家,一忙著投簡歷,二為準備面試而蒐集整理各種面試題。故常常關注個人所建的Algorithms1-14群內朋友關於

程式設計師程式設計藝術----------最長公共子序列(LCS)問題

  程式設計師程式設計藝術第十一章:最長公共子序列(LCS)問題0、前言    程式設計師程式設計藝術系列重新開始創作了(前十章,請參考程式設計師程式設計藝術第一~十章集錦與總結)。回顧之前的前十章,有些程式碼是值得商榷的,因當時的程式碼只顧闡述演算法的原理或思想,所以,很多的與程式碼規範相關的問題都未能做到

程式設計師程式設計藝術----- ~ -----海量整數處理、蓄水池抽樣、迴文

       程式設計師程式設計藝術第十二~十五章:中籤概率,IP訪問次數,迴文等問題(初稿)作者:上善若水.qinyu,BigPotato,luuillu,well,July。程式設計藝術室出品。前言    本文的全部稿件是由我們程式設計藝術室的部分成員:上善若水.qinyu,BigPotato,luuil

程式設計師程式設計藝術----------尋找滿足和為定值的兩個或多個數

                    程式設計師程式設計藝術:第五章、尋找和為定值的兩個或多個數 前奏    希望此程式設計藝術系列能給各位帶來的是一種方法,一種創造力,一種舉一反三的能力。本章依然同第四章一樣,選取比較簡單的面試題,恭祝各位旅途愉快。同樣,有任何問題,歡迎不吝指正。謝謝。第一節、尋找和為定

程式設計師程式設計藝術----------求解500以內的親和數(素數、完數)

前奏    本章陸續開始,除了繼續保持原有的字串、陣列等面試題之外,會有意識的間斷性節選一些有關數字趣味小而巧的面試題目,重在突出思路的“巧”,和“妙”。本章親和數問題之關鍵字,“500萬”,“線性複雜度”。第一節、親和數問題題目描述:求500萬以內的所有親和數如果兩個數a和b,a的所有真因數之和等於b,b的

程式設計師程式設計藝術----------求連續子陣列的最大和

 程式設計師程式設計藝術:第七章、求連續子陣列的最大和 前奏希望更多的人能和我一樣,把本狂想曲系列中的任何一道面試題當做一道簡單的程式設計題或一個實質性的問題來看待,在閱讀本狂想曲系列的過程中,希望你能儘量暫時放下所有有關面試的一切包袱,潛心攻克每一道“程式設計題”,在解決程式設計題的過程中,好好享受程式設計

程式設計師程式設計藝術----------現場編寫類似strstr/strcpy/strpbrk的函式

               第四章、現場編寫類似strstr/strcpy/strpbrk的函式   作者:July。    說明: 如果在部落格中程式碼使用了\n,csdn blog系統將會自動回給我變成/n。據後續驗證,可能是原來舊blog版本的bug,新版已不存在此問題。至於,本文程式碼,日後統一修正

程式設計師程式設計藝術----------閒話連結串列追趕問題

前奏    有這樣一個問題:在一條左右水平放置的直線軌道上任選兩個點,放置兩個機器人,請用如下指令系統為機器人設計控制程式,使這兩個機器人能夠在直線軌道上相遇。(注意兩個機器人用你寫的同一個程式來控制)。    指令系統:只包含4條指令,向左、向右、條件判定、無條件跳轉。其中向左(右)指令每次能控制機器人向左

程式設計師程式設計藝術----------從頭至尾漫談虛擬函式

前奏    有關虛擬函式的問題層出不窮,有關虛擬函式的文章千篇一律,那為何還要寫這一篇有關虛擬函式的文章呢?看完本文後,相信能懂其意義之所在。同時,原狂想曲系列已經更名為程式設計師程式設計藝術系列,因為不再只專注於“面試”,而在“程式設計”之上了。ok,如果有不正之處,望不吝賜教。謝謝。第一節、一道簡單的虛擬

程式設計師程式設計藝術-----第二八 ~ 二-----最大連續乘積子串、字串編輯距離

               第二十八~二十九章:最大連續乘積子串、字串編輯距離前言    時間轉瞬即逝,一轉眼,又有4個多月沒來更新blog了,過去4個月都在幹啥呢?對的,今2013年元旦和朋友利用業餘時間一起搭了個方便朋友們找工作的程式設計面試演算法論壇:為學論壇http://www.51weixue.c

程式設計師程式設計藝術-----第二十三 ~ 二-----楊氏矩陣、不重複Hash編碼

  第二十三、四章:楊氏矩陣查詢,倒排索引關鍵詞Hash不重複編碼實踐作者:July、yansha。程式設計藝術室出品。出處:結構之法演算法之道。前言    本文闡述兩個問題,第二十三章是楊氏矩陣查詢問題,第二十四章是有關倒排索引中關鍵詞Hash編碼的問題,主要要解決不重複以及追加的功能,同時也是經典演算法研

程式設計師程式設計藝術-----第二-----不改變正負數相對順序重新排列陣列

10、翻轉句子中單詞的順序。題目:輸入一個英文句子,翻轉句子中單詞的順序,但單詞內字元的順序不變。句子中單詞以空格符隔開。為簡單起見,標點符號和普通字母一樣處理。例如輸入“I am a student.”,則輸出“student. a am I”。而此題可以在O(N)的時間複雜度內解決:    由於本題需要翻

程式設計師程式設計藝術-----第二-----二分查詢實現(Jon Bentley90%程式設計師無法正確實現)

第二十五章:二分查詢實現(Jon Bentley:90%程式設計師無法正確實現)作者:July出處:結構之法演算法之道引言    Jon Bentley:90%以上的程式設計師無法正確無誤的寫出二分查詢程式碼。也許很多人都早已聽說過這句話,但我還是想引用《程式設計珠璣》上的如下幾段文字:  “二分查詢可以解決

程式設計師程式設計藝術第二基於給定的文件生成倒排索引(含原始碼下載)

第二十六章:基於給定的文件生成倒排索引的編碼與實踐作者:July、yansha。出處:結構之法演算法之道引言    本週實現倒排索引。實現過程中,尋找資料,結果發現找份資料諸多不易:1、網上搜倒排索引實現,結果千篇一律,例子都是那幾個同樣的單詞;2、到谷歌學術上想找點稍微有價

程式設計師程式設計藝術、求解500以內的親和數

   第六章、親和數問題--求解500萬以內的親和數 前奏     本章陸續開始,除了繼續保持原有的字串、陣列等面試題之外,會有意識的間斷性節選一些有關數字趣味小而巧的面試題目,重在突出思路的“巧”,和“妙”。本章親和數問題之關鍵字,“500萬”,“線性複雜度”。

程式設計師程式設計藝術第二Jon Bentley90%無法正確實現二分查詢

第二十五章:二分查詢實現(Jon Bentley:90%程式設計師無法正確實現) 作者:July 出處:結構之法演算法之道 引言     Jon Bentley:90%以上的程式設計師無法正確無誤的寫出二分查詢程式碼。也許很多人都早已聽說過這句話,但我

程式設計師程式設計藝術、現場編寫類似strstr/strcpy/strpbrk的函式

               第四章、現場編寫類似strstr/strcpy/strpbrk的函式    前奏    有網友向我反應,之前三章(http://t.cn/hgVPmH)的面試題目,是否有點太難了。誠如他所說,絕大部分公司的面試題不會像微軟等公司的面試題目出的那麼