面試題精解之二: 字符串、數組(1)
本篇文章發表在下面三個博客中,如果出現排版問題,請移步到另一個博客。
http://www.cppblog.com/flyinghearts
http://www.cnblogs.com/flyinghearts
http://blog.csdn.net/flyinghearts
1 在一個字符串中找到第一個只出現一次的字符,如輸入abac,則輸出b。
2 輸出字符串的所有組合,如"abc"輸出a、b、c、ab、ac、bc、abc。
1 在一個字符串中找到第一個只出現一次的字符,如輸入abac,則輸出b。
本題看似很簡單,開個長度為256的表,對每個字符hash計數就可以了,但很多人寫的代碼都存在bug,可能會發生越界訪問。這是C/C++語言上的一個陷阱,C/C++中的char有三種類型:char、signed char和unsigned char。char類型的符號是由編譯器指定的,一般是有符號的。在對字符進行hash時,應該先將字符轉為無符號類型,不然,下標為負值時,就會出現越界訪問。
另外,可以用一個cache數組,記錄當前找到的只出現一次的字符,避免對原字符串進行第二次遍歷。
char get_first_only_one(const char str[])
{
if (str == NULL) return 0;
const int table_size = 256; //最好寫成: 1 << CHAR_BIT 或 UCHAR_MAX + 1
unsigned count[table_size] = {0};
char cache[table_size];
char *q = cache;
for (const char* p = str; *p != 0; ++p)
if (++count[(unsigned char)*p] == 1) *q++ = *p; //要先轉成無符號數!!!
for (const char* p = cache; p < q; ++p)
if (count[(unsigned char)*p] == 1) return *p;
return 0;
}
2 輸出字符串的所有組合,如"abc"輸出a、b、c、ab、ac、bc、abc。
本題假定字符串中的所有字符都不重復。根據題意,如果字符串中有n個字符,那么總共要輸出2^n – 1種組合。這也就意味着n不可能太大,否則的話,以現在CPU的運算速度,程序運行一次可能需要跑幾百年、幾千年,而且也沒有那么大的硬盤來儲存運行結果。因而,可以假設n小於一個常數(比如64)。
本題最簡潔的方法,應該是采用遞歸法。遍歷字符串,每個字符只能取或不取。取該字符的話,就把該字符放到結果字符串中,遍歷完畢后,輸出結果字符串。
n不是太小時,遞歸法效率很差(棧調用次數約為2^n,尾遞歸優化后也有2^(n-1))。注意到本題的特點,可以構照一個長度為n的01字符串(或二進制數)表示輸出結果中最否包含某個字符,比如:"001"表示輸出結果中不含字符a、b,只含c,即輸出結果為c,而"101",表示輸出結果為ac。原題就是要求輸出"001"到"111"這2^n – 1個組合對應的字符串。
//迭代法
void all_combine(const char str[])
{
if (str == NULL || *str == 0) return;
const size_t max_len = 64;
size_t len = strlen(str);
if (len >= max_len ) {
puts("輸入字符串太長。\n你願意等我一輩子嗎?");
return;
}
bool used[max_len] = {0}; //可以用一個64位無符號數表示used數組
char cache[max_len];
char *result = cache + len;
*result = 0;
while (true) {
size_t idx = 0;
while (used[idx]) { //模擬二進制加法,一共有2^len – 1個狀態
used[idx] = false;
++result;
if (++idx == len) return;
}
used[idx] = true;
*--result = str[idx];
puts(result);
}
}
//遞歸解法
static void all_combine_recursive_impl(const char* str, char* result_begin, char* result_end)
{
if (*str == 0) {
*result_end = 0;
if (result_begin != result_end) puts(result_begin);
return;
}
all_combine_recursive_impl(str + 1, result_begin, result_end); //不取*str
*result_end = *str;
all_combine_recursive_impl(str + 1, result_begin, result_end + 1); //取*str
}
void all_combine_recursive(const char str[])
{
if (str == NULL) return;
const size_t max_len = 64;
size_t len = strlen(str);
if (len >= max_len ) {
puts("輸入字符串太長。\n你願意等我一輩子嗎?");
return;
}
char result[max_len];
all_combine_recursive_impl(str, result, result);
}
3 根據條件找出兩個數。
① 數組中,除了兩個數字出現奇數次外,其它數字都出現偶數次,找出這兩個數字:
② 長度為n的數組,由數字1到n組成,其中數字a不出現,數字b出現兩次,其它的數字恰好出現一次,在不修改原數組的情況下,找出數字a和數字b。
① 數組中,只有兩個數字出現奇數次,其它數字都出現偶數次。假設這兩數為a、b。
利用 異或的性質:a xor a = 0 a xor 0 = a
以及 a xor b = b xor a (a xor b) xor c = a xor (b xor c)
對數組中的所有數進行異或,結果c等於(a xor b)。由於a和b不相乘,因而c不為0,假設c的二進制表示中,第m位不為0。根據第m位是否為0,可以將原數組划分為兩塊,
顯然,a和b不可能分在同一塊。由於各塊中,只有a或b是奇數次出現的,因而各塊所有的數的異或值要么等於a、要么等於b。
struct Pair {
int first;
int second;
};
Pair find_two_appear_once_number(const int data[], size_t len)
{
assert(data && len >= 2);
int xor_all = 0;
for (size_t i = 0; i < len; ++i) xor_all ^= data[i];
//下面的位運算寫法只適用於采用二補數的機器。
const int flag = xor_all & -(unsigned)xor_all;
/*
根據C/C++標准,為了兼容一補數之類的老古董,正確的寫法應該是:
const unsigned tx = *(const unsigned*)(&xor_all);
const unsigned ty = tx & -tx;
const int flag = *(const int*)&ty;
*/
int xor_a = 0;
for (size_t i = 0; i < len; ++i)
if (data[i] & flag) xor_a ^= data[i];
const Pair ret = { xor_a, xor_a ^ xor_all};
return ret;
}
② 有三種解法:
方法一:如果將1到n這n個數也放入數組中,則數字a出現1次,數字b出現3次,可以利用上面的解法,通過兩次遍歷找出數字c與d,再通過第三次遍歷,只遍歷原數組,判斷數字c是否在原數組中,從而確定c與d,哪個等於a,哪個是等於b。
方法二:對方法一進行改進。在第一次遍歷時,額外計算出a與b的差值,利用該信息,可以確定前兩次遍歷找出的只出現奇數次的兩個數,哪個是a、哪個b。
方法三:只進行一次遍歷,計算出a與b的差c,以及它們間的平方差d,由這兩個信息可以直接解得a、b的具體值。
方法一與方法二,在實現中必須計算 0 xor 1 xor 2 xor 3 … xor n,這個有O(1)解法,對偶數a,顯然a xor (a + 1) = 1,因而每4個數的異或值為0,即周期為4。同樣的,可證明,根據(a xor b)不為0的某一位,把原數組划分兩塊后,各塊每8個數的異或值為0,周期為8。
方法二與方法三,在實現中,比須考慮到計算過程中可能出現的溢出問題。避免溢出最好的方法就是將有符號數轉為無符號數,利用“無符號數算術運算是采用模運算,不存在溢出”這個特點。(在32位平台,兩個無符號數a、b的和是定義為:(a + b) mod 2^32。)
對方法三的詳細解釋,可參考本人的文章《避免計算過程中出現溢出的一個技巧》。
//方法二
Pair find_number2(const int arr[], unsigned len)
{
assert(arr && len >= 2);
const unsigned* const data = (const unsigned*)arr;
unsigned xor_all = 0, sum = 0;
for (unsigned i = 0; i < len; ++i) {
const unsigned value = data[i];
xor_all ^= value;
sum += value;
}
//1 + 2 + 3 + ... + len = len * (len + 1) / 2
const unsigned sum_all = (len + 1) / 2u * (len + (len + 1) % 2u);
const unsigned diff = sum_all - sum;
// 0 xor 1 xor 2 xor 3 ... xor len 由於每2個數(2*k與2*k+1)異或值為1,每4個數的異或值為0,
// 可證明總異或值等於arr[len % 4] (其中 arr[4] = {len, 1, len ^ 1 (= len + 1), 0})
const unsigned xor_n = (len % 2u == 0 ? len : 1u) ^ (len % 4u / 2u);
xor_all ^= xor_n;
const unsigned flag = xor_all & -xor_all;
unsigned xor_a = 0;
for (unsigned i = 0; i < len; ++i)
if (data[i] & flag) xor_a ^= data[i];
//每8個數(8*k到8*k+7),根據某一位是否為1,划為兩部分后,每部分的異或值均為0
for (unsigned i = len & ~7u; i <= len; ++i)
if (flag & i) xor_a ^= i;
const unsigned xor_b = xor_a ^ xor_all;
if (xor_a - xor_b == diff) {
const Pair result = { xor_a, xor_b};
return result;
}
const Pair result = { xor_b, xor_a};
return result;
}
//方法三, 這是最高效的做法,但也比方法二麻煩很多。缺點很明顯,效率太依賴於數組長度n的大小。
//32位CPU平台,長度n一定小於2^16次方時,表示一個數的平方值,可采用32位無符號數類型,效率極高。
//長度n一定小於2^31次方時,就必須用到64位無符號數類型,效率稍差。
//長度n若在[2^31, 2^32)時,表示 所有數的和sum,就必須改用64位無符號數類型,效率很差。
Pair find_number3(const int arr[], unsigned len)
{
const unsigned bits = CHAR_BIT * sizeof(unsigned);
#if SMALL_ARRAY
const unsigned max_len = 1u << (bits / 2u);
typedef unsigned int uint;
#else
const unsigned max_len = 1u << (bits - 1);
typedef unsigned long long uint;
#endif
assert(arr && len >= 2 && len < max_len);
const unsigned* const data = (const unsigned*)arr;
unsigned sum = 0;
uint square_sum = 0;
for (unsigned i = 0; i < len; ++i) {
const unsigned value = data[i];
sum += value;
square_sum += (uint)value * value; //注意兩個數的乘積是否會溢出
}
//1 + 2 + 3 + ... + len = len * (len + 1) / 2
const uint sum_all = (len + 1) / 2u * (uint)(len + (len + 1) % 2u);
//1^2 + 2^2 + 3^2 + ... + len^2 = len * (len + 1) * (2 * len + 1) / 6
const unsigned len2 = 2u * len + 1;
const uint square_sum_all = len2 % 3u == 0 ? len2 / 3u * sum_all : sum_all / 3u * len2;
unsigned difference = (unsigned)sum_all - sum;
uint square_difference = square_sum_all - square_sum;
const bool is_negative = difference > INT_MAX;
if (is_negative) {
difference = -difference;
square_difference = -square_difference;
}
assert(difference != 0 && square_difference % difference == 0);
const unsigned sum_two = square_difference / difference;
assert((sum_two + difference) % 2u == 0);
const unsigned larger = (sum_two + difference) / 2u;
const unsigned smaller = (sum_two - difference) / 2u;
if (is_negative) {
const Pair result = { smaller, larger};
return result;
}
const Pair result = { larger, smaller};
return result;
}
4 求數組(或環狀數組)的最大連續(或不連續)子序列和。
本題共有4小題。遇到這類題,首先想到的應該是動態規划思想。(下面的代碼中,都假定所進行的有符號數算術運算不會發生溢出。可以通過改用64位整數表些某些數,來保證這點。)
① 數組的最大連續子序列和(連續子序列和的最大值)
假設f(n)為數組的前n個元素中,以第n個元素結尾的最大連續子序列和,則對第n+1個元素(值為v),只有兩種選擇: 將該元素放入前n-1的最大連續子序列后、新開一個子序列。
因而f(n+1) = max(f(n) + v, v)。顯然 max{ f(i) | i = 1, 2, 3 .. } 即為所求
int max_continuous_sum(const int arr[], size_t len)
{
assert(arr && len > 0);
int cur_max_sum = arr[0], max_sum = cur_max_sum;
for (size_t i = 1; i < len; ++i) {
cur_max_sum = max(cur_max_sum + arr[i], arr[i]);
max_sum = max(cur_max_sum, max_sum);
}
return max_sum;
}
② 環狀數組的最大連續子序列和
從環狀數組中任一點A,從0開始編號,
假設環狀數組中,和最大的連續子序列,是從下標i開始,到下標j(不包括j)結束。
若i < j, 則可以從A點前面斷開,問題轉為“求普通數組的最大連續子序列和”。
若i > j,由於: 所有元素和 = i->j的子序列和 + j->i的子序列和
求“i->j的子序列和最大” 等價於求 “j->i的子序列和最小”。
即“求普通數組的最小連續子序列和”。
int max_ring_continuous_sum(const int arr[], size_t len)
{
assert(arr && len > 0);
int sum = arr[0];
int cur_min_sum = sum, min_sum = sum;
int cur_max_sum = sum, max_sum = sum;
for (size_t i = 1; i < len; ++i) {
const int value = arr[i];
sum += value;
cur_max_sum = max(cur_max_sum + value, value);
max_sum = max(cur_max_sum, max_sum);
cur_min_sum = min(cur_min_sum + value, value);
min_sum = min(cur_min_sum, min_sum);
}
return max(max_sum, sum - min_sum);
}
③ 數組的最大不連續子序列和(不連續子序列和的最大值)
假設f(i)表示數組arr前i個元素的最大不連續子序列和,對第i個數(arr[i-1])只有三種選擇:
忽略該數、放在前i-2個元素的最大不連續子序列后、新開一序列。
(由於要保證不連續,不能放在前i-1個元素的最大不連續子序列后)
因而 f(i) = max(f(i-1), f(i-2) + arr[i-1], arr[i-1]) (i >= 3)
初始值:f(1) = arr[0] (另外,可設f(0) = 0,使f(2)也滿足上式)。顯然,f(n)即為所求。
int max_discontinuous_sum(const int arr[], size_t len)
{
assert(arr && len > 0);
int max_sum = arr[0], prev_max_sum = 0;
for (size_t i = 1; i < len; ++i) {
const int value = arr[i];
const int old_max_sum = max_sum;
max_sum = max(max_sum, prev_max_sum + value, value);
prev_max_sum = old_max_sum;
}
return max_sum;
}
其它DP方法:
⒈ 假設f(i)表示前i個元素中, 以第i個元素結尾的最大不連續子序列和,
g(i)表示前i個元素中,不以第i個元素結尾的最大不連續子序列和,
(也就是前i-1個元素的最大不連續子序列和)
則 f(i) = max(g(i-1)+arr[i-1], arr[i-1]) (i >= 3)
g(i) = max(f(i-1), g(i-1)) (i >= 3)
初始值: f(2) = arr[1], g(2) = arr[0]
當n>=2時,max(f(n), g(n)) 即為所求
⒉ 假設f(i)表示前i個元素中,以第i個元素結尾的最大不連續子序列和,
g(i)表示前i個元素中,最大不連續子序列和。
則 f(i) = max(g(i-2)+arr[i-1], arr[i-1]) (i >= 3)
g(i) = max(f(i), g(i-1)) (i >= 2)
初始值: g(1) = arr[0] (可設g(0) = 0,使f(2)也滿足上式)
g(n)即為所求
④ 環狀數組的最大不連續子序列和
假設數組長度為n,若從環狀數組中任一點,從0開始編號,則結束編號為n-1。由於要保證不連續,編號為0的元素和編號為n-1的元素不能同時取,則對編號為0到n-2和編號為1到n-1的兩個子數組分別求最大不連續子序列和,較大的即為所求。具體實現上,可以先求編號為0到n-1的具有最大和的不連續子序列,是否同時包含編號為0和編號為n-1的元素,若不同時包含的話,所得結果即為所求;若同時包含的話,需要再計算編號為1到n-1的子數組的最大不連續子序列和。這需要遍歷數組一到二次,下面是只遍歷一次的寫法:
int max_ring_discontinuous_sum(const int arr[], size_t len)
{
assert(arr && len > 0);
if (len == 1) return arr[0];
int max_sum1 = max(arr[0], arr[1]), prev_max_sum1 = arr[0];
int max_sum2 = arr[1], prev_max_sum2 = 0;
for (size_t i = 2; i < len - 1; ++i) {
#if 0
if (max_sum1 == max_sum2) {
for (size_t j = i; j < len; ++j) {
const int value = arr[j];
const int old_max_sum2 = max_sum2;
max_sum2 = max(max_sum2, prev_max_sum2 + value, value);
prev_max_sum2 = old_max_sum2;
}
return max_sum2;
}
#endif
const int value = arr[i];
const int old_max_sum1 = max_sum1;
const int old_max_sum2 = max_sum2;
max_sum1 = max(max_sum1, prev_max_sum1 + value, value);
max_sum2 = max(max_sum2, prev_max_sum2 + value, value);
prev_max_sum1 = old_max_sum1;
prev_max_sum2 = old_max_sum2;
}
const int value = arr[len - 1];
max_sum2 = max(max_sum2, prev_max_sum2 + value, value);
return max(max_sum1, max_sum2);
}