文章目录
- 面试题36:二叉搜索树与双向链表
- 面试题37:序列化二叉树
- 面试题38:字符串的排列
- 面试题39:数组中出现次数超过一半的数字
- 面试题40:最小的k个数
- 面试题41:数据流中的中位数
- 面试题42:连续子数组的最大和
- 面试题43:1~*n*整数中1出现的次数
- 面试题44:数字序列中某一位数字
- 面试题45:把数组排成最小的数字
- 面试题46:把数字翻译成字符串
- 面试题47:礼物的最大价值
- 面试题48:最长不含重复字符的子字符串
- 面试题49:丑数
- 面试题50:第一个只出现一次的字符
- 面试题51:数组中的逆序对
- 面试题52:两个链表的第一公共节点
- 面试题53:在排序数组中查找数字
- 面试题54:二叉搜索树的第k个大节点
- 面试题55:二叉树的深度
- 面试题56:数组中数字出现的次数
- 面试题57:和为s的数字
- 面试题58:翻转字符串
- 面试题59:队列的最大值
- 面试题60:n个骰子的点数
面试题36:二叉搜索树与双向链表
题目:输入一棵二叉搜索树,将该二叉搜索树转成一个排序的双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
分析:二叉搜索树的中序遍历就是一个从小到大的排序遍历。将树看成,左子树、根、右子树三部分,将左子树的最大值与根相连,再与右子树的最小值相连。
struct BinaryTreeNode
{
int m_nValue;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
};
BinaryTreeNode* convert_BST_to_interList(BinaryTreeNode* pRoot)
{
BinaryTreeNode* pLastNodeInList=nullptr;
//让pLastNodeInList值在函数结束时能发生变化,故给pLastNodeInList取地址
convert(pRoot, &pLastNodeInList);
BinaryTreeNode* pHead = pLastNodeInList;
//pHead目前在双向链表的尾部,应返回头节点
while(nullptr!=pHead&&nullptr!=pHead->m_pLeft)
pHead=pHead->m_pLeft;
return pHead;
}
void convert(BinaryTreeNode* pNode, BinaryTreeNode** pLastNodeInList)
{
if(nullptr==pNode)
return;
BinaryTreeNode* pCurrentNode = pNode;
if(nullptr!=pCurrentNode->m_pLeft)
convert(pCurrentNode, pLastNodeInList);
pCurrentNode->m_pLeft = *pLastNodeInList;
if(nullptr!=*pLastNodeInList)
pLastNodeInList->m_pRight = *pCurrentNode;
*pCurrentNode = pCurrentNode;
if(nullptr!=pCurrentNode->m_pRight)
convert(pCurrentNode->m_pRight, pLastNodeInList);
}
面试题37:序列化二叉树
题目:请实现两个函数,分别用来序列化和反序列化二叉树。
分析:通过面试题7“重建二叉树”,我们知道可以从前序遍历序列和中序遍历序列中构造一棵二叉树。思路缺点:一是要求该二叉树中不能有数值重复的节点;而是只有当序列汇总所有数据都读出来以后才能开始反序列化。如果二叉树的序列化是从根节点开始的,那么我们就可以用前序遍历,碰到nullptr指针时,将这些nullptr指针序列化成一个特殊字符。节点之间用’,'隔开。
void Serialize(BinaryTreeNode* pRoot, ostream& stream)
{
if(nullptr==pRoot)
{
stream << "$,";
return;
}
stream << pRoot->m_nValue << ',';
Serialize(pRoot->m_pLeft, stream);
Serialize(pRoot->m_pRight, stream);
}
void Deserialize(BinaryTreeNode** pRoot, istream& stream)
{
int number;
if(Readstream(stream, &number))
{
*pRoot=new BinaryTreeNode();
(*pRoot)->m_nValue = number;
(*pRoot)->m_pLeft = nullptr;
(*pRoot)->m_pRight = nullptr;
Deserialize(&((*pRoot)->m_pLeft,stream);
Deserialize(&((*pRoot)->m_pRight, stream);
}
}
bool Readstream(istream& stream, int *number)
{
string buffer;
if(getline(stream, buffer, ','))
{
if(buffer!="$")
{
*number = atoi(buffer.c_str());
return true;
}
return false;
}
return false;
}
面试题38:字符串的排列
题目:输入一个字符串,打印出该字符串中字符的所有排列。例如:输入abc;输出abc, acb, bac, bca, cab, cba。
分析:把一个字符看成由两部分组成:第一部分是它的第一个字符;第二部分是后面的所有字符。排列可分为两步:第一步求所有可能出现在第一个位置的字符,即把第一个字符和后面所以的字符交换。
void Permutation(char *pStr)
{
if(nullptr==pStr)
return;
Permutation1(pStr, pStr);
}
void Permutation1(char *pStr, char *pBegin)
{
if('\0'==*pBegin)
{
printf("%s\n",pStr);
}
else
{
for(char *pCh = pBegin; *pCh != '\0'; ++pCh)
{
char tmp = *pCh;
*pCh = *pBegin;
*pBegin = tmp;
Permutation1(pStr, pBegin+1);
tmp = *pCh;
*pCh = *pBegin;
*pBegin = tmp;
}
}
}
面试题39:数组中出现次数超过一半的数字
题目:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数。
分析:首先是想到排序。排序的时间复杂度为O(nlogn)。但通常不是面试官最满意的算法。
解法一:基于partition函数的时间复杂度为O(n)的算法
int more_than_half_num(int *numbers, int length)
{
if(check_invalid_array(numbers, length))
return 0;
int middle = length >> 1;
int start = 0;
int end = length - 1;
int index = partition(numbers, length, start, end);
while(index != middle)
{
if(index>middle)
{
end = index-1;
index = partition(numbers, length, start, end);
}
else
{
start = index + 1;
index = parttion(numbers, length, start, end);
}
}
int result = numbers[middle];
if(!check_more_than_half(numbers, length, result))
result = 0;
return result;
}
//检查输入是否满足要求
bool g_bInputInvalid = false;
bool check_invalid_array(int *numbers int length)
{
g_bInputInvalid = false;
if(nullptr==numbers || length <= 0)
g_bInputInvalid = true;
return g_bInputInvalid;
}
//检查数组是否满足题目要求
bool check_more_than_half(int *numbers, int length, int number)
{
int times = 0;
for(int i=0; i<length; ++i)
{
if(numbers[i]==number)
times++;
}
bool isMoreThanHalf = true;
if(times*2 <= length)
{
g_bInputInvalid = false;
isMoreThanHalf = false;
}
return isMoreThanHalf;
}
int partition(int data[], int length, int start, int end)
{
if(nullptr==data || length<=0 || start<0 || end >length-1)
throw new std::exception("Invlaid Parameters");
int index = RandomInRange(start, end);
Swap(&data[index], &data[end]);
int small = start -1;
for(index = start; index < end; ++index)
{
if(data[index]<data[end])
{
++small;
if(small!=index)
Swap(&data[index], &data[small]);
}
}
++small;
Swap(&data[small], &data[end]);
return small;
}
解法二:根据数组特点找出时间复杂度为O(n)的算法
分析:数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数的和还要多。我们考虑在遍历数组的时候保存两个值:一个是数组中的艺术数字;另一个是次数。我们遍历到下一数字时,如果下一个数字和我们之前保存的数字相同,则次数加1;如果下一个数字和我们保存的数字不同,则次数减1.如果次数为0,那么骂我们保存下一个数字,并把次数设为1。我们要找的数字肯定是最后把次数变1时对应的数字。
int more_than_half_num(int *numbers, int length)
{
if(check_invalid_array(numbers, length))
return 0;
int result = numbers[0];
int times = 1;
for(int i=1; i<length; ++1)
{
if( 0 == times )
{
result = numbers[i];
times = 1;
}
else if( numbers[i] == result )
times++;
else if
times--;
}
if(!check_more_than_half( numbers, length, result ) )
result = 0;
return result;
}
面试题40:最小的k个数
题目:输入n个整数,找出其中最小的k个数。
首先先想到的就是排序,时间复杂度为O(nlogn),面试时需要更快的算法。
解法一:利用Partition算法,要求为可修改数组,时间复杂度为O(n).
Partition在面试题39中已写。
void get_least_K_numbers(int *input int n, int *output, int k)
{
if( nullptr==input || nullptr == nullptr || k>n || n<=0 || k<=0)
return;
int start = 0;
int end = n-1;
int indext = Partition( input, n, start, end);
while( index != k-1)
{
if( index > k-1)
{
end = index - 1;
index = Partition( input, n, start, end );
}
else
{
start = index + 1;
index = Partition( input, n, start, end );
}
}
for(int i=0; i< k; ++i)
output[i] = intput[i];
}
解法二:时间复杂度为O(nlogk),适合处理海量数据。
分析:建立一个大小为k的容器来存储最小的k个数字,接下来每次从输入的n个整数中读入一个数。如果容器的数字少于k个,则直接把这次读入的整数放入容器之中;如果容器中已有k个数字了,也就是容器已满,此时我们不能再插入新的数字而只能替换已有的数字。找出k个数组最大值,然后拿待插入的整数和最大值进行比较。若插入值较小,则替换最大值;若插入值较大,则抛弃。
现在的问题:找到容器的最大值,插入新数字。很容易想到最大堆。需要O(logk)时间去完成删除及插入操作。我们还可用红黑树来实现我们的容器。红黑树的查找、插入和删除也只要O(logk)时间。在STL中,set和multiset都是基于红黑树实现的。
typedef multiset<int, greater<int> > intSet;
typedef multiset<int, greater<int> >::iterator setIterator;
void get_least_K_numbers(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(*iner);
}
}
}
}
面试题41:数据流中的中位数
题目:如何得到数据流中的中位数?
分析:如果数据流数据个数奇数,则需要排序后的位于中间的数值;如果为偶数,那么需要中间两个数。
现在需要一个容器来装数据流中的数据。
假设已经排序,将数据分为两半,中间的数为左边的最大数,和右边最小的数。
数据结构 | 插入的时间复杂度 | 得到中位数的时间复杂度 |
---|---|---|
没有排序的数组 | O(1) | O(n) |
排序的数组 | O(n) | O(1) |
排序的链表 | O(n) | O(1) |
二叉搜索树 | 平均O(logn),最差O(n) | 平均O(logn),最差O(n) |
AVL树 | O(logn) | O(1) |
最大堆与最小堆 | O(logn) | O(1) |
AVL树的效率很高,但是大多数语言都没有库,自己写的时间较长,故利用最大堆最小堆的方法。
细节问题:首先保证数据平均分配打两个堆中 ,因此两个堆中数据的数目之差不能超过1。在数据总数目是偶数时插入最小堆,否则插入最大堆。
还要保证最大堆中的所有数据都要大于最小堆中的数据。假设此时数据的总数目是偶数,按照前面的分配规则会把新的数据插入最小堆,但新数据比最大堆的某些数据还要小,此时可以先把这个数据插入最大堆,将最大堆中最大的数字插入最小堆。这样就保证了最小的数字都大于最大堆的数字。
基于STL中的函数push_heap/push_heap/vector实现堆。
template<typename T> class DynamicArray
{
public:
void Insert(T 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<T>());
num = max[0];
pop_heap(max.begin(), max.end(), less<T>());
max.pop_back();
}
min.push_back(num);
push_heap(min.begin, min.end(), greater<T>());
}
else
{
if(min.size()>0 && min[0] < num)
{
min.push_back(num);
push_heap(min.begin, min.end(), greater<T>());
num = min[0];
pop_heap(min.begin, min.end(), greater<T>());
min.pop_back();
}
max.push_back(num);
push_heap(max.begin(), max.end(), less<T>());
}
}
T GetMedian()
{
int size = min.size() + max.size();
if( size==0 )
throw exception("No numbers are available");
T median = 0;
if( size&1 ==0)
median = ( max[0] + min[0] ) /2.0 ;
else
median = min[0];
return median;
}
private:
vector<T> min;
vector<T> max;
}
面试题42:连续子数组的最大和
题目:输入一个整型数组,数组里有整数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为O(n)。
分析:动态规划。
f
(
i
)
=
{
p
D
a
t
a
[
i
]
i
=
0
或
者
f
(
i
−
1
)
≤
0
f
(
i
−
1
)
+
p
D
a
t
a
[
i
]
i
=
0
或
者
f
(
i
−
1
)
>
0
f(i)=\left\{ \begin{aligned} \ & pData[i] && {i=0或者f(i-1)≤0} \\ \ & f(i-1)+pData[i] && {i=0或者f(i-1)>0} \\ \end{aligned} \right.
f(i)={ pData[i]f(i−1)+pData[i]i=0或者f(i−1)≤0i=0或者f(i−1)>0
int find_Greatest_sum_of_SubArray(int *pData, int nLength)
{
if( ( nullptr == pData ) || ( nLength < 0 ) )
{
g_InvalidInput = true;
return 0;
}
g_InvalidInput = false;
int nCurSum = 0;
int nGreatestSum = 0x80000000; //最小的32位负数
for(int i=0; i < nLength; ++i)
{
if( nCurSum <= 0 )
nCurSum = pData[i];
else
nCurSum += pData;
if( nCurSum > nGreatestSum )
nGreatestSum = nCurSum;
}
return nGreatestSum;
}
面试题43:1~n整数中1出现的次数
题目:输入一个整数n,求1~n这个n个整数的十进制表示中1出现的次数。例如:输入12,包含1的数字有1、 10、 11、 12,总共4个。
分析:暴力法不行,速度过慢。首先来找规律,先找个稍微大一点的数字如21345,把1至21345分为两段,1至1345,1346至21345。
先看第二段出现1的情况。1出现分为两种情况。首先分析1出现在最高位的情况。在此数据段中,1出现在第5位的情况,有10000次。值得注意的是,若输入的是12345,则1出现在第5位的情况就是2346次了。
接下来分析后四位,排列组合,其中1位为1,其它0至9任选,则2x4x10x10x10=8000次。
至于1至1345,思路与1-21345一样,递归。
int number_of_1_Between_1_and_N( int n )
{
if( n <= 0 )
return 0;
char strN[50];
sprintf( strN, "%d", n);
return number_of_1(strN);
}
int number_of_1( const char *strN)
{
if( (nullptr != strN) || (*strN<'0') || (*strN>'9') || ('\0' == *strN) )
return 0;
int first = *srN - '0';
unsigned int length = static_cast<unsigned int>( strlen(strN) );
//递归结束标志
if( (1==length) && (0==first) )
return 0;
if( (1==length) && (1==first) )
return 1;
//numFirstDigit是最高位为1的的树木
int numFirstDigit = 0;
if( first > 1 )
numFirstDigit = power_base_10(length - 1);
else
numFirstDigit = atoi(strN+1) + 1;
//numOtherDigits是除最高位之外中的数目
int numOtherDigits = first * (length-1) * power_base_10( length - 2);
int numRecursive = number_of_1( strN+1 );
return numFirstDigit+numOtherDigits+numRecursive;
}
int power_base_10(unsigned int n)
{
//面试题16:数值的整数次方
return (int)power( 10, n);
}
面试题44:数字序列中某一位数字
题目:数字以0123456789101112131415···的格式序列化到一个字符序列中。在这个序列中,第5位(从0开始计数)是5,第13位是1,第19位是4,等等。请写一个函数,求任意第n对应的数字。
分析:找规律,1位数有10个,2位数有90个,三位数有900个…可知道其除1位数,m位数有9x10^(m-1)个数字。首先找到这个第n个数位在几位数中,然后是这几位数中的哪一数字,然后是这个数字的第几位。
int digit_at_index(int index)
{
if( index < 0 )
return -1;
int digits = 0;
analyze_index( &index, &digits );
return digit_of_index(index, digit);
}
void analyze_index(int *index, int *digits)
{
int tmpIndex = *index;
while(tmpIndex<0)
{
++(*digits);
if( 1 == *digit )
tmpIndex -= 10;
else
tmpIndex -= *digit * 9 * (int)std::pow(10, digits-1);
if( tmpIndex > 0 )
*index = tmpIndex;
}
}
int digit_of_index(int index, int digits)
{
int number = begin_number(digits) + index/digits;
int indexFromRight = digits - index%digits;
for(int i=1; i < indexFromRight; ++i)
{
number /= 10;
}
return number%10 ;
}
int begin_number(int digits)
{
if( 1==digits )
return 0;
return (int)std::pow( 10, digits-1);
}
面试题45:把数组排成最小的数字
题目:输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如,输入数组{3, 32, 321},则打印321323。
const int g_MaxNumberLength = 10;
char* g_StrCombine1 = new char[g_MaxNumberLength*2+1];
char* g_StrCombine2 = new char[g_MaxNumberLength*2+1];
void print_min_number(int *number, int length)
{
if( nullptr==number || length<0 )
return;
char **strNumbers = new char*[length];
for(int i=0; i<length; ++i)
{
strNumbers[i] = new char[g_MaxNumberLength+1];
sprintf(strNumbers[i], "%d", number[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排前面的话就返回负数
* strNumber1排后面的话就返回正数
*/
int compare(const void* strNumber1, const void* strNumber2)
{
strcpy(g_StrCombine1, *(const char**)strNumber1);
strcat(g_StrCombine1, *(const char**)strNumber2);
strcpy(g_StrCombine2, *(const char**)strNumber2);
strcat(g_StrCombine2, *(const char**)strNumber1);
/* str1 < str2, return 负数 */
/* str1 > str2, return 正数 */
/* str1 == str2, return 0 */
return strcmp(g_StrCombine1, g_StrCombine2);
}
面试题46:把数字翻译成字符串
题目:给定一个数字,我们按照如下规则把它翻译成字符串:0翻译成‘a’,1翻译成‘b’…25翻译成‘z’.求一个数字可能有多少个翻译。
分析:这是一个动态规划题,我们定义f(i)表示从第i个数字开始不同翻译的数目,那么
f
(
i
)
=
f
(
i
+
1
)
+
g
(
i
,
i
+
1
)
×
f
(
i
+
2
)
f(i)=f(i+1)+g(i,i+1)\times f(i+2)
f(i)=f(i+1)+g(i,i+1)×f(i+2)
当第i和第i+1位两位数字拼接起来的数字在10~25的范围内时,函数g(i,i+1)的值为1;否则为0.
递归是从左往右,右边是最小的问题,故我们可以从数字的末尾开始,然后从右往左翻译并计算不同翻译的数目。
int get_translation_count(int number)
{
if(number < 0)
{
return 0;
}
string numberInString = to_string(number);
return get_translation_str_count(numberInString);
}
int get_translation_str_count(const string& number)
{
int length = number.length();
int *counts = new int[length]();
int count;
for(int i=length-1; i>=0; --i)
{
count = 0;
if(i<length-1)
count = counts[i+1];
else
count = 1;
if(i<length-1)
{
int digit1 = number[i]-'0';
int digit2 = number[i+1]-'0';
int converted = digit1*10+digit2;
if(converted>=10&&converted<=25)
{
if(i<length-2)
count +=counts[i+2];
else
count +=1;
}
}
counts[i] = count;
}
count = counts[0];
delete[] counts;
return count;
}
面试题47:礼物的最大价值
题目:在一个m
×
\times
×n的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或向下一格,直到达到棋盘的右下角。给定一格棋盘及其上面的礼物,请计算你最多能拿到多少价值的礼物。
分析:动态规划,先用递归的思路来分析。我们先定义第一个函数
f
(
i
,
j
)
f(i,j)
f(i,j)表示到达坐标为
(
i
,
j
)
(i,j)
(i,j)的格子时能拿到的礼物总和的最大值。根据题目要求,我们两种可能的途径到达坐标为
(
i
,
j
)
(i,j)
(i,j)的格子:通过
(
i
−
1
,
j
)
(i-1,j)
(i−1,j)或者
(
i
,
j
−
1
)
(i,j-1)
(i,j−1)。所以
f
(
i
,
j
)
=
m
a
x
(
f
(
i
−
1
,
j
)
,
f
(
i
,
j
−
1
)
)
+
g
i
f
t
[
i
,
j
]
f(i,j)=max(f(i-1,j),f(i,j-1))+gift[i,j]
f(i,j)=max(f(i−1,j),f(i,j−1))+gift[i,j]。
g
i
f
t
[
i
,
j
]
gift[i,j]
gift[i,j]表示坐标为
(
i
,
j
)
(i,j)
(i,j)的格子里礼物的价值。
尽管我们用递归来分析问题,但有大量重复的计算,导致递归的代码并不是最优的。相对而言,基于循环的代码效率要高得多。为了缓存中间计算结果,我们需要一个辅助的二维数组。
int get_max_value_solution1(const int *values, int rows, int cols)
{
if( nullptr==values || rows <= 0 || cols <= 0)
return 0;
int **maxValues = new int*[rows];
for(int i=0; i < rows; ++i)
maxValues[i] = new int[cols];
for(int i = 0; i < rows; ++i)
{
for(int j=0; j < cols; ++j)
{
int left = 0;
int up = 0;
if(i>0)
up = maxValues[i-1][j];
if(j>0)
left = maxValues[i][j-1];
maxValues[i][j] = std::max(left, up) + values[i*cols+j];
}
}
int maxValue = maxValues[rows-1][cols-1];
for(int i = 0; i < rows; ++i)
delete[] maxValues[i];
delete[] maxValues;
return maxValue;
}
优化:在动态规划可看到,计算 f ( i , j ) f(i,j) f(i,j)的值取决于 f ( i − 1 , j ) 与 f ( i , j − 1 ) f(i-1,j)与f(i,j-1) f(i−1,j)与f(i,j−1),前面算的那些数据都可舍去,实现空间复杂度的减小。
int get_max_value_solution2(const int *values, int rows, int cols)
{
if( nullptr==values || rows <= 0 || cols <= 0)
return 0;
int *maxValues = new int[cols];
for(int i = 0; i < rows; ++i);
{
for(int j = 0; j < cols; ++j)
{
int left = 0;
int up = 0;
if( i > 0 )
up = maxValues[j];
if( j > 0 )
left = maxValues[j-1];
maxValues[j] = std::max(left, up) + values[i*cols+j];
}
}
int maxValue = maxValues[cols-1];
return maxValues;
}
面试题48:最长不含重复字符的子字符串
题目:请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长字符串的长度。
分析:伪代码
if(第i个字符没有出现过)
f(i) = f(i-1) + 1;
else
{
//假设第i个字符和它上次出现在字符串中的距离记为d
if( d <= f(i1-1) )
f(i) = d;
else
f(i) = f(i-1) + 1;
}
int longest_substring_without_duplication(const std::string& str)
{
int curLength = 0;
int maxLength = 0;
//记录上一次字母出现的位置
int *position = new int[26];
memset(position, -1, 26*sizeof(int));
for(int i = 0; i < str.length(); ++i)
{
int prevIndex = position[str[i] - 'a'];
if( prevIndex < 0 || i-prevIndex > curLength)
++curLength;
else
{
if( curLength > maxLength )
maxLength = curLength;
curLength = i-prevIndex;
}
position[str[i]-'a'] = i;
}
if( curLength > maxLength )
maxLength = curLength;
delete[] position;
return maxLength;
}
面试题49:丑数
题目:我们把只包含因子2、3、5的数成为丑数。求它按从小到大的顺序的第1500个丑数。
分析:暴力法虽直观,但时间复杂度过大。利用空间换时间的做法,得申请数量为1500的数组。
int get_ugly_number(int index)
{
if(index<=0)
return 0;
int *pUglyNumbers = new int[index];
pUglyNumbers[0] = 1;
int nextUglyIndex = 1;
int *pMultiply2 = pUglyNumbers;
int *pMultiply3 = pUglyNumbers;
int *pMultiply5 = pUglyNumbers;
while(nextUglyIndex < index)
{
int min = Min(*pMultiply2*2, *pMultiply3*3, *pMultiply5*5);
pUglyNumbers[nextUglyIndex] = min;
while(*pMultiply2*2 <= pUglyNumbers[nextUglyIndex])
++pMultiply2;
while(*pMultiply3*3 <= pUglyNumbers[nextUglyIndex])
++pMultiply3;
while(*pMultiply5*5 <= pUglyNumbers[nextUglyIndex])
++pMultiply5;
++nextUglyIndex;
}
int ugly = pUglyNumbers[nextUglyIndex-1];
delete[] pUglyNumbers;
return ugly;
}
int Min(int number1, int number2, int number3)
{
int min = (number1 < number2)?number1:number2;
min = (min < number3)?min:number3;
return min;
}
面试题50:第一个只出现一次的字符
题目一:字符串中第一个只出现一次的字符:在字符串中找出第一个只出现一次的字符。
分析:利用哈希表原理,假设一个字符串为1一个字节,那么有256种可能,则可以用数量为256的整型数组来表示各字符的出现的次数,256为常数,故空间复杂度为O(1),第一次遍历计数,第二次遍历找出第一个一次的数。
char first_not_repeating_char(char *pString)
{
if( nullptr == pString )
return '\0';
const int tablesize = 256;
unsigned int hashTable[tablesize];
memset(hashTable, 0, tablesize*sizeof(int));
char *pHashKey = pString;
while( *(pHashKey) != '\0' )
hashTable[*(pHashKey++)]++;
pHashKey = pString;
while(*pHashKey != '\0' )
{
if(hasTable[*pHashKey] == 1)
return *pHashKey;
pHashKey++;
}
return '\0';
}
题目二:字符流第一个出现一次的字符。
请实现一个函数,用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符“go”时,输出‘g’;当读出“google”,输出为‘l’。
分析:字符只能一个接着一个从字符流中读出来。可以定义一个数据容器来保存它在字符流中的位置。
class CharStastics
{
public:
CharStastics():index(0)
{
for(int i= 0; i < 256; ++i)
occurrence[i] = -1;
}
void Insert(char ch)
{
if( -1 == occurrence[ch] )
occurrence[ch] = index;
else if(occurrence[ch] >= 0)
occurrence[ch] = -2;
++index;
}
char first_appearing_once()
{
char ch = '\0';
int minIndex = numeric_limits<int>::max();
for(int i=0; i<256; ++i)
{
if( occurrence[i]>=0 && occurrence[i] < minIndex)
{
ch = (char)i;
minIndex = occurrence[i];
}
}
return ch;
}
private:
int occurrence[256];
int index;
}
面试题51:数组中的逆序对
题目:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组的逆序对的总数。
分析:利用归并排序来写这个基本算法。
int inverse_pairs(int *data, int length)
{
if( nullptr==data || length < 0 )
return 0;
int *copy = new int[length];
memcpy(copy, data, length*sizeof(int));
int count = inverse_pairs_core(data, copy, 0, length-1);
delete[] copy;
return count;
}
int inverse_pairs_core(int *data, int *copy, int start, int end)
{
if(start==end)
{
copy[start] = data[start];
return 0;
}
int length = (end-start)/2;
int left = inverse_pairs_core(copy, data, start, start+length);
int right = inverse_pairs_core(copy, data, start+length+1, end);
int i = start + length;
int j = end;
int indexCopy = end;
int count = 0;
while( i>=start && j>=start+length+1 )
{
if(data[i]>data[j])
{
copy[indexCopy--]=data[i--];
count += j-start-length;
}
else
{
copy[indexCopy--] = data[j--];
}
}
while(i>=start)
copy[indexCopy--] = data[i--];
while(j>=statr+length+1)
copy[indexCopy--] = data[j--];
return left+right+count;
}
面试题52:两个链表的第一公共节点
题目:输入两个链表,找出它们的第一个公共节点。
分析:利用栈,将两条链表压入两个栈,然后从尾部开始比较。
还可以利用长度,得到两条链的长度,然后得到长度之差,长链先经过长度之差,然后再与短链一个一个比较。
struct ListNode
{
int m_nKey;
ListNode *m_pNext;
}
ListNode *find_first_common_node(ListNode *pHead1, ListNode *pHead2)
{
unsigned int nLength1 = get_list_length(pHead1);
unsigned int nLength2 = get_list_length(pHead2);
int nLengthDif = nLength1-nLength2;
ListNode *pHeadLong = nLength1;
ListNode *pHeadShort = nLength2;
if(nLength2>nLength1)
{
pHeadLong = nLength2;
pHeadShort = nLength1;
nLengthDif = nLength2 - nLength1;
}
for(int i=0; i<nLengthDIf; ++i)
pHeadLong = pHeadLong->m_pNext;
while((nullptr!=pHeadLong)&&(nullptr!=pHeadShort)&&(pHeadLong!=pHeadShort))
{
pHeadLong = pHeadLong->m_pNext;
pHeadShort = pHeadShort->m_pNext;
}
ListNode *pFisrtCommonNode = pHeadLong;
return pFisrtCommonNode;
}
unsigned int get_list_length(ListNode *pHead)
{
unsigned int nLength = 0;
ListNode *pNode = pHead;
while(nullptr!=pNode)
{
++nLength;
pNode = pNode->m_pNext;
}
return nLength;
}
面试题53:在排序数组中查找数字
题目一:数字在排序数组中出现的次数。
分析:利用二分法,找到出现的对应第一个数的序列和最后一个数的序列。
int get_first_K(int *data, int length, int k, int start, int end)
{
if(start > end)
return -1;
int middleIndex = (start + end)/2;
int middleData = data[middleIndex];
if(middleData==k)
{
if((middleIndex>0&&data[middleIndex-1]!=k)||middleIndex==0)
return middleIndex;
else
end = middleIndex-1;
}
else if(middleData>k)
end = middleIndex-1;
else
start = middleIndex+1;
return get_first_K(data, length, k, start, end);
}
int get_last_K(int *data, int length, int k, int start, int end)
{
if(start > end)
return -1;
int middleIndex = (start + end)/2;
int middleData = data[middleIndex];
if(middleData==k)
{
if((middleIndex<length-1&&data[middleIndex+1]!=k)||middleIndex==length-1)
return middleIndex;
else
start = middleIndex+1;
}
else if(middleData>k)
end = middleIndex-1;
else
start = middleIndex+1;
return get_last_K(data, length, k, start, end);
}
int get_number_of_K(int *data, int length, int k)
{
int number = 0;
if(data!=nullptr && length>0)
{
int first = get_first_K(data, length, k, 0, length-1);
int last = get_last_K(data, length, k, 0, length-1);
if(first>-1&&last>-1)
number = last - first + 1;
}
return number;
}
题目二:0~n-1中缺失的数字。
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在0~n-1范围内,但在这个范围内的n个数字中有且只有一个数字不在该数组内,请找出该数字。
分析:可知缺少的数字之前的数字与序号都是相等的,之后的数字不相等,以此为条件来二分法进行判断。
int get_missing_number(const int *numbers, int length)
{
if(numbers == nullptr || length <= 0)
return -1;
int left = 0;
int right = length-1;
while(left<=right)
{
int middle = (left+right)>>1;
if(numbers[middle]==middle)
left = middle+1;
else
{
if((middle>0&&numbers[middle-1]==middle-1)||middle==0)
return middle;
else
right = middle-1;
}
}
if(left==length)
return length;
return -1;
}
题目三:数组中数值和下标相等的元素。
假设一个单调递增里的每个元素都是整数并且是唯一的。请编程实现一个函数,找出数组中任意一个数值等于下标的元素。
int get_number_same_as_index(const int *number, int length)
{
if(numbers==nullptr||length<=0)
return -1;
int left = 0;
int right = length -1;
while(left<=right)
{
int middle = left + ((right-left)>>1);
if(numbers[middle] == middle)
return middle;
if(numbers[middle]>middle)
right = middle -1 ;
else
left = middle + 1;
}
return -1;
}
面试题54:二叉搜索树的第k个大节点
题目:给定一棵二叉搜索树,请找出其中从小到大第k个节点。
利用中序遍历修改版来进行编写。
BianryTreeNode* kth_node_core(BianryTreeNode* pRoot, unsigned int& k)
{
BianryTreeNode* target = nullptr;
if(pRoot->m_pLeft!=nullptr)
target = kth_node_core(pRoot->m_pLeft, k);
if(target==nullptr)
{
if(k==1)
target = pRoot;
k--;
}
if(target==nullptr&&pRoot->m_pRight!=nullptr)
target = kth_node_core(pRoot->m_pRight, k);
return target;
}
面试题55:二叉树的深度
题目一:输入一棵二叉树的根节点,求该树的深度。
int tree_depth(BinaryTreeNode* pRoot)
{
if(nullptr==pRoot)
return 0;
int nLeft = tree_depth(pRoot->m_pLeft);
int nRight = tree_depth(pRoot->m_pRight);
return (nLeft>nRight)?(nLeft+1):(nRight+1);
}
题目二:平衡二叉树。
输入一棵二叉树的根节点,判断该树是不是平衡二叉树。如果某二叉树中任意节点的左右子树的深度相差不超过1,那么它就是一棵平衡二叉树。
只遍历一次。
bool is_balanced_core(BianryTreeNode* pRoot, int *pDepth)
{
if( pRoot == nullptr )
{
*pDepth = 0;
return true;
}
int left, right;
if(is_balanced_core(pRoot->m_pLeft, &left)&&is_balanced_core(pRoot->m_pRight, &right))
{
int diff = left - right;
if( diff <= 1 && diff >= -1 )
{
*pDepth = 1+(right>left?right:left);
return true;
}
}
return false;
}
bool is_balanced(BianryTreeNode* pRoot)
{
int depth = 0;
return is_balanced_core( pRoot, &depth );
}
面试题56:数组中数字出现的次数
题目一:数组中只出现一次的两个数字。
一个整数数组里除了两个数字之外,其他数字都出现了两次,找到这两个数字,要求时间复杂度是O(n),空间复杂度是O(1)。
分析:已知两个相同的数异或等于0,那么将其分成两个组,两个组分别都有一个不重复的数,将其全部数字进行异或,那么结果就是这两个数字的异或,这个结果按位运算,以第一位不为0的位来区别。
void find_nums_appear_once(int data[], int length, int *num1, int *num2)
{
if( data==nullptr || length<2 )
return;
int resultXOR = 0;
for(int i=0; i < length; ++i)
resultXOR ^= data[i];
unsigned int indexOf1 = find_first_bit1( resultXOR );
*num1 = *num2 = 0;
for( int j=0; j<length; ++j)
{
if( is_bit_1(data[j], indexOf1) )
*num1 ^= data[j];
else
*num2 ^= data[j];
}
}
unsigned find_first_bit1( int num )
{
int indexBit = 0;
while( ( (num&1)==0 ) && ( indexBit < 8*sizeof(int) ))
{
num = num >> 1;
++indexBit;
}
return indexBit;
}
bool is_bit_1(int num, int index)
{
num = num >> index;
return (1&num);
}
题目二:数组中唯一只出现一次的数字。
除了只出现一次的数字,其他数字出现了三次,找出那个只出现一次的数字。
分析:我们把所有出现三次的数字的二进制表示的每一位分别加起来,那么每一位的和都能被3整除。
int find_number_appearing_once(int numbers[], int length)
{
if( numbers==nullptr || length<=0 )
throw new std::exception("Invalid input.");
int bitSum[32] = {0};
for(int i; i<length; ++i)
{
int bitMask = 1
//第0位在最右边
for(int j=31; j>=0; --j)
{
int bit = numbers[i]&bitMask;
if( bit!=0 )
++bitSum[j];
bitMask = bitMask<<1;
}
}
int result;
for(int i=0; i<32; ++i)
{
result = result << 1;
result += bitSum[i]%3;
}
return result;
}
另一种解法:利用状态机变化,详情见LeetCode。
int find_number_appearing_once(int numbers[], int length)
{
if( numbers==nullptr || length<=0 )
throw new std::exception("Invalid input.");
int one = 0;
int two = 0;
for(int i=0; i<length; ++i)
{
one = one^numbers[i]&(~two);
two = two^numbers[i]&(~one);
}
return one;
}
面试题57:和为s的数字
题目一:和为s的两个数字。
题目:输入一个递增排序的数组和一个数字s,在数组查找两个数,使得它们的和正好是s。输出任意一对即可。
bool find_numbers_wiht_sum(int data[], int length, int sum, int *num1, int *num2)
{
bool found = false;
if( data==nullptr || length <= 0)
return found;
int left = 0;
int right = length -1 ;
while(left<right)
{
long long curSum = data[left]+data[right];
if(curSum == sum)
{
*num1 = data[left];
*num2 = data[right];
found = true;
break;
}
else if(curSum > sum)
--right;
else
--left;
return found;
}
题目二:和为s的连续正数序列
题目:输入一个正数s,打印所有和为s的连续正数序列(至少含有两个数)。
分析:根据题目一经验,在连续正数之间滑动,来解决,将left遍历到(1+s)/2。
void find_continuous_sequence(int sum)
{
if(sum<3)
return;
int left = 1;
int right = 2;
int curSum = left + right;
int middle = (sum+1)/2;
while( left<middle )
{
if( curSum==sum )
print_continuous_sequence(left,right);
while( curSum>sum && small<middle )
{
curSum -= left;
++left;
if( curSum==sum )
print_continuous_sequence(left,right);
}
++right;
curSum += right;
}
}
void print_continuous_sequence(int left, int right)
{
for(int i=left; i<=right; ++i)
printf("%d ");
printf("\n");
}
面试题58:翻转字符串
题目一:翻转单词顺序
题目:输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。为简单起见,标点符号和普通字母一样处理。例如"I am s student. “, 则输出"student. a am I”。
void reverse(char *pBegin, char *pEnd)
{
if( pBegin==nullptr || pEnd==nullptr )
return;
while(pBegin<pEnd)
{
char temp = *pBegin;
*pBegin = *End;
*End = temp;
++pBegin;
++pEnd;
}
}
char *reverse_sentence(char *pData)
{
if(pData==nullptr)
return nullptr;
char *pBegin = pData;
char *pEnd = pData;
while(*pEnd != '\0')
++pEnd;
--pEnd;
reverse(pBegin, pEnd);
while(*pBegin!='\0')
{
if(*pBegin==' ')
{
++pBegin;
++pEnd;
}
while(*pEnd!=' ' && *pEnd!='\0')
++pEnd;
reverse(pBegin, --pEnd);
pBegin = ++pEnd;
}
return pData;
}
题目二:左旋转字符串
题目:字符串的左旋转操作时把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋操作的功能。比如:输入的字符串"abcdefg"和2,得到"cdefgab"。
void reverse(char *pBegin, char *pEnd)
{
if( pBegin==nullptr || pEnd==nullptr )
return;
while(pBegin<pEnd)
{
char temp = *pBegin;
*pBegin = *End;
*End = temp;
++pBegin;
++pEnd;
}
}
char *left_rotate_string(char *pStr, int n)
{
if(pStr!=nullptr)
{
int nLength = strlen(pStr);
if(nLength>0 && n>0 && n<nLength)
{
char *pFirstBegin = pStr;
char *pFisrtEnd = pStr+n-1;
char *pSecondBegin = pStr+n;
char *pSecondEnd = pStr + nLength-1;
reverse(pFirstBegin, pFisrtEnd);
reverse(pSecondBegin, pSecondEnd);
reverse(pFirstBegin, pSecondEnd);
}
}
return pStr;
}
面试题59:队列的最大值
题目一:滑动窗口的最大值
题目:给定一个数组和滑动窗口的大小,请找出所有滑动窗口里的最大值。例如,如果输入数组{2, 3, 4, 2, 6, 2, 5, 1}及滑动窗口的大小3, 那么一共存在6个滑动窗口,它们的最大值分别为{4, 4, 6, 6, 6, 5}。
vector<int> max_in_windows(const vector<int>& num, unsigned int size)
{
vector<int> maxInWindows;
if( num.size()>=size && size>=1 )
{
deque<int> index;
for(unsigned int i=0; i<size; ++i)
{
while( !index.empty() && num[i]>=num[index.back()] )
index.pop_back();
index.push_back();
}
for(unsigned int i = size; i < num.size(); ++i)
{
maxInWindows.push_back(num[index.front()]);
while( !index.empty() && num[i]>=num[index.back()])
index.pop_back();
while( !index.empty() && index.front() <= (int)(i-size) )
index.front_back();
index.push_back(i);
}
maxInWindows.push_back(num[index.front()]);
}
return maxInWindows;
}
题目二:队列的最大值
题目:请定义一个队列并实现函数max得到队列里的最大值,要求函数max、push_back和pop_front的时间复杂度都是O(1)。
template<typename T> class QueueWithMax
{
public:
QueueWithMax():currentIndex(0)
{
}
void push_back(T number)
{
while( !maximums.empty() && number >= maximums.back().number )
maximums.pop_back();
InternalData internalData = {number, currentIndex};
data.push_back(internalData);
maximums.push_back(internalData);
++currentIndex;
}
void pop_front(void)
{
if( maximums.empty() )
throw new exception("queue is empty");
if( maximums.front().index == data.front().index )
maximums.pop_front();
data.pop_front();
}
T max() const
{
if( maximums.empty() )
throw new exception("queue is empty");
return maximums.front().number;
}
private:
typedef struct
{
T number;
int index;
}InternalData;
deque<InternalData> data;
deque<InteranlData> maximums;
int currentIndex;
};
面试题60:n个骰子的点数
题目:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
解法一:基于递归求筛子点数,时间效率不高,申请一个5n+1的空间,计算每个数出现的次数。
int g_maxValue = 6;
void print_probability(int number)
{
if( number > 1 )
return;
int maxSum = number * g_maxValue;
int *pProbabilities = new int[maxSum - number + 1];
for(int i = number; i <= maxSum; ++i)
pProbabilities[i-number] = 0;
probability(number, pProbabilities);
int total = pow( (doublt)g_maxValue, number );
for(int i = number; i<=maxSum; ++i)
{
double ratio = (double)pProbabilities[i - number]/total;
printf("%d:%e\n", i, ratio);
}
delete[] pProbabilities;
}
void probability( int number, int *pProbabilities)
{
for(int i = 1; i < g_maxValue; ++i)
probability(number, number, i, pProbabilities);
}