数据结构:字符串 C++

题目来自于

https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/01.00.md

非常感谢July大神和众网友。


本博文初衷是为了方便自己准备面试,有一篇文章可以看所有的东西,省的翻来翻去。

代码是手打的Qt C++,解法中不考虑实现最简单的毫无难度的暴力解法了。

因为很短,不区分*.cc和*.hh文件了,都写在一起; 注释初衷也是为了自己理解。

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

1.1 旋转字符串

题目描述

给定一个字符串,要求把字符串前面的若干个字符移动到字符串的尾部,如把字符串“abcdef”前面的2个字符'a'和'b'移动到字符串的尾部,使得原字符串变成字符串“cdefab”。请写一个函数完成此功能,要求对长度为n的字符串操作的时间复杂度为 O(n),空间复杂度为 O(1)。

分析与C++实现

// 编程之美: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;
        }
    }
}


1.2 字符串包含

题目描述

给定两个分别由字母组成的字符串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。

分析与C++实现

// 编程之美:1.2 字符串包含
// 题目:给定两个分别由字母组成的字符串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;
}

1.3 字符串转换成整数

题目描述

输入一个由数字组成的字符串,把它转换成整数并输出。例如:输入字符串"123",输出整数123。

给定函数原型int StrToInt(const char *str) ,实现字符串转换成整数的功能,不能使用库函数atoi。


分析与C++实现

// 编程之美: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;
}


1.4 回文判断

题目描述

回文,英文palindrome,指一个顺着读和反过来读都一样的字符串,比如madam、我爱我,这样的短句在智力性、趣味性和艺术性上都颇有特色,中国历史上还有很多有趣的回文诗。

那么,我们的第一个问题就是:判断一个字串是否是回文?

为了增加难度,改为单向链表结构的字串e.g. A->B->C->B->A 并且事先不知道字串长度

即 判断一条单向链表是不是“回文”

分析与C++代码

// 编程之美: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 最长回文字串

题目描述

给定一个字符串,求它的最长回文子串的长度

分析与C++代码

// 编程之美: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);
}

1.6 字符串的全排列

题目描述

输入一个字符串,打印出该字符串中字符的所有排列。

例如输入字符串abc,则输出由字符a、b、c 所能排列出来的所有字符串

abc、acb、bac、bca、cab 和 cba。


分析与C++代码

// 编程之美: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;
}
 
  

你可能感兴趣的:(C++,数据结构,面试)