目錄
Solution 3:STL的next_permutation求全排列
Solution 1:排序以後統計個數, 時間複雜度爲 O(nlogn)
Solution 2:選擇或者交換排序,n*O(k) = O(n*k)
Solution 5:最小堆與優先隊列,O(n+k*logn)
Solution 2:DP,時間複雜度 O(n),空間複雜度O(n)
劍指Offer-43:整數中1出現的次數(從1到n整數中1出現的次數)hard
Solution 1:Brute Force, 複雜度是0(n^2)
Solution 1:暴力法/冒泡排序,時間複雜度 O(n^2)。
Solution 2:右對齊兩個鏈表,時間複雜度O(m+n)
劍指Offer-62:孩子們的遊戲(圓圈中最後剩下的數)--約瑟夫環 hard
Solution 1:標準解法用list,模擬遊戲過程,時間複雜度O(mn)
劍指Offer-34:二叉樹中和爲某一值的路徑
DFS的模板題,注意不同參數傳遞形式的影響:普通 形參 , static,實參
劍指Offer-35:複雜鏈表的複製
題目描述
輸入一個複雜鏈表(每個節點中有節點值,以及兩個指針,一個指向下一個節點,另一個特殊指針指向任意一個節點)。
要求你編寫函數複製這個複雜鏈表
- 用map來存儲新舊鏈表的節點對應關係(空間換取時間(),新舊地址的映射。
- 用next指針域關聯新舊結點。
算法的流程如下:
1.遍歷一遍原始鏈表,複製結點N對應的N’,將其插入到結點N的後面,如下圖所示
2.確定每個隨機指針的指向,只需遍歷一遍鏈表即可確定每個結點的隨機指針的指向,得到如下圖結構
3.再次遍歷一遍,將原始鏈表和複製鏈表分開,奇數爲原始鏈表,偶數爲複製鏈表,得到如下圖型
劍指Offer-36:二叉搜索樹與雙向鏈表
題目描述
輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表。要求不能創建任何新的結點,只能調整樹中結點指針的指向。
二叉搜索樹的話就要考慮排序,排序的話就要中序遍歷,常規的兩種方法:遞歸和循環。
循環代碼如下:
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
TreeNode* Convert(TreeNode* pRootOfTree)
{
if (!pRootOfTree) return nullptr;
stack<TreeNode*> s;
TreeNode* res = nullptr;
TreeNode* pCurr = pRootOfTree;
TreeNode* pPreNode = nullptr;
s.push(pCurr);
pCurr =pCurr->left;
while(pCurr || !s.empty())
{
while (pCurr)
{
s.push(pCurr);
pCurr = pCurr->left;
}
pCurr = s.top();
s.pop();
if (!res)
res = pCurr;
if (pPreNode)
{
pPreNode->right = pCurr;
pCurr->left = pPreNode;
}
pPreNode = pCurr;
pCurr=pCurr->right;
}
return res;
}
};
遞歸的話相對簡單一些,將左右子樹變換爲鏈表後,將他們與根鏈接成鏈表
劍指Offer-37:序列化二叉樹 hard
題目描述
請實現兩個函數,分別用來序列化和反序列化二叉樹。這裏沒有規定序列化的方式
其實這道題約定的序列化沒有固定的格式, 只要你序列化後的結果, 再反序列化後與原樹相同即可,
因此我們可以隨意指定自己的格式,
比如空節點用$表示,或則#表示,
然後遍歷採用先序, 中序, 後序或者層次都可以,
我們的示例程序中採用空結點用#表示, 結點與結點用逗號,分隔
選擇了合適的遍歷算法, 那麼剩下的問題就是字符串序列和整數權值的相互轉換問題
- 序列化的關鍵, 其實就是將樹的權值(整數)轉換爲字符串序列, 可以採用ostringstream, sprintf和itoa
- 反序列化的關鍵, 則正好相反, 將字符串轉換爲整數, 可以使用istringstream, sscanf和atoi
C++ ostringstream、istringstream、stringstream 用法淺析
istringstream、ostringstream、stringstream 類介紹 .
C++的輸入輸出分爲三種:
(1)基於控制檯的I/O
(2)基於文件的I/O
(3)基於字符串的I/O
JeanCheng博客裏介紹的方法:
序列化(整數轉換爲字符串)
- IntToStringByitoa
- IntToStringBysprintf
- IntToStringBystringstream
- IntToStringByostringstream
反序列化(字符串轉換爲整數)
- StringToIntByatoi
- StringToIntBysscanf
- StringToIntBystringstream
- StringToIntByistringstream
#include <iostream>
#include <sstream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
using namespace std;
/*
* string to int
*/ #ifndef __GNUC__ /* GCC or G++ */
/************************* 序列化****************************/
// itoa不是標準C語言函數所以不能在所有的編譯器中使用(比如gcc)
string IntToStringByitoa(int num) {
char *pstr = itoa(num);
string str(pstr);
debug <<"int(" <<num <<") to "<<"str("<<str <<")" <<endl;
return str;
}
#endif
// 使用sprintf將任意的格式化信息輸出到char *中
string IntToStringBysprintf(int num) {
char pstr[81];
sprintf(pstr, "%d", num);
string str(pstr);
debug <<"int(" <<num <<") to "<<"str("<<str <<")" <<endl;
return str;
}
// 使用stringstream進行字符串的格式化
string IntToStringBystringstream(int num) {
string str;
stringstream ss;
ss <<num; ss >>str;
debug <<"int(" <<num <<") to "<<"str("<<str <<")" <<endl;
return str;
}
// 使用ostringstream將任意格式的信息轉換爲字符串(輸出)
string IntToStringByostringstream(int num) {
string str;
ostringstream ss;
ss <<num;
str = ss.str( );
debug <<"int(" <<num <<") to "<<"str("<<str <<")" <<endl;
return str;
}
/************************* 反序列化****************************/
// 使用atoi將char*轉換爲整數
int StringToIntByatoi(string str) {
int num = atoi(str.c_str());
debug <<"str(" <<str <<") to "<<"int("<<num <<")" <<endl;
return num;
}
// 使用sscanf將字符串轉換爲任意格式的信息
int StringToIntBysscanf(string str) {
int num;
sscanf(str.c_str(), "%d", &num);
debug <<"str(" <<str <<") to "<<"int("<<num <<")" <<endl;
return num;
}
// 使用stringstream進行字符串的格式化(輸入)
int StringToIntBystringstream(string str) {
int num;
stringstream ss;
ss <<str;
ss >>num;
debug <<"str(" <<str <<") to "<<"int("<<num <<")" <<endl;
return num;
}
// 使用istringstream將字符串轉換爲整數
int StringToIntByistringstream(string str) {
int num;
istringstream ss;
ss.str(str);
ss >>num;
debug <<"str(" <<str <<") to "<<"int("<<num <<")" <<endl;
return num;
}
序列化和反序列化的function直接用遞歸寫就好。
劍指Offer-38:字符串的排列
題目描述
輸入一個字符串,按字典序打印出該字符串中字符的所有排列
例如輸入字符串abc,
則打印出由字符a,b,c所能排列出來的所有字符串abc,acb,bac,bca,cab和cba。
結果請按字母順序輸出。
注意
輸入一個字符串,長度不超過9(可能有字符重複),字符只包括大小寫字母
Solution 1:Permutation的模板題
class Solution {
public:
vector<string> Permutation(string str) {
vector<string> ans;
int len = str.length();
string curr;
vector<int> visited(len);
dfs(str,0, visited,curr,ans);
return ans;
}
void dfs(const string &str,int d, vector<int> visited, string curr, set<string> &ans)
{
if (d==str.length() && curr.length() != 0)
{
ans.insert(curr);
return;
}
for (int i=0;i<str.length();i++)
{
if(visited[i]) continue;
if (i>0 && str[i] == str[i-1] && !visited[i-1]) continue;
curr += str[i];
visited[i] = 1;
dfs(str,d+1, visited,curr,ans);
curr = curr.substr(0,curr.length()-1);
visited[i] = 0;
}
}
};
Solution 2:基於交換的全排列(遞歸)
PermutationRecursion(str, 0);
sort(m_res.begin( ), m_res.end( ));
for(int i = begin; str[i] != '\0'; i++)
{
//debug <<str[i] <<str[begin] <<endl;
if(i == begin || str[i] != str[begin])
{
swap(str[i], str[begin]);
PermutationRecursion(str, begin + 1);
swap(str[i], str[begin]);
}
}
Solution 3:STL的next_permutation求全排列
用STL的next_permutation可以很方便的求一個容器的全排列
class Solution {
public:
vector<string> Permutation(string str)
{
vector<string> res;
if(str.empty( ) == true)
{
return res;
}
sort(str.begin( ), str.end( ));
do
{
res.push_back(str);
debug <<str <<endl;
}
while(next_permutation(str.begin( ), str.end( )));
return res;
}
};
劍指Offer-39:數組中出現次數超過一半的數字
Solution 1:排序以後統計個數, 時間複雜度爲 O(nlogn)
vector<int> numbers;
sort(numbers.begin( ), numbers.end( ));
Solution 2:基於快速排序的思想。
基於Partition函數的O(n)的查找第K大的數。
int Partition(vector<int> &numbers, int left, int right);
先用快排找到pivot的index,再繼續用二分搜索找到第K大的數。
Solution 3:陣地攻守
特點–它出現的次數比其他所有的數組出現的次數之和還要多
Solution 4:STL
使用STL的count函數統計某個值出現的次數
if(count(numbers.begin( ), numbers.end( ), numbers[i]) * 2 > numbers.size( ))
Solution 5:map
使用map來建立數字到出現次數的映射
map<string, int>中的 int 的初始值爲什麼是 0?
變種以及擴展
昨天看到了擴展問題,
如果有且只有一個的出現最多的那個數字出現的次數是數組長度的一半呢?又或者是一半減1?
我們還是繼續從我們之前的那道超過一半的數來入手,我們的”陣地攻守”的解法是每遇到2個不同的數,就刪除,剩下的就是那個出現次數超過一半的數字。
這個方法對於超過一半的情況可以成立,對於擴展問題就不再行得通了。
但是他們的本質是相同的,
對於一個長度爲2n的數組,如果有個數出現次數超過n,那麼至少有1組,連續2個數是重複的相應的,於是”陣地攻守”中每次不同的元素就刪除(同歸於盡)就可以找到那個元素
如果出現的次數爲n, 那麼至少有一組連續3個數是重複。每3個不重複的數刪除一次
如果是n-1, 就是一組至少有連續4個數重複。每4個不重複的數刪除一次
用兩個數組rnum,roccur,大小都爲2,記錄最近2個讀到數字的值和其出現的次數.
每讀到一個數,查詢其在rnum是否存在:
- 如果存在,將對應出現的次數加1
- 如果不存在,替換出現次數比較少的那個元素,並將其次數置爲1
當讀取結束時,最後的記錄裏,出現次數比較多的那個元素,就是所求的數字
劍指Offer-40:最小的K個數 hard
Solution 1:排序 O(nlogn)
sort(numbers.begin( ), numbers.end( ));
Solution 2:選擇或者交換排序,n*O(k) = O(n*k)
Solution 3:線性排序,計數排序
Solution 4:快速排序的分治劃分(中位數作爲樞軸)
Solution 5:最小堆與優先隊列,O(n+k*logn)
意思是針對整個數組序列建立最小堆,建堆所用時間爲O(n),然後取堆中的前k個數,即總的時間複雜度爲:O(n+k*logn)。
class greater_class
{
public:
bool operator()(int a, int b)
{
return a > b;
}
};
vector<int> LeastKNumbers_ByMinHeap(vector<int> numbers, int k)
{
vector<int> res;
if(numbers.size( ) == 0 || numbers.size( ) < k)
return res;
make_heap(numbers.begin( ), numbers.end( ), greater_class());
for(int i = 0; i < k; i++)
{
// 最小的元素在棧頂
res.push_back(numbers[0]);
/// 一下兩種操作均可以
// [1] -- 清除它, 然後重新排序堆
//numbers.erase(numbers.begin( ));
//sort_heap(numbers.begin( ), numbers.end( ));
// [2] -- 當然從堆出彈出這個棧頂元素
pop_heap(numbers.begin( ), numbers.end( ), greater_class( ));
// 彈出一個元素後,剩下的又重建了 heap,仍保持heap的性質
numbers.pop_back(); // vector 刪除末尾元素
}
return res;
}
可以用vector, push_heap, pop_heap來實現最大堆和最小堆,less 和greater分別對應的最大堆和最小堆。
1.priority_queue
priority_queue默認是最大堆,要用最小堆需要比較函數
greater<int>
priority_queue<int, vector<int>, less<int>> maxHeap; priority_queue<int, vector<int>, greater<int>> minHeap;
也可以自定義比較函數
struct cmp { bool operator()(const int &a, const int &b) { return a > b; } }; priority_queue<int, vector<int>, cmp> minHeap;
如果不是內置類型,也可以
struct cmp { bool operator()(const Node&a, const Node&b) { return a.val > b.val; } };
2. STL 堆操作
頭文件是#include <algorithm>
一般用到這四個:make_heap()、pop_heap()、push_heap()、sort_heap();
(1)make_heap()構造堆
void make_heap(first_pointer,end_pointer,compare_function);
默認比較函數是(<),即最大堆。
函數的作用是將[begin,end)內的元素處理成堆的結構(2)push_heap()添加元素到堆
void push_heap(first_pointer,end_pointer,compare_function);
新添加一個元素在末尾,然後重新調整堆序。該算法必須是在一個已經滿足堆序的條件下。
先在vector的末尾添加元素,再調用push_heap(3)pop_heap()從堆中移出元素
void pop_heap(first_pointer,end_pointer,compare_function);
把堆頂元素取出來,放到了數組或者是vector的末尾。
要取走,則可以使用底部容器(vector)提供的pop_back()函數。
先調用pop_heap再從vector中pop_back元素(4)sort_heap()對整個堆排序
排序之後的元素就不再是一個合法的堆了。void testHeap() { vector<int> data{ 3,1,2,7,5 }; //構造堆,最大堆 make_heap(data.begin(), data.end(), less<int>()); //pop堆頂元素,最大的元素 pop_heap(data.begin(), data.end(), less<int>()); cout << data.back() << endl;//輸出7 data.pop_back(); //往堆中添加元素 data.push_back(4); push_heap(data.begin(), data.end(), less<int>());//調整堆 //排序 sort_heap(data.begin(), data.end(), less<int>()); for (int x : data) cout << x << " "; cout << endl;//輸出 1,2,3,4,5 }
參考其他博主文章:
make_heap(), pop_heap(), push_heap()用法
Solution 6:最大堆O(log(k)*(n-k))
//最大堆
typedef multiset<int, std::greater<int> > intSet;
typedef multiset<int, std::greater<int> >::iterator setIterator;
void GetLeastNumbers_Solution2(const vector<int>& data, intSet& leastNumbers, int k)
{
leastNumbers.clear();
if(k < 1 || data.size() < k)
return;
vector<int>::const_iterator iter = data.begin();
for(; iter != data.end(); ++ iter)
{
if((leastNumbers.size()) < k)
leastNumbers.insert(*iter);
else
{
setIterator iterGreatest = leastNumbers.begin();
if(*iter < *(leastNumbers.begin()))
{
leastNumbers.erase(iterGreatest);
leastNumbers.insert(*iter);
}
}
}
}
因爲內存有限,基於堆或紅黑樹的解法更適合於海量數據的運算。
劍指Offer-41:數據流中的中位數
題目描述
如何得到一個數據流中的中位數?如果從數據流中讀出奇數個數值,那麼中位數就是所有數值排序之後位於中間的數值。如果從數據流中讀出偶數個數值,那麼中位數就是所有數值排序之後中間兩個數的平均值。
思路是維護兩個堆,最大堆和最小堆
class Solution {
public:
void Insert(int num)
{
if ( ((min.size()+max.size()) & 1) == 0)
{
if (max.size() >0 && num < max[0])
{
max.push_back(num);
push_heap(max.begin(),max.end(),less<int>());
num = max[0];
pop_heap(max.begin(),max.end(),less<int>());
max.pop_back();
}
min.push_back(num);
push_heap(min.begin(),min.end(),greater<int>());
}
else
{
if (min.size() >0 && num > min[0])
{
min.push_back(num);
push_heap(min.begin(),min.end(),greater<int>());
num = min[0];
pop_heap(min.begin(),min.end(),greater<int>());
min.pop_back();
}
max.push_back(num);
push_heap(max.begin(),max.end(),less<int>());
}
}
double GetMedian()
{
int size = min.size()+max.size();
if (size == 0) return 0;
if ( (size & 1) == 1)
return min[0];
else
return (static_cast<double>(min[0]) + max[0])/2;
}
vector<int> min;
vector<int> max;
};
劍指Offer-42:連續子數組的最大和
Solution 1:暴力方法
找出所有的子數組,然後求其和,取最大,兩個for,時間複雜度 O(n^2)
Solution 2:DP,時間複雜度 O(n),空間複雜度O(n)
解體思路:
如果用函數f(i)表示以第i個數字結尾的子數組的最大和,那麼我們需要求出max(f[0…n])。我們可以給出如下遞歸公式求f(i)
DP優化的話,空間複雜度可以將爲O(1)
劍指Offer-43:整數中1出現的次數(從1到n整數中1出現的次數)hard
這個題目要注意是1出現的次數,不是有1的數字個數。
所以思路的話,要從兩方面考慮:
- 最高位是1
- 除了最高位外的其他位置爲1
- 某一位是1,其他位是0-9任意的數字
- 剩下的數字用遞歸處理
劍指Offer-44:數字序列中某一位的數字
題目:
數字以0123456789101112131415…的格式序列化到一個字符序列中。在這個序列中,第5位(從0開始計數)是5,第13位是1,第19位是4,等等。請寫一個函數,求任意第n位對應的數字。
和 LeetCode 400. Nth Digit 一樣,只是LeetCode是從1開始。
class Solution {
public:
int findNthDigit(int n) {
if (n<0) return -1;
if (n<10) return n;
int digit(0),index(n);
long long countOfIntegers(0);
while (index > 0 && index> countOfIntegers)
{
index -= countOfIntegers;
digit++;
countOfIntegers = 9 * pow(10,digit-1)*digit;
}
int numBegin = index / digit;
int indexFromRight = digit - index % digit + 1;
int number = pow(10,digit-1) + numBegin - 1;
if (index % digit)
{
number++;
for (int i=1;i<indexFromRight;i++)
number /= 10;
}
return number%10;
}
};
劍指Offer-45:把數組排成最小的數
題目描述
輸入一個正整數數組,把數組裏所有數字拼接起來排成一個數,打印能拼接出的所有數字中最小的一個。
思路:
先將整數數組轉爲字符串數組,然後字符串數組進行排序,最後依次輸出字符串數組即可。這裏注意的是字符串的比較函數需要重新定義,不是比較a和b,而是比較ab與 ba。如果ab < ba,則a < b;如果ab > ba,則a > b;如果ab = ba,則a = b。比較函數的定義是本解決方案的關鍵。這道題其實就是希望我們能找到一個排序規則,根據這個規則排出來的數組能排成一個最小的數字。
下面的代碼針對的是string
template<class T>
string ToString(const T& t)
{
ostringstream oss; // 創建一個流
oss <<t; // 把值傳遞如流中
return oss.str( ); // 獲取轉換後的字符轉並將其寫入result
}
/// 比較函數
// 我們比較的不是兩個字符串本身的大小,而是他們拼接後的兩個數字的大小
static bool Compare(const string &left, const string &right)
{
string leftright = left + right;
string rightleft = right + left;
return leftright < rightleft;
}
用Cstring會複雜一些
int compare(const void* strNumber1, const void* strNumber2);
// int型整數用十進制表示最多隻有10位
const int g_MaxNumberLength = 10;
char* g_StrCombine1 = new char[g_MaxNumberLength * 2 + 1];
char* g_StrCombine2 = new char[g_MaxNumberLength * 2 + 1];
void PrintMinNumber(const int* numbers, int length)
{
if(numbers == nullptr || length <= 0)
return;
char** strNumbers = (char**)(new int[length]);
for(int i = 0; i < length; ++i)
{
strNumbers[i] = new char[g_MaxNumberLength + 1];
sprintf(strNumbers[i], "%d", numbers[i]);
}
qsort(strNumbers, length, sizeof(char*), compare);
for(int i = 0; i < length; ++i)
printf("%s", strNumbers[i]);
printf("\n");
for(int i = 0; i < length; ++i)
delete[] strNumbers[i];
delete[] strNumbers;
}
// 如果[strNumber1][strNumber2] > [strNumber2][strNumber1], 返回值大於0
// 如果[strNumber1][strNumber2] = [strNumber2][strNumber1], 返回值等於0
// 如果[strNumber1][strNumber2] < [strNumber2][strNumber1], 返回值小於0
//compar參數指向一個比較兩個元素的函數。比較函數的原型應該像下面這樣。
//注意兩個形參必須是const void *型,同時在調用compar 函數
//(compar實質爲函數指針,這裏稱它所指向的函數也爲compar)時,
//傳入的實參也必須轉換成const void *型。在compar函數內部會將const void *型轉換成實際類型
int compare(const void* strNumber1, const void* strNumber2)
{
// [strNumber1][strNumber2]
strcpy(g_StrCombine1, *(const char**)strNumber1);
strcat(g_StrCombine1, *(const char**)strNumber2);
// [strNumber2][strNumber1]
strcpy(g_StrCombine2, *(const char**)strNumber2);
strcat(g_StrCombine2, *(const char**)strNumber1);
return strcmp(g_StrCombine1, g_StrCombine2);
}
證明:
- 自反性
- 對稱性
- 傳遞性
劍指Offer-46:把數字翻譯成字符串
題目:
給定一個數字,我們按照如下規則把它翻譯爲字符串:0翻譯成“a”,1翻譯成“b”,……,11翻譯成“1”,……,25翻譯成“z”。一個數字可能有多個翻譯。例如:12258有5種不同的翻譯,分別是“bccfi”、“bwfi”、“bczi”、“mcfi”和“mzi”。請編程實現一個函數,用來計算一個數字有多少種不同的翻譯方法。
用遞歸自頂向下分析,用動態規劃自低向上求解to_string
- DP:用一個遞歸式來表示:定義f(i)表示從第i位數字開始的不同翻譯的數目,那麼f(i) = f(i+1) + g(i,i+1)*f(i+2);當第i位和第i+1位兩位數字拼接起來的數字在10~25的範圍內時,函數g(i,i+1)的值爲1,否則爲0。
- 遞歸的話可以從前往後,其實一樣的。
劍指Offer-47:禮物的最大價值
題目:
在一個m*n的棋盤的每一格都放有一個禮物,每個禮物都有一定的價值(價值大於0)。你可以從棋盤的左上角開始拿格子裏的禮物,並每次向右或者向下移動一格,直到到達棋盤的右下角。給定一個棋盤及其上面的禮物,請計算你最多能拿多少價值的禮物?
這個題目是DP的經典題目, 時間複雜度 O(m*n),空間複雜度 O(m*n)。
這個題目需要注意的是輸入參數是 const int* values,建立DP表是二維數組:
int** maxValues = new int*[rows];
...
// 在堆上新建的數組最後要delete掉
for (int i=0;i<rows;i++)
delete[] maxValues[i];
delete[] maxValues;
劍指Offer-48:最長不含重複字符的子字符串
題目:
請從字符串中找出一個最長的不包含重複字符串的子字符串,計算該最長子字符串的長度。假設字符串中只包含‘a’~‘z’的字符。
例如,在字符串“arabcacfr”中,最長的不含重複字符的子字符串是“acfr”,長度爲4。
Solution 1:Brute Force, 複雜度是0(n^2)
Solution 2:DP
使用一個輔助數組,26個字母的長度,初值-1,裏面填充字幕第一次出現的index。
定義問題:
- 如果第i個字符之前沒有出現過,那麼f(i) = f(i-1)+1;
- 如果第i個字符之前出現過,記第i個字符和它上出現在字符串中的位置的距離,記爲d。如果d>f(i-1),那麼仍然有f(i) = f(i-1)+1;如果d<f(i-1),則爲d。
劍指Offer-49:醜數
題目描述
把只包含因子2、3和5的數稱作醜數(Ugly Number)。
例如6、8都是醜數,但14不是,因爲它包含因子7。
習慣上我們把1當做是第一個醜數。求按從小到大的順序的第N個醜數。
Solution 1:Brute Force
Solution 2:三個指針,時間複雜度O(n)
新的下一個醜數,一定是之前的醜數乘以2,3,5得到的,可以記錄三個指針,乘積取最小,就是下一個醜數
劍指Offer-50:第一個只出現一次的字符位置
題目描述
在一個字符串(1<=字符串長度<=10000,全部由字母組成)中找到第一個只出現一次的字符的位置。若爲空串,返回-1。位置索引從0開始
Hash Table 的代表題目,兩次遍歷,第一次計數在hashtable裏,第二次輸出結果。
計數法的話,用哈希表,map和unordered_map都可以。這裏還可以簡化,字符char長度爲8,256長度的array就可以滿足要求。
ASCII對應下標。使用的時候強制轉換就行。
hashTable[*(pHashKey++)] ++;
注意這個代碼,先運行讀取值的操作,再++;
相關題目:
1. 輸入兩個字符串,從第一個字符串中刪除在第二個字符串中出現的所有字符。
2. 刪除字符串中所有重複出現的字符。
3. 判斷兩個單詞是不是變位詞(字母和次數都一樣)
4. 字符流中第一個只出現一次的字符
這個題目輔助數組256size,裏面存字符出現的先後次序,沒出現-1,出現一次的話就是index,多次出現-2
找第一個只出現一次的字符,需要最後再遍歷256的hashtable,找到正值的最小的index,對應的字符就是結果。
劍指Offer-51:數組中的逆序對 hard
題目描述
在數組中的兩個數字,如果前面一個數字大於後面的數字,則這兩個數字組成一個逆序對。
輸入一個數組,求出這個數組中的逆序對的總數
Solution 1:暴力法/冒泡排序,時間複雜度 O(n^2)。
Solution 2:歸併排序 hard
歸併排序的思想是,先把一個排序問題分成兩個子問題,再merge兩個子問題。
劍指Offer-52:兩個鏈表的第一個公共結點
Solution 1:暴力法,時間複雜度O(mn)
Solution 2:右對齊兩個鏈表,時間複雜度O(m+n)
-
長鏈表先走,實現右對齊
-
將兩個鏈表拼接起來實現右對齊
-
棧的後進先出實現右對齊
- 使用輔助空間unordered_map,兩次遍歷
利用unordered_map,這個模板類是基於hash表實現的,速度與map、hashmap相比較快
unordered_map<ListNode*, bool> umap; umap.insert(make_pair(left, 1 )); if (umap.count(right)>0) { ... }
map
- 優點:
- 有序性,這是map結構最大的優點,其元素的有序性在很多應用中都會簡化很多的操作
- 紅黑樹,內部實現一個紅黑書使得map的很多操作在lgn的時間複雜度下就可以實現,因此效率非常的高
- 缺點:
- 空間佔用率高,因爲map內部實現了紅黑樹,雖然提高了運行效率,但是因爲每一個節點都需要額外保存父節點,孩子節點以及紅/黑性質,使得每一個節點都佔用大量的空間
- 適用處,對於那些有順序要求的問題,用map會更高效一些
- unordered_map
- 優點:
- 因爲內部實現了哈希表,因此其查找速度非常的快
- 缺點:
- 哈希表的建立比較耗費時間
- 適用處,對於查找問題,unordered_map會更加高效一些,因此遇到查找問題,常會考慮一下用unordered_map
劍指Offer-53:數字在排序數組中出現的次數
Solution 1:暴力方法,時間複雜度 O(n)。
Solution 2:改進的二分查找
返回第一個和最後一個的位置。
如果 low >high 返回-1;這個可以檢查值是不是存在。first和last有一個是-1就返回0。
Solution 3:multiset
multiset裏面就有現成的count方法可以用。
multiset<int> mData;
for(int i = 0; i < data.size( ); i++)
{
mData.insert(data[i]);
}
return mData.count(k);
Solution 4:stl的方法:
class Solution {
public:
int GetNumberOfK(vector<int> data ,int k)
{
int upper = upper_bound(data.begin(),data.end(),k);
int low = lower_bound(data.begin(),data.end(),k);
return upper - low;
}
};
二分查找的其他題目:
0~n-1中缺失的數字。
數組中數值和下標相等的元素。
劍指Offer-54:二叉搜索樹的第K大結點
這個題目的本質是二叉樹的遍歷。
Solutoin 1:遞歸 hard
重載函數,參數k可以修改,中序遍歷,k爲1的時候輸出。
Solutoin 2:循環
劍指Offer-55:二叉樹的深度
Solutoin 1:遞歸方法
Solutoin 2:BFS
- 標準寫法
- 建立兩個queue,存父級和子級的note, 父級空了以後swap,cout++
- 用vector,通過cur和end來決定什麼時候更新cout
- 用vector,不過父級結束的時候添加NULL
- 用vector,定義parantSize和 childSize
看一棵樹是不是平衡二叉樹
- 用上面的函數看TreeDepth, 不好,因爲要重複很多次。想要不重複,其實只要記錄每個結點的深度就行。
- 最簡單的記錄方式就是多加一個輸入參數。一邊遍歷,一邊記錄深度。
劍指Offer-56:數組中數字出現的次數 hard
一個整型數組裏除了兩個數字之外,其他的數字都出現了兩次。請寫程序找出這兩個只出現一次的數字。要求時間複雜度O(n),空間複雜度O(1)。
這個題目思路是用異或。
變種1:兩個數出現一次,其餘兩次
所有數字異或,找第一個爲1的bit,分兩組,分別異或得到兩個數字。
變種2:一個數出現一次,其餘三次
建立輔助數組,32長度,遍歷所有數字,記錄每個bit 的長度,
然後,不爲3的就是唯一的一個數字。
劍指Offer-57:和爲S的連續正數序列
變種1:和爲s的兩個數字
這個快速排序的思想,兩邊向中間移動。
需要注意的是,兩個int相加,爲了防止值溢出,和的變量數據類型應該定義爲long long。
變種2:和爲S的連續正數序列
Solution 1: 直接使用公式:
從n=1開始遍歷,計算,直到
Solution 2: 雙指針
定義big,small兩個index,初值爲1和2,根據sum來更新big和small。
需要注意的是,currSum > sum 的時候 small++ 還要加上判定條件 small < (1+sum)/2。
劍指Offer-58:翻轉單詞順序列
題目描述
牛客最近來了一個新員工Fish,每天早晨總是會拿着一本英文雜誌,寫些句子在本子上。同事Cat對Fish寫的內容頗感興趣,有一天他向Fish借來翻看,但卻讀不懂它的意思。例如,“student. a am I”。後來才意識到,這傢伙原來把句子單詞的順序翻轉了,正確的句子應該是“I am a student.”。Cat對一一的翻轉這些單詞順序可不在行,你能幫助他麼?
- 先翻轉所有字符,再逐個單詞翻轉
需要考慮的特殊情況,開頭和結尾右空格。
-
使用STL的函數庫來實現
size_t left = 0;
size_t right = 0;
while(right != string::npos)
{
left = str.find_first_not_of(' ', left); // 第一個非空格字符是單詞的起始位置
right = str.find_first_of(' ', left); // 第一個空格位置標識了單詞的結束
// 處理末尾是空格(原字符串開頭是空格的情況)
// 當最後全是空格的時候,此時可以結束循環
if (left == string::npos)
{
break;
}
// 如果查找不到空格, 到了字符串的末尾
// 此時[degin, str.size( )]是末尾的字符串
if (right == string::npos) {
reverse(str.begin( ) + left, str.end( ));
}
else // 否則[left, end]是一個單詞
{
reverse(str.begin( ) + left, str.begin() + right);
left = right + 1;
}
}
主要是函數find_first_not_of和find_first_of
- 用棧的後進先出特性實現翻轉
定義兩個stack來實現,這個寫法不好,先把字符壓入一個棧,然後通過top讀出第一個的頂字符壓入第二個棧,直到空格,
這個時候再把第二個棧的字符壓入result就行。囉嗦麻煩的寫法。
- 從後向前重新組裝字符串
其實就是用的字符串函數,
找最後一個空格的函數:str.rfind, 找子串的函數: str.substr(),
class Solution
{
public:
string ReverseSentence(string str)
{
string result;
while (str.rfind(" ") != -1)
{
unsigned long position = str.rfind(" ");
string temp = str.substr(position + 1);
//debug <<temp <<endl;
result = result + temp + ' ';
str = str.substr(0, position);
debug <<"str = " <<str <<endl;
}
//debug <<str <<endl;
result = result + str;
return result;
}
};
類似題目:左旋轉字符串
其實字符串複製一遍並在後面就可以,不過何老師用了三次reverse,也可以啦。。。
- 直接找到旋轉後的對應關係
(n+i)%str.size(),直接對應賦值就好。
n = n%str.size();
str.substr(n,str.size());
- 通過翻轉直線循環移位
reverse(0,i-1);
reverse(i,n-1);
reverse(1,n-1);
劍指Offer-59:隊列的最大值
棧的話,求最大值,開闢一個max的新棧存max就OK了。
變種題目: 快速得到棧、隊列的最大值
用array實現棧,再用棧實現隊列
題目一:滑動窗口的最大值
LeetCode 239. Sliding Window Maximum
回到這個題目,實現的思路是利用一個deque來存最大值。
還是把滑動窗口當成是隊列來處理,其實是最大值隊列的改進策略, 思路基本類似, 但是隊列中存儲的是最大值的下標, 爲了得到滑動窗口的最大值,隊列序可以從兩端刪除元素,因此使用雙端隊
原則:對新來的元素k,將其與雙端隊列中的元素相比較
前面比k小的,直接移出隊列(因爲不再可能成爲後面滑動窗口的最大值了!),
前面比k大的X,比較兩者下標,判斷X是否已不在窗口之內,不在了,直接移出隊列
其他方法:
-
最大堆的方法, 定義了個pair<int,int> Pair; 最大堆用的是priority_queue,Q.push(Pair(num[i],i));
- 兩個棧的方法
題目二:隊列的最大值
- 同上一題相同,我們要尋找隊列的最大值,相當與將滑動窗口設置爲整個隊列。
- 這裏需要使用兩個隊列,一個隊列用來保存入隊的數據,一個隊列用來保存隊列的當前最大值。
- 同時需要注意出隊操作,數據隊列出隊的同時需要判斷其索引是否和當前最大值隊列首部索引相同,如果相同則同時也將最大值隊列頭部出隊。
劍指Offer-60:n個骰子的點數
題目:
把n個骰子仍在地上,所有骰子朝上一面的點數之和爲s。輸入n,打印出s的所有可能的值出現的概率。
思路是,最終要求出和的出現的次數,除以6^n(全排列的全部可能性),即爲概率。
n個骰子的和 範圍 是 n~6n。
Solution 1:遞歸,時間複雜度O(6^n)
何老師書中用的方法,時間複雜度太高,不推薦。
Solution 2:DP,時間複雜度O(n^2)
假設f(m,n)表示投第m個骰子時,點數之和n出現的次數,投第m個骰子時的點數之和只與投第m-1個骰子時有關。
遞歸方程:f(m,n)=f(m-1,n-1)+f(m-1,n-2)+f(m-1,n-3)+f(m-1,n-4)+f(m-1,n-5)+f(m-1,n-6),表示本輪點數和爲n出現次數等於上一輪點數和爲n-1,n-2,n-3,n-4,n-5,n-6出現的次數之和。
初始條件:第一輪的f(1),f(2),f(3),f(4),f(5),f(6)均等於1.
DP數組的話,可以使用兩個array,切換的時候用flag,每次更新 flag = 1-flag;
或者每次更新從後往前更新,這樣一個array就行了。
最後求概率的時候要轉換數據類型爲double,不然算出來還是整型數。
劍指Offer-61:撲克牌順子
題目描述
LL今天心情特別好,因爲他去買了一副撲克牌,發現裏面居然有2個大王,2個小王(一副牌原本是54張^_^)…
他隨機從中抽出了5張牌,想測測自己的手氣,看看能不能抽到順子,如果抽到的話,他決定去買體育彩票
,嘿嘿!!“紅心A,黑桃3,小王,大王,方片5”,
“Oh My God!”不是順子…..LL不高興了,他想了想,決定大\小王可以看成任何數字,並且A看作1,J爲11,Q爲12,K爲13。
上面的5張牌就可以變成“1,2,3,4,5”(大小王分別看作2和4),“So Lucky!”。LL決定去買體育彩票啦。 現在,要求你使用這幅牌模擬上面的過程,然後告訴我們LL的運氣如何。爲了方便起見,你可以認爲大小王是0。
Solution 1
先排序,再計數0的個數,如果空缺的總數小於或者等於0的個數,那麼這個數組就是連續的;反之則不連續。
Solution 2
條件: 5張牌,順子,除0之外不能重複
結論: 非0元素的極差(最大值最小值的差)不超過4, 非0元素不重複
定義了宏來進行運算:
定了個 int flag = 0;
#define BIT_GET(number, pos) ((number) >> (pos) & 1) /// 用宏得到某數的某位
#define BIT_SET(number, pos) ((number) |= 1 << (pos)) /// 把某位置1
#define BIT_CLR(number, pos) ((number) &= ~(1 << (pos))) /// 把某位清0
#define BIT_CPL(number, pos) ((number) ^= 1 << (pos)) /// 把number的POS位取反
其實就是用來檢查數字有沒有重複。
定義了int flag = 0; 然後來設置bit來檢查。
劍指Offer-62:孩子們的遊戲(圓圈中最後剩下的數)--約瑟夫環 hard
題目描述
每年六一兒童節,NowCoder都會準備一些小禮物去看望孤兒院的小朋友,今年亦是如此。
HF作爲NowCoder的資深元老,自然也準備了一些小遊戲。
其中,有個遊戲是這樣的:
首先,讓小朋友們圍成一個大圈。然後,他隨機指定一個數m,讓編號爲0的小朋友開始報數。
每次喊到m的那個小朋友要出列唱首歌,然後可以在禮品箱中任意的挑選禮物,並且不再回到圈中,從他的下一個小朋友開始,繼續0…m-1報數….這樣下去….直到剩下最後一個小朋友,可以不用表演,
並且拿到NowCoder名貴的“名偵探柯南”典藏版(名額有限哦!!^_^)。
請你試着想下,哪個小朋友會得到這份禮品呢?
Solution 1:標準解法用list,模擬遊戲過程,時間複雜度O(mn)
Solution 2:遞歸公式
推導過程:
劍指Offer-63:股票的最大利潤 hard
題目1:一次買入賣出
LeetCode-121:Best Time to Buy and Sell Stock
思路:保存最小值和最大利潤遍歷就行。
題目2:多次買入賣出
LeetCode-122:Best Time to Buy and Sell Stock II
思路:
- 可以對股票進行多次的買入和賣出;
- 則如果第二天的價格高於第一天的價格就可以以第二天的價格賣出,則賣出後立即再次買入;
- 如果第二天的價格低於第一天的價格,那麼就在第一天結束前就賣出,相當於不盈利。
- 所以通過逐個相鄰兩數進行比較即可,如果後面的大,則記爲利潤。
題目3:最多兩次買入賣出
LeetCode-123:Best Time to Buy and Sell Stock III
我的想法是和上一個先一樣, 不過整合連續正的序列,然後挑最大的兩個就好。
博客裏講了兩種其他思路,
1. 分段考慮,時間複雜度O(n)
- 以i爲分界線,前i天的最大和i天后面的最大,分兩段進行每次的一個交易;
- 兩段的最大和,則爲最大的利潤;
前i天的最大利潤和i天后的最大利潤和,maxProfitDuringFormmerDays 和 maxProfitDuringLatterDays 兩個數組然後求和取最大值。
兩個數組裏分別存了從開始和結束開始的最大利潤。就是個最大利潤問題。
2. 動態規劃,時間複雜度O(n)
- Buy1[i]表示前i天做第一筆交易買入股票後剩下的最多的錢;
- Sell1[i]表示前i天做第一筆交易賣出股票後剩下的最多的錢;
- Buy2[i]表示前i天做第二筆交易買入股票後剩下的最多的錢;
- Sell2[i]表示前i天做第二筆交易賣出股票後剩下的最多的錢;
那麼:
Sell2[i]=max{Sell2[i-1],Buy2[i-1]+prices[i]}
Buy2[i]=max{Buy2[i-1],Sell[i-1]-prices[i]}
Sell1[i]=max{Sell[i-1],Buy1[i-1]+prices[i]}
Buy1[i]=max{Buy[i-1],-prices[i]}
劍指Offer-64:求1+2+3+...+n hard
題目描述
求1+2+3+…+n,
要求不能使用乘除法、for、while、if、else、switch、case等關鍵字及條件判斷語句(A?B:C)。
Solution 1: 位運算計算
我們其實知道,等差數列求和的問題
1+2+3+...+n=n(n+1)2
但是是一個乘除的運算, 本題限制使用乘除,因此我們得另尋它法
知道a*b運算在計算機內部其實是通過移位和加法來完成的。
int Multi(int a, int b)
{
int res = 0;
while(a != 0)
{
if((a & 1) != 0)
{
res += b;
}
a >>= 1;
b <<= 1;
}
return res;
}
Solution 1.5:優化,遞歸+短路
class Solution
{
public:
int res;
int Sum_Solution(int n)
{
res = 0;
return (MultiRecursion(n, n + 1) >> 1);
}
int MultiRecursion(int a, int b)
{
a && MultiRecursion(a >> 1, b << 1); // 遞歸的進行運算
(a & 1) && (res += b); // 短路
return res;
}
};
Solution 2:利用構造函數求解
定義private變量N,Sum,構造器N每次++,Sum再加N。
Solution 3:利用虛函數求解
- 我們同樣可以圍繞遞歸做文章, 既然不能在一個函數中通過分支判斷是不是終止遞歸
- 我們可以定義兩個函數, 一個函數充當遞歸函數的角色,另一個函數處理終止遞歸的情況,
- 而我們需要在兩個函數中二選一, 從二選一我們自然的想到了布爾變量, 比如值爲true的時候調用第一個, 值爲false的時候, 調用第二個函數,
- 那麼我們怎麼把一個值n轉換爲布爾類型呢, 如果對n連續做兩次反運算, 即!!n, 那麼非零的n轉換爲true, 0轉換爲false
class base;
base *parray[2];
class base
{
public:
virtual unsigned int sum(unsigned int n)
{
// 遞歸的終止
return 0;
}
};
class derive :public base
{
public: virtual unsigned int sum(unsigned int n)
{
// !!n兩次非運算, 從而將n轉換爲bool類型, 來選擇函數
// sum(n) = sum(n - 1) + n 則是遞歸公式
return parray[!!n]->sum(n - 1) + n;
}
};
class Solution
{
public: int Sum_Solution(int n)
{
base a;
derive b;
parray[0] = &a;
parray[1] = &b;
return parray[1]->sum(n);
}
};
int main()
{
Solution s;
cout<<s.Sum_Solution(10);
return 0;
}
Solution 4:利用函數指針求解
typedef int (*func)(int);
// 遞歸的終止函數
int Teminator(int n)
{
return 0;
}
// 遞歸函數, 選擇函數進行遞歸
int Recursion(int n)
{
static func pf[2] = { Teminator, Recursion, };
return n + pf[!!n](n - 1);
}
class Solution
{
public :
int Sum_Solution(int n)
{
return Recursion(n);
}
};
Solution 5:利用模板類型求解
利用編譯器幫助完成類似於遞歸的運算
#include <iostream>
using namespace std;
template < unsigned int n>
struct Sum
{
enum Value
{
N = Sum< n - 1 >::N + n
};
};
template <>
struct Sum<1>
{
enum Value { N = 1 };
};
int main( )
{
cout <<Sum<100>::N <<endl;
return 0;
}
Sum<100>::N就是1+2++…+100的結果, 當編譯器看到Sum<100>::N時, 就會爲模板類Sum以參數100生成對象, 而由於這個類的定義是遞歸的, 因此編譯器會一直遞歸的操作, 直至參數爲1, 遞歸結束。
缺陷
- 由於這個過程是由編譯器在編譯階段完成的, 因此要求n必須是一個編譯期間就能確定的常量, 不能動態定義
- 編譯器對遞歸編譯代碼的遞歸深度是由限制的, 因此要去n不能太大。
Solution 6:數組指針+求和公式
我沒看懂這個方法,大家誰可以給我解釋一下嗎?
#include <stdio.h>
#include <stdint.h>
int rich(int n)
{
return ( (int)( &((uint8_t (*) [n])0)[1+n][0]) ) >> 1;
}
int main()
{
printf("%d\n", rich(10));
return 0;
}
Solution 7:邏輯與的短路性質方法
class Solution
{
public:
int Sum_Solution(int n)
{
int sum = n;
bool ans = (n>0)&&((sum+=Sum_Solution(n-1))>0);
return sum;
}
};
劍指Offer-65:不用加減乘除做加法 hard
Solution 1:C/C++內斂彙編
通過內斂彙編我們直接使用add指令來進行相加操作
C/C++內斂彙編使用AT&T彙編語法
在INTEL語法中,第一個表示目的操作數,第二個表示源操作數,賦值方向從右向左。
AT&T語法第一個爲源操作數,第二個爲目的操作數,方向從左到右,合乎自然。
class Solution
{
public:
int Add(int left, int right)
{
__asm__ __volatile__
(
//"lock;\n"
"addl %1,%0;\n" /* 相當於 add b, a結果存儲在a中*/
: "=m"(left)
: "r"(right), "m"(left)
// :
);
return left;
}
};
Solution 2:位運算模擬加法
class Solution {
public:
int Add(int num1, int num2)
{
int sum,carry;
do
{
sum = num1 ^ num2;
carry = (num1 & num2) <<1;
num1 = sum;
num2 = carry;
}
while (num2 != 0);
return num1;
}
};
劍指Offer-66:構建乘積數組
題目:
給定一個數組A[0,1,…,n-1],請構建一個數組B[0,1,…,n-1],其中B中的元素B[i]=A[0]A[1]…A[i-1]*A[i+1]…*A[n-1]。不能使用除法。
思路:
不能使用除法,但是可以用乘法。
對於B[i]來說,他需要的是 A[0~i-1] 和 A[i+1~n-1] 。
求 A[0~i-1] 和 A[i+1~n-1] 的話,需要定義一個二維的舉證,按照長度+1不斷循環更新就OK。
class Solution {
public:
vector<int> multiply(const vector<int>& A) {
vector<int> B(A);
int length = A.size();
if (length <= 1) {
vector<int> B;
return B;
}
else{
B[0] = 1;
for (int i = 1; i < length; ++i) {
B[i] = B[i - 1] * A[i - 1];
}
int temp = 1;
for(int i = length - 2; i >= 0; --i) {
temp *= A[i + 1];
B[i] *= temp;
}
}
return B;
}
};
面試案例:
1. c++中成員變量的初始化順序
c++中成員變量的初始化順序只與他們在類中聲明的順序有關,而與在初始化列表中的順序無關。
2. StrToInt
題目本身不難,但是如果考慮空指針,空字符串,正負號,溢出,只有一個正負號就難了。。。
給int類型賦值的話,0X7FFFFFFF代表最大值,0X80000000代表最小值
INT_MAX 代表最大值, INT_MIN 代表最小值 、#include<limits.h>
C/C++中各種類型int、long、double、char表示範圍(最大最小值)
定義的num 類型是 long long;最後返回的時候再轉化爲 int。
作者的源碼寫的很好。
#include <cstdio>
long long StrToIntCore(const char* str, bool minus);
enum Status {kValid = 0, kInvalid};
int g_nStatus = kValid;
int StrToInt(const char* str)
{
g_nStatus = kInvalid;
long long num = 0;
if(str != nullptr && *str != '\0')
{
bool minus = false;
if(*str == '+')
str ++;
else if(*str == '-')
{
str ++;
minus = true;
}
if(*str != '\0')
num = StrToIntCore(str, minus);
}
return (int)num;
}
long long StrToIntCore(const char* digit, bool minus)
{
long long num = 0;
while(*digit != '\0')
{
if(*digit >= '0' && *digit <= '9')
{
int flag = minus ? -1 : 1;
num = num * 10 + flag * (*digit - '0');
if((!minus && num > 0x7FFFFFFF)
|| (minus && num < (signed int)0x80000000))
{
num = 0;
break;
}
digit++;
}
else
{
num = 0;
break;
}
}
if(*digit == '\0')
g_nStatus = kValid;
return num;
}
3. 樹中兩個結點的最低公共祖先。
LeetCode 236 Lowest Common Ancestor of a Binary Tree
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == p || root == q || !root)
return root;
TreeNode* lr = lowestCommonAncestor(root->left,p,q);
TreeNode* rr = lowestCommonAncestor(root->right,p,q);
if (!lr && !rr) return nullptr;
if (!lr ) return rr;
if (!rr ) return lr;
return root;
}
};