题目来自于
https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/01.00.md
非常感谢July大神和众网友。
本博文初衷是为了方便自己准备面试,有一篇文章可以看所有的东西,省的翻来翻去。
代码是手打的Qt C++,解法中不考虑实现最简单的毫无难度的暴力解法了。
因为很短,不区分*.cc和*.hh文件了,都写在一起; 注释初衷也是为了自己理解。
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
给定一个字符串,要求把字符串前面的若干个字符移动到字符串的尾部,如把字符串“abcdef”前面的2个字符'a'和'b'移动到字符串的尾部,使得原字符串变成字符串“cdefab”。请写一个函数完成此功能,要求对长度为n的字符串操作的时间复杂度为 O(n),空间复杂度为 O(1)。
// 编程之美:1.1旋转字符串
// 题目:给定一个字符串,要求把字符串前面的若干个字符移动到字符串的尾部,
// 如把字符串“abcdef”前面的2个字符'a'和'b'移动到字符串的尾部,使得原
// 字符串变成字符串“cdefab”。请写一个函数完成此功能,要求对长度为n的字
// 符串操作的时间复杂度为 O(n),空间复杂度为 O(1)。
#include
#include
using namespace std;
// 记得用reference,不然改变后的string传不回来
void leftShiftString(string& str, int shiftNum);
void reverseString(string& str, int from, int to);
void rotate(string& str, int shiftNum);
int main()
{
string test_str = "abcd";
int shiftNumber = 2;
cout << "before: " << test_str << endl;
//leftShiftString(test_str, shiftNumber);
rotate(test_str, shiftNumber);
cout << "after : " << test_str << endl;
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// 推荐解法: 三步翻转法
// 将一个字符串分成X和Y两个部分,在每部分字符串上定义反转操作,如X^T,即把X的所有字符反转
// (如,X="abc",那么X^T="cba"),那么就得到下面的结论:(X^TY^T)^T=YX,显然就解决了字符串的反转问题。
// 例如,字符串 abcdef ,若要让def翻转到abc的前头,只要按照下述3个步骤操作即可:
// 1) 首先将原字符串分为两个部分,即X:abc,Y:def;
// 2) 将X反转,X->X^T,即得:abc->cba;将Y反转,Y->Y^T,即得:def->fed。
// 3) 反转上述步骤得到的结果字符串X^TY^T,即反转字符串cbafed的两部分(cba和fed)给予反转,
// cbafed得到defabc,形式化表示为(X^TY^T)^T=YX,这就实现了整个反转。
void leftShiftString(string& str, int shiftNum)
{
int str_len = str.length();
if (str_len == 0)
{
// 空串忽视
return;
}else
{
// 取模两个好处:一是避免不必要的移动,
// 二是防止index越界,尤其是shiftNum可能有负值,这时候要加上str_len避免取模返回负值
shiftNum = (shiftNum + str_len) % str_len;
}
if (shiftNum == 0)
{
//移动0位可以直接返回
return;
}
// 原字符串的X部分 -> X^T
reverseString(str, 0, shiftNum - 1);
cout << "X part: " << str << endl;
// 原字符串的Y部分 -> Y^T
reverseString(str, shiftNum, str_len - 1);
cout << "Y part: " << str << endl;
// (X^TY^T)^T -> YX
reverseString(str, 0, str_len - 1);
cout << "Both part: " << str << endl;
}
void reverseString(string& str, int from, int to)
{
while(from < to)
{
swap(str[from++], str[to--]);
}
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
//其他解法: 不是很通用,但是我自己想出来的,见
//http://blog.csdn.net/u013300875/article/details/44126909
//思路简单说就是我们知道str_len,也知道shiftNum,所以对于一个特定位置current的字符,
//它的目标位置target其实就确定了,我们只要把这它从current移动到target就行了
void rotate(string& str, int shiftNum)
{
int str_len = str.length();
if (str_len == 0)
{
// 空串忽视
return;
}
if (shiftNum == 0)
{
//移动0位可以直接返回
return;
}
//每个字符应该只被移动一次,也必须也被移动一次
//所以我们在 count == str_len 时停止
int count = 0;
for (int i = 0; i < str_len ; ++i)
{
int index = i;
char tmp = str[index];
while(1)
{
count++;
// 在index位置的字符将被移动到index+new
int index_new = (index - shiftNum + str_len) % str_len;
// 把index_new位置的元素保存到空出来的i位置
str[i] = str[index_new];
// 完成str[index] -> str[index_new]的移动
str[index_new] = tmp;
// 准备处理原来位于index_new的字符
tmp = str[i];
index = index_new;
// 如果我们已经处理了所有的字符,即完成了一个圈
// 我们是从i这个位置开始处理的,如果我们下一个要处理的又是i,
// 我们可以跳过它了
if (i == index)
{
break;
}
}
if (count == str_len)
{
break;
}
}
}
给定两个分别由字母组成的字符串A和字符串B,字符串B的长度比字符串A短。请问,如何最快地判断字符串B中所有字母是否都在字符串A里?
为了简单起见,我们规定输入的字符串只包含大写英文字母,请实现函数bool StringContains(string &A, string &B)
比如,如果是下面两个字符串:
String 1:ABCD
String 2:BAD
答案是true,即String2里的字母在String1里也都有,或者说String2是String1的真子集。
如果是下面两个字符串:
String 1:ABCD
String 2:BCE
答案是false,因为字符串String2里的E字母不在字符串String1里。
同时,如果string1:ABCD,string 2:AA,同样返回true。
// 题目:给定两个分别由字母组成的字符串A和字符串B,字符串B的长度比字符串A短。
// 请问,如何最快地判断字符串B中所有字母是否都在字符串A里?
// 为了简单起见,我们规定输入的字符串只包含大写英文字母,请实现函数
// bool StringContains(string &A, string &B)
// 比如,如果是下面两个字符串:
// String 1:ABCD
// String 2:BAD
// 答案是true,即String2里的字母在String1里也都有,或者说String2是String1的真子集。
// 如果是下面两个字符串:
// String 1:ABCD
// String 2:BCE
// 答案是false,因为字符串String2里的E字母不在字符串String1里。
// 同时,如果string1:ABCD,string 2:AA,同样返回true。
#include
#include
#include
using namespace std;
bool StringContains(string &str1, string &str2);
int main()
{
string str1 = "ABCD";
string str2 = "BAD";
string str3 = "BCE";
cout << StringContains(str1, str2) << endl;
cout << StringContains(str1, str3) << endl;
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// 推荐解法: HashTable
// 可以先把长字符串str1中的所有字符都放入一个Hashtable里,然后轮询短字符串str2,
// 看短字符串str2的每个字符是否都在HashTable里.
// 如果都存在,说明长字符串str1包含短字符串str2,否则,说明不包含。
// 再进一步,我们可以对字符串str1,用位运算(26bit整数表示)计算出一个“签名”,
// 再用str2中的字符到A里面进行查找。
// 这个方法的实质是用一个整数代替了HashTable,空间复杂度为O(1),时间复杂度还是O(n + m)。
// But, 用26bits整数代替HashTable太取巧了,我还是觉得用STL map
// 搞一个真正的HashTable比较好,毕竟英文字母起码还有大小写呢!
// map是用红黑树实现的,插入和查询都是O(log n),
// 插入str1的字符,n次插入; 查询str2的字符,m次查询
// 时间复杂度是O((m+n)logn)级别
bool StringContains(string &str1, string &str2)
{
int str1_len = str1.length();
int str2_len = str2.length();
// str1 应该比 str2 长
if ((str1_len == 0)|| (str2_len == 0) ||(str1_len <= str2_len))
{
return false;
}
// 用 stl map 创建一个HashTable
map<char, bool> HashTable;
// 插入所有的str1的字符,有的话就是true O(n)
for (int ii = 0; ii < str1_len; ++ii)
{
HashTable[str1[ii]] = true;
}
// 查询每一个str2的字符,O(m)
for (int ii = 0; ii < str2_len; ++ii)
{
// bool型的默认值是false
// 所以 HashTable['a nonexisting letter'] = false
if (HashTable[str2[ii]] == false)
{
return false;
}
}
return true;
}
输入一个由数字组成的字符串,把它转换成整数并输出。例如:输入字符串"123",输出整数123。
给定函数原型int StrToInt(const char *str)
,实现字符串转换成整数的功能,不能使用库函数atoi。
// 编程之美:1.3字符串转整数
// 题目:输入一个由数字组成的字符串,把它转换成整数并输出。
// 例如:输入字符串"123",输出整数123。
// 给定函数原型int StrToInt(const char *str) ,实现字符串转换成整数的功能,不能使用库函数atoi。
#include
#include
#include
#include
using namespace std;
int StrToInt(const char *str);
int main()
{
string test_str = "-2147483648";
cout << "Integer: " << StrToInt(test_str.c_str()) << endl;
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// 推荐解法:
// 此题的基本思路便是:从左至右扫描字符串,把之前得到的数字乘以10,再加上当前字符表示的数字。
// 一些细节:
// 1)空指针输入:输入的是指针,在访问空指针时程序会崩溃,因此在使用指针之前需要先判断指针是否为空。
// 2)正负符号:整数不仅包含数字,还有可能是以'+'或'-'开头表示正负整数,因此如果第一个字符是'-'号,则要把得到的整数转换成负整数。
// 3)非法字符:输入的字符串中可能含有不是数字的字符。因此,每当碰到这些非法的字符,程序应停止转换。
// 4)整型溢出:输入的数字是以字符串的形式输入,因此输入一个很长的字符串将可能导致溢出。
// 细节4特别重要,所以我们要使用#include里的宏定义INT_MAX和INT_MIN,
// 如果溢出了,就返回INT_MAX或INT_MIN
int StrToInt(const char *str)
{
// 细节1) 空指针输入
if (str == NULL)
{
return 0;
}
// 细节3) 非法字符中的空白字符
while(isspace(*str))
{
++str;
}
// 细节2) 正负号(可能有也可能没有,默认为+)
int sign = 1;
if ((*str == '+') || (*str == '-'))
{
if (*str == '-')
{
sign = -1;
}
++str;
}
//确定是数字后才执行循环
int n = 0;
while (isdigit(*str))
{
//处理溢出
int c = *str - '0';
if (sign > 0 && (n > INT_MAX / 10 || (n == INT_MAX / 10 && c > INT_MAX % 10)))
{
n = INT_MAX;
break;
}
else if (sign < 0 && (-n < INT_MIN / 10 || (-n == INT_MIN / 10 && -c < INT_MIN % 10)))
{
n = INT_MIN;
break;
}
//把之前得到的数字乘以10,再加上当前字符表示的数字。
n = n * 10 + c;
++str;
}
return sign > 0 ? n : -n;
}
回文,英文palindrome,指一个顺着读和反过来读都一样的字符串,比如madam、我爱我,这样的短句在智力性、趣味性和艺术性上都颇有特色,中国历史上还有很多有趣的回文诗。
那么,我们的第一个问题就是:判断一个字串是否是回文?
为了增加难度,改为单向链表结构的字串e.g. A->B->C->B->A 并且事先不知道字串长度
即 判断一条单向链表是不是“回文”
// 编程之美:1.4回文判断
// 题目:回文,英文palindrome,指一个顺着读和反过来读都一样的字符串,
// 比如madam、我爱我,这样的短句在智力性、趣味性和艺术性上都颇有特色,中国历史上还有很多有趣的回文诗。
// 那么,我们的第一个问题就是:判断一个字串是否是回文?
// 为了增加难度,改为单向链表结构的字串e.g. A->B->C->B->A 并且事先不知道字串长度
// 即 判断一条单向链表是不是“回文”
#include
#include
#include
using namespace std;
//define node
typedef struct node* pNode;
typedef struct node
{
char ch;
pNode next;
}Node;
bool isPalindromeList(pNode myList);
bool isPalindromeList2(pNode myList);
pNode reverseList(pNode from, pNode to);
int main()
{
// create a list
pNode myList = NULL;
pNode head = NULL;
string init = "ABCBA";
for(size_t ii = 0; ii < init.length(); ++ii)
{
if (head == NULL)
{
head = new Node;
myList = head;
head->ch = init[ii];
head->next = NULL;
}else
{
head->next = new Node;
head = head->next;
head->ch = init[ii];
head->next = NULL;
}
}
// print the list
head = myList;
for (size_t ii = 0; ii < init.length(); ++ii)
{
cout << head->ch << "->";
head = head->next;
}
cout << "NULL" << endl;
cout << "isPalindromeList1: " << isPalindromeList(myList) << endl;
cout << "isPalindromeList2: " << isPalindromeList2(myList) << endl;
// // free the list
// for (size_t ii = 0; ii < init.length(); ++ii)
// {
// head = myList;
// myList = myList->next;
// if (head != NULL)
// {
// delete head;
// }
// }
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// 解法一: 采用快慢指针,慢指针入栈,当快指针为NULL时,出栈和慢指针比较即可。
// 时间复杂度O(n),空间复杂度O(n)。
// 优点:比较好实现,且没有修改链表
// 缺点:需要分配内存
bool isPalindromeList(pNode myList)
{
if (myList == NULL)
{
return false;
}
// 快慢指针
pNode fast = myList;
pNode slow = myList;
// 需要判断整个链表有奇数个还是偶数个node
// false是奇数个, true是偶数个
bool evenOrOdd = true;
stack<char> myStack;
// 把slow移动到整个链表的中间
while (fast != NULL)
{
myStack.push(slow->ch);
// 慢指针一步,快指针两步
slow = slow->next;
fast = fast->next;
if (fast != NULL)
{
fast = fast->next;
}else
{
// fast不是slow的两倍,说明是奇数个node
evenOrOdd = false;
}
}
// 奇数个node的情况下,正中间的那个需要出栈,不参加比较
if (evenOrOdd == false)
{
myStack.pop();
}
// 现在慢指针指向了后半段链表的开头,而stack的top是
// 前半段链表的倒序开头,可以比较了
while ((slow != NULL) && (!myStack.empty()))
{
if (slow->ch != myStack.top() )
{
return false;
}
slow = slow->next;
myStack.pop();
}
return true;
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// 解法二:采用快慢指针,当快指针为NULL时,翻转慢指针。
// 比较前半部分链表和翻转链表即可。时间复杂度O(n),空间复杂度O(1)。
// 优点:节约空间
// 缺点:改变了链表的值,而且后半段链表翻转后很可能和前半段链表断开了
// 最后delete 返还内存时会出现内存泄露,要前半段和后半段分别delete
pNode reverseList(pNode from, pNode to)
{
pNode tmp1 = from;
pNode tmp2 = NULL;
while (tmp1->next != to)
{
tmp2 = tmp1->next;
tmp1->next = tmp2->next;
tmp2->next = from;
from = tmp2;
}
return from;
}
bool isPalindromeList2(pNode myList)
{
if (myList == NULL)
{
return false;
}
// 链表头指针,快慢指针
pNode head = myList;
pNode fast = myList;
pNode slow = myList;
// 把slow移动到整个链表的中间
while (fast != NULL)
{
// 慢指针一步,快指针两步
slow = slow->next;
fast = fast->next;
if (fast != NULL)
{
fast = fast->next;
}
}
// 翻转后半段链表
pNode head2 = reverseList(slow, NULL);
// 比较前半段链表和翻转后的后半段链表
while (head != NULL && head2 != NULL)
{
if (head->ch != head2->ch)
{
return false;
}
head = head->next;
head2 = head2->next;
}
return true;
}
给定一个字符串,求它的最长回文子串的长度
// 编程之美:1.5最长回文字串
// 题目:给定一个字符串,求它的最长回文子串的长度
#include
#include
using namespace std;
string preProcess(string str);
string longestPalindrome(string s);
int main()
{
string str = "absfsfsfsfffffffffse";
cout << str << ": " << longestPalindrome(str) << endl;
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// 推荐解法: Manacher算法
// 时间复杂度O(N)空间复杂度O(N)
// 首先用一个非常巧妙的方式,将所有可能的奇数/偶数长度的回文子串都转换成了奇数长度:
// 在每个字符的两边都插入一个特殊的符号。比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。
// 为了进一步减少编码的复杂度,可以在字符串的开始加入另一个特殊字符,这样就不用特殊处理越界问题,
// 比如$#a#b#a#(注意,下面的代码是用C语言写就,由于C语言规范还要求字符串末尾有一个'\0'所以正好OK,
// 但其他语言可能会导致越界)。
// 下面以字符串12212321为例,经过上一步,变成了 S[] = "$#1#2#2#1#2#3#2#1#";
// 实在是太长了 看这个http://articles.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html
// 或者 http://www.felix021.com/blog/read.php?2040
string preProcess(string str)
{
int n = str.length();
if (n == 0)
{
return "^$";
}
string ret = "^";
for (int i = 0; i < n; i++)
{
ret += "#" + str.substr(i, 1);
}
ret += "#$";
return ret;
}
string longestPalindrome(string s)
{
string T = preProcess(s);
int n = T.length();
int *P = new int[n];
int C = 0, R = 0;
for (int i = 1; i < n-1; i++)
{
int i_mirror = 2*C-i; // equals to i' = C - (i-C)
P[i] = (R > i) ? min(R-i, P[i_mirror]) : 0;
// Attempt to expand palindrome centered at i
while (T[i + 1 + P[i]] == T[i - 1 - P[i]])
{
P[i]++;
}
// If palindrome centered at i expand past R,
// adjust center based on expanded palindrome.
if (i + P[i] > R)
{
C = i;
R = i + P[i];
}
}
// Find the maximum element in P.
int maxLen = 0;
int centerIndex = 0;
for (int i = 1; i < n-1; i++)
{
if (P[i] > maxLen)
{
maxLen = P[i];
centerIndex = i;
}
}
delete[] P;
return s.substr((centerIndex - 1 - maxLen)/2, maxLen);
}
输入一个字符串,打印出该字符串中字符的所有排列。
例如输入字符串abc,则输出由字符a、b、c 所能排列出来的所有字符串
abc、acb、bac、bca、cab 和 cba。
// 编程之美:1.6字符串的全排列
// 输入一个字符串,打印出该字符串中字符的所有排列。
// 例如输入字符串abc,则输出由字符a、b、c 所能排列出来的所有字符串
// abc、acb、bac、bca、cab 和 cba。
#include
#include
#include
using namespace std;
bool CalcAllPermutation(string& perm);
int main()
{
string str = "15342";
// 注意next_permutation只能找到下一个字符串,但是之前的字符串无法找到
// 所以我们必须人工保证是从最小的情况开始
sort(str.begin(), str.end());
cout << str << endl;
while(1)
{
if (!CalcAllPermutation(str))
{
break;
}
cout << str << endl;
}
}
//%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// 推荐解法: 字典序排列 (next permuatation算法)
// 举个例子做启发: 现在我们要找21543的下一个排列,我们可以从左至右逐个扫描每个数,
// 看哪个能增大(至于如何判定能增大,是根据如果一个数右面有比它大的数存在,那么这个数就能增大),
// 我们可以看到最后一个能增大的数是:x = 1。
// 而1应该增大到多少?1能增大到它右面比它大的那一系列数中最小的那个数,即:y = 3,
// 故此时21543的下一个排列应该变为23xxx,显然 xxx(对应之前的B’)应由小到大排,
// 于是我们最终找到比“21543”大,但字典顺序尽量小的23145,找到的23145刚好比21543大。
//由这个例子可以得出next_permutation算法流程为:
// next_permutation算法
// 定义
// 升序:相邻两个位置ai < ai+1,ai 称作该升序的首位
// 步骤(二找、一交换、一翻转)
// 找到排列中最后(最右)一个升序的首位位置i,x = ai
// 找到排列中第i位右边最后一个比ai 大的位置j,y = aj
// 交换x,y
// 把第(i+1)位到最后的部分翻转
// 还是拿上面的21543举例,那么,应用next_permutation算法的过程如下:
// x = 1;
// y = 3
// 1和3交换
// 得23541
// 翻转541
// 得23145
// 23145即为所求的21543的下一个排列。
// 由于全排列总共有n!种排列情况,所以时间复杂度为O(n!)
bool CalcAllPermutation(string& perm)
{
int i;
int num = perm.length();
//①找到排列中最后(最右)一个升序的首位位置i,x = ai
for (i = num - 2; (i >= 0) && (perm[i] >= perm[i + 1]); --i)
{
;
}
// 已经找到所有排列
if (i < 0){
return false;
}
int k;
//②找到排列中第i位右边最后一个比ai 大的位置j,y = aj
for (k = num - 1; (k > i) && (perm[k] <= perm[i]); --k)
{
;
}
//③交换x,y
swap(perm[i], perm[k]);
//④把第(i+ 1)位到最后的部分翻转
int from = i+1;
int to = num-1;
while(from < to)
{
swap(perm[from++], perm[to--]);
}
return true;
}