由於之前要準備paper以及研電賽,好久沒有更新了,paper已經投了個會議了,然後研電賽只拿到了西北賽區人工智能組的二等獎,沒能晉級國賽有點遺憾。廢話不多說,準備秋招了,刷點編程題練練手。每題都有思路,有些是參考的,會註明出處。題目順序參照牛客網,語言選擇C++,因爲C++速度確實快。
1 二維數組的查找
在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。
class Solution {
public:
/**
利用
每一行都按照從左到右遞增的順序排序
每一列都按照從上到下遞增的順序排序
如果 xx 小於target,則 xx 左邊的數一定都小於target,我們可以直接排除當前一整行的數;
如果 xx 大於target,則 xx 下邊的數一定都大於target,我們可以直接排序當前一整列的數;
參考:https://www.acwing.com/solution/AcWing/content/702/
*/
bool Find(int target, vector<vector<int> > a) {
//如果a爲空 直接返回false
if(a.empty()||a[0].empty())return false;
int i = 0, j = a[0].size() - 1;//要從右上角往左下角找
while(i < a.size() && j >= 0){
if(a[i][j] == target)return true;//找到直接返回
if(a[i][j] > target) j--;//列往左邊移動
else i++;//行往下移動找
}
return false;
}
};
2 替換空格
請實現一個函數,將一個字符串中的每個空格替換成“%20”。例如,當字符串爲We Are Happy.則經過替換之後的字符串爲We%20Are%20Happy。
class Solution {
public:
void replaceSpace(char *str,int length) {
if(str == NULL) return; //如果是空 直接返回
//空格數 原始串的長 新串的長
int blanks = 0, len = 0, newlen = 0;
for(int i = 0;str[i]!='\0';i++){
len++;
if(str[i]==' ')
blanks++;
}
//新串的長 每次替換 多增加兩個字符
newlen = len + 2*blanks;
//如果新串的長大於length 直接返回
if(newlen + 1 > length)return;
//str1指針指向舊串的最後一個字符
char * str1 = str + len;
//str2指針指向新串的最後一個字符
char * str2 = str + newlen;
//從後向前自制舊串 並替換空格
while(str1 != str2){
if(*str1 == ' '){
*str2 -- = '0';
*str2 -- = '2';
*str2 -- = '%';
}
else
*str2 -- = *str1;
--str1;
}
}
};
3 從尾到頭打印鏈表
輸入一個鏈表,按鏈表值從尾到頭的順序返回一個ArrayList。
/**
* struct ListNode {
* int val;
* struct ListNode *next;
* ListNode(int x) :
* val(x), next(NULL) {
* }
* };
沒有什麼要求,直接遍歷一次鏈表,每次向vector.begin()處插入就行了
*/
class Solution {
public:
vector<int> printListFromTailToHead(ListNode* head) {
vector<int> res;
while(head){
res.insert(res.begin(),head->val);
head = head->next;
}
return res;
}
};
4 重建二叉樹
輸入某二叉樹的前序遍歷和中序遍歷的結果,請重建出該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重複的數字。例如輸入前序遍歷序列{1,2,4,7,3,5,6,8}和中序遍歷序列{4,7,2,1,5,3,8,6},則重建二叉樹並返回。
/**
* Definition for binary tree
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
算法
(遞歸) O(n)O(n)
遞歸建立整棵二叉樹:先遞歸創建左右子樹,然後創建根節點,並讓指針指向兩棵子樹。
具體步驟如下:
先利用前序遍歷找根節點:前序遍歷的第一個數,就是根節點的值;
在中序遍歷中找到根節點的位置 kk,則 kk 左邊是左子樹的中序遍歷,右邊是右子樹的中序遍歷;
假設左子樹的中序遍歷的長度是 ll,則在前序遍歷中,根節點後面的 ll 個數,是左子樹的前序遍歷,剩下的數是右子樹的前序遍歷;
有了左右子樹的前序遍歷和中序遍歷,我們可以先遞歸創建出左右子樹,然後再創建根節點;
時間複雜度分析
我們在初始化時,用哈希表(unordered_map<int,int>)記錄每個值在中序遍歷中的位置,
這樣我們在遞歸到每個節點時,在中序遍歷中查找根節點位置的操作,
只需要 O(1)O(1) 的時間。此時,創建每個節點需要的時間是 O(1)O(1),所以總時間複雜度是 O(n)O(n)。
參考:https://www.acwing.com/solution/AcWing/content/706/
*/
class Solution {
public:
TreeNode* dfs(unordered_map<int,int> &pos,vector<int> &pre, vector<int> &vin, int pl, int pr, int vl, int vr){
if(pl > pr)return 0;
int k = pos[pre[pl]] - vl ;//k的左邊爲左子樹,右邊爲右子樹
TreeNode* root = new TreeNode(pre[pl]);//創建根
root->left = dfs(pos, pre, vin, pl + 1, pl + k, vl, vl + k - 1);//遞歸創建左子樹,更新下次pre和vin
root->right = dfs(pos, pre, vin, pl + k + 1, pr, vl + k + 1, vr);//遞歸創建右子樹,更新下次pre和vin
return root;
}
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin) {
int n = pre.size();
unordered_map<int,int> pos;//記錄中序遍歷的位置
for(int i = 0; i < n; i++){
pos[vin[i]] = i;
}
return dfs(pos, pre, vin, 0, n - 1, 0, n - 1);
}
};
5 用棧實現隊列
用兩個棧來實現一個隊列,完成隊列的Push和Pop操作。 隊列中的元素爲int類型。
/**
用兩個棧 一個緩存用
當操作時 先把主棧的數據先全部放到緩存
然後再pop
完成後放到主棧
**/
class Solution
{
public:
void copy(stack<int> &a, stack<int> &b) {
while (a.size()) {
b.push(a.top());
a.pop();
}
}
void push(int node) {
stack1.push(node);
}
int pop() {
copy(stack1,stack2);
int res = stack2.top();
stack2.pop();
copy(stack2,stack1);
return res;
}
private:
stack<int> stack1;//主棧
stack<int> stack2;//緩存棧
};
6 旋轉數組中最小的數字
把一個數組最開始的若干個元素搬到數組的末尾,我們稱之爲數組的旋轉。 輸入一個非減排序的數組的一個旋轉,輸出旋轉數組的最小元素。 例如數組{3,4,5,1,2}爲{1,2,3,4,5}的一個旋轉,該數組的最小值爲1。 NOTE:給出的所有元素都大於0,若數組大小爲0,請返回0。
/**
數組{3,4,5,1,2}爲{1,2,3,4,5}的一個旋轉
可以看出
因爲旋轉前數組是遞增
旋轉後有一個這個遞增就斷了
相當於找到一個數比第一個數小的情況
就可以返回這個數
**/
class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray) {
if(!rotateArray.size())return 0;
int res = rotateArray[0];
for(int i = 1; i < rotateArray.size(); i++){
if(rotateArray[i] < res)return rotateArray[i];
}
return res;
}
};
7 斐波那契數列
大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項(從0開始,第0項爲0)。n<=39。
/**
這個沒什麼好說的
建議用遞推
除非面試官要求 否則不要用遞歸
**/
class Solution {
public:
int Fibonacci(int n) {
if(n == 0) return 0;
if(n == 1) return 1;
int first = 0, second = 1, third = 0;
while(n--){
third = first + second;
first = second;
second = third;
}
return first;
}
};
8 跳臺階
一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法(先後次序不同算不同的結果)。
/**
假設處於當前臺階
那麼從其他臺階跳上來的
要麼從前一個臺階跳上來 要麼從前面第2個跳過來
那麼 跳到當前臺階的就有f(n-1) + f(n - 2)
**/
class Solution {
public:
int jumpFloor(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
return jumpFloor(n - 1) + jumpFloor(n - 2);
}
};
9 變態跳臺階
一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
/**
因爲n級臺階,第一步有n種跳法:跳1級、跳2級、到跳n級
跳1級,剩下n-1級,則剩下跳法是f(n-1)
跳2級,剩下n-2級,則剩下跳法是f(n-2)
所以f(n)=f(n-1)+f(n-2)+...+f(1)
因爲f(n-1)=f(n-2)+f(n-3)+...+f(1)
所以f(n)=2*f(n-1)
假設
f(1) = 1;
所以:
f(2)=1+f(1)=1+1=2=2^1;
f(3)=1+f(2)+f(1)=1+2+1=4=2^2;
f(4)=1+f(3)+f(2)+f(1)=1+4+2+1=8=2^3;
......
歸納:
f(n) = 2^(n-1);(n>=1的整數)
**/
class Solution {
public:
int jumpFloorII(int number) {
int a = 1;
return a << (number - 1);//左移乘2 右移除2 如在折半查找中 可以 int mid = (l + r) >> 1
}
};
10 矩形覆蓋
我們可以用2*1的小矩形橫着或者豎着去覆蓋更大的矩形。請問用n個2*1的小矩形無重疊地覆蓋一個2*n的大矩形,總共有多少種方法?
class Solution {
public:
int rectCover(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
int first = 1, second = 2, third = 0;
for(int i=3;i<=n;i++){
third = first + second;
first = second;
second = third;
}
return third;
}
};
11 二進制中1的個數
輸入一個整數,輸出該數二進制表示中1的個數。其中負數用補碼錶示。
/**
(位運算) O(logn)O(logn)
迭代進行如下兩步,直到 nn 變成0爲止:
如果 nn 在二進制表示下末尾是1,則在答案中加1;
將 nn 右移一位,也就是將 nn 在二進制表示下的最後一位刪掉;
這裏有個難點是如何處理負數。
在C++中如果我們右移一個負整數,系統會自動在最高位補1,這樣會導致 nn 永遠不爲0,就死循環了。
解決辦法是把 nn 強制轉化成無符號整型,這樣 nn 的二進制表示不會發生改變,但在右移時系統會自動在最高位補0。
時間複雜度
每次會將 nn 除以2,最多會除 lognlogn 次,所以時間複雜度是 O(logn)O(logn)。
參考大神:https://www.acwing.com/solution/AcWing/content/732/
**/
class Solution {
public:
int NumberOf1(int n) {
int res = 0;
unsigned int u = n;
while(u) res += u & 1, u >>= 1;
return res;
}
};
12 數值的整數次方
給定一個double類型的浮點數base和int類型的整數exponent。求base的exponent次方。
/**
求冪就是n個base相乘
如果是負數,取倒數
**/
class Solution {
public:
double Power(double base, int exponent) {
double res = 1;
for(int i = 0; i < abs(exponent); i++)
res *= base;
if(exponent < 0)
res = 1 / res;
return res;
}
};
13 調整數組順序使奇數位於偶數前面
輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得所有的奇數位於數組的前半部分,所有的偶數位於數組的後半部分,並保證奇數和奇數,偶數和偶數之間的相對位置不變。
/**
由於沒有內存申請限制
直接用兩個隊列 一個裝奇數 一個裝偶數
依次按放到原來的數組
**/
class Solution {
public:
void reOrderArray(vector<int> &array) {
queue<int> s1,s2;
for(auto it: array){
if(it % 2 == 0)
s2.push(it);
else s1.push(it);
}
for(int i = 0; i < array.size(); i++){
if(!s1.empty()){
array[i] = s1.front();
s1.pop();
}
else{
array[i] = s2.front();
s2.pop();
}
}
}
};
14 鏈表中倒數第k個結點
輸入一個鏈表,輸出該鏈表中倒數第k個結點。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
先算出鏈表的長度n
倒數第k個結點即爲順數第n-k個結點
};*/
class Solution {
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) {
int len = 0;
auto cur = pListHead;
while(cur){
len++;
cur = cur->next;
}
if(pListHead == NULL || k > len || k < 1)return NULL;
int i = len - k;
cur = pListHead;
while(i--){
cur = cur->next;
}
return cur;
}
};
15 反轉鏈表
輸入一個鏈表,反轉鏈表後,輸出新鏈表的表頭
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
ListNode *pre = NULL, *next = NULL;
while(pHead){
//做循環,如果當前節點不爲空的話,始終執行此循環,此循環的目的就是讓當前節點從指向next到指向pre
//如此就可以做到反轉鏈表的效果
//先用next保存head的下一個節點的信息,保證單鏈表不會因爲失去head節點的原next節點而就此斷裂
next = pHead->next;//記錄當前結點的下一個結點
//保存完next,就可以讓head從指向next變成指向pre了
pHead->next = pre;
//head指向pre後,就繼續依次反轉下一個節點
//讓pre,head,next依次向後移動一個節點,繼續下一次的指針反轉
pre = pHead;
pHead = next;
}
//如果head爲null的時候,pre就爲最後一個節點了,但是鏈表已經反轉完畢,pre就是反轉後鏈表的第一個節點
//直接輸出pre就是我們想要得到的反轉後的鏈表
return pre;
}
};
16 合併兩個有序鏈表
輸入兩個單調遞增的鏈表,輸出兩個鏈表合成後的鏈表,當然我們需要合成後的鏈表滿足單調不減規則。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};
沒有什麼可說的
就像合併兩個有序數組
*/
class Solution {
public:
ListNode* Merge(ListNode* p1, ListNode* p2)
{
auto d = new ListNode(-1);//虛擬頭結點
auto t = d;
while(p1&&p2){
if(p1->val < p2->val){//小的先排
t->next = p1;
p1 = p1->next;
t = t->next;
}
else{
t->next = p2;
p2 = p2->next;
t = t->next;
}
}
if(p1)t->next = p1;//如果還有直接接到t的屁股後面
if(p2)t->next = p2;
return d->next;
}
};
17 樹的子結構
輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
算法
(二叉樹,遞歸) O(nm)O(nm)
代碼分爲兩個部分:
遍歷樹A中的所有非空節點R;
判斷樹A中以R爲根節點的子樹是不是包含和樹B一樣的結構,且我們從根節點開始匹配;
對於第一部分,我們直接遞歸遍歷樹A即可,遇到非空節點後,就進行第二部分的判斷。
對於第二部分,我們同時從根節點開始遍歷兩棵子樹:
如果樹B中的節點爲空,則表示當前分支是匹配的,返回true;
如果樹A中的節點爲空,但樹B中的節點不爲空,則說明不匹配,返回false;
如果兩個節點都不爲空,但數值不同,則說明不匹配,返回false;
否則說明當前這個點是匹配的,然後遞歸判斷左子樹和右子樹是否分別匹配即可;
時間複雜度
最壞情況下,我們對於樹A中的每個節點都要遞歸判斷一遍,每次判斷在最壞情況下需要遍歷完樹B中的所有節點。
所以時間複雜度是 O(nm)O(nm),其中 nn 是樹A中的節點數, mm 是樹B中的節點數。
參考大神:https://www.acwing.com/solution/AcWing/content/745/
};*/
class Solution {
public:
bool isSubtree(TreeNode* r1, TreeNode* r2){
if(!r2)return true;
if(!r1 || r1->val != r2->val)return false;
return isSubtree(r1->left, r2->left) && isSubtree(r1->right, r2->right);
}
bool HasSubtree(TreeNode* r1, TreeNode* r2)
{
if(!r1 || !r2)return false;
if(isSubtree(r1, r2))return true;
return HasSubtree(r1->left, r2) || HasSubtree(r1->right, r2);
}
};
18 二叉樹的鏡像
操作給定的二叉樹,將其變換爲源二叉樹的鏡像。
輸入描述:
二叉樹的鏡像定義:源二叉樹
8
/ \
6 10
/ \ / \
5 7 9 11
鏡像二叉樹
8
/ \
10 6
/ \ / \
11 9 7 5
/*
沒什麼好說的
直接交換當前結點的左右孩子
如果當前結點還有左右孩子
分別遞歸交換
直到遞歸結束
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
void Mirror(TreeNode *r) {
if(!r)return;//沒有左右孩子則直接返回
swap(r->left,r->right);//交換左右孩子
Mirror(r->left);//遞歸,繼續遍歷左子樹
Mirror(r->right);//遞歸,繼續遍歷右子樹
}
};
19 順時針打印矩陣
輸入一個矩陣,按照從外向裏以順時針的順序依次打印出每一個數字,例如,如果輸入如下4 X 4矩陣: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 則依次打印出數字1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10.
/**
分別得到行和列的開始和末尾索引
首先遍歷完當前行後 開始行++
然後列從開始行索引開始遍歷後 列--
末尾行從末尾列往回遍歷
開始列從末尾行到開始行往回遍歷
直到list長度和size一樣 結束循環
由於沒有其他空間,不算題目要的list的話 空間複雜度O(1) 時間複雜度O(n^2)
**/
class Solution {
public:
vector<int> printMatrix(vector<vector<int> > matrix) {
vector<int> list;
if(matrix.size() == 0) return list;
//水平行的最開始的索引 和 最後的索引
int h_first_index = 0, h_last_index = matrix.size() - 1;
//垂直列的最開始的索引 和 最後的索引
int v_first_index = 0, v_last_index = matrix[0].size() - 1;
//數組的長度
int size = (h_last_index + 1)*(v_last_index + 1);
//是否遍歷結束
bool flag = true;
while(flag){
// 先算開始第一行
for(int i = v_first_index;i <= v_last_index;i++) {
list.push_back(matrix[h_first_index][i]);
if(list.size() >= size){
flag = false;
break;
}
}
h_first_index ++;
if(list.size() >= size){
break;
}
// 再算最後一列
for(int i=h_first_index;i<=h_last_index;i++) {
list.push_back(matrix[i][v_last_index]);
if(list.size() >= size){
flag = false;
break;
}
}
v_last_index --;
if(list.size()>=size){
break;
}
// 再算最後一行
for(int i=v_last_index;i>=v_first_index;i--) {
list.push_back(matrix[h_last_index][i]);
if(list.size() >= size){
flag = false;
break;
}
}
h_last_index --;
if(list.size()>=size){
break;
}
// 最後算開始第一列
for(int i = h_last_index;i >= h_first_index;i--) {
list.push_back(matrix[i][v_first_index]);
if(list.size() >= size){
flag = false;
break;
}
}
v_first_index ++;
if(list.size() >= size){
break;
}
}
return list;
}
};
另外,我還寫了一個遞歸的版本可以用一個bool數組來判斷是否遍歷,這個消耗一點空間,但是代碼看起來很清晰,代碼:
int di[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};
vector<int> res;
vector<vector<bool>> used;
bool isArea(int x, int y, int row, int col){
return row>=0 && row<x && col>=0 && col<y;
}
void dfs(int d, const vector<vector<int> > &matrix, int x, int y, int row, int col){
int dx = di[d%4][0], dy = di[d%4][1];
if(isArea(x,y,row,col) && !used[row][col]){
int tempRow = row, tempCol = col;
row += dx, col += dy;
if(!isArea(x,y,row,col) || used[row][col] ){
if(res.size() + 1 == x * y){//只有最後一個元素了,直接添加並返回
res.push_back(matrix[tempRow][tempCol]);
return;
}
dfs(d + 1, matrix, x, y, row - dx, col - dy);
}else{
cout<<matrix[tempRow][tempCol]<<endl;
res.push_back(matrix[tempRow][tempCol]);
used[tempRow][tempCol] = true;
dfs(d, matrix, x, y, row, col);
}
}
}
vector<int> printMatrix(vector<vector<int> > &matrix) {
if(!matrix.size() || !matrix[0].size()) return res;
int x = matrix.size(),y = matrix[0].size();
used = vector<vector<bool>>(x, vector<bool>(y, false));
dfs(0, matrix, x, y, 0, 0);
return res;
}
也可以參考yxc大神的方法。
class Solution {
public:
vector<int> printMatrix(vector<vector<int>>& matrix) {
vector<int> res;
if (matrix.empty()) return res;
int n = matrix.size(), m = matrix[0].size();
vector<vector<bool>> st(n, vector<bool>(m, false));
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int x = 0, y = 0, d = 1;
for (int k = 0; k < n * m; k ++ )
{
res.push_back(matrix[x][y]);
st[x][y] = true;
int a = x + dx[d], b = y + dy[d];
if (a < 0 || a >= n || b < 0 || b >= m || st[a][b])
{
d = (d + 1) % 4;
a = x + dx[d], b = y + dy[d];
}
x = a, y = b;
}
return res;
}
};
參考鏈接:https://www.acwing.com/solution/AcWing/content/748/
20 包含min函數的棧
定義棧的數據結構,請在該類型中實現一個能夠得到棧中所含最小元素的min函數(時間複雜度應爲O(1))。
class Solution {
public:
/**
我們除了維護基本的棧結構之外,還需要維護一個單調棧,來實現返回最小值的操作。
下面介紹如何維護單調棧:
當我們向棧中壓入一個數時,如果該數 ≤≤ 單調棧的棧頂元素,則將該數同時壓入單調棧中;否則,不壓入,這是由於棧具有先進後出性質,所以在該數被彈出之前,棧中一直存在一個數比該數小,所以該數一定不會被當做最小數輸出。
當我們從棧中彈出一個數時,如果該數等於單調棧的棧頂元素,則同時將單調棧的棧頂元素彈出。
單調棧由於其具有單調性,所以它的棧頂元素,就是當前棧中的最小數。
時間複雜度
四種操作都只有常數次入棧出棧操作,所以時間複雜度都是 O(1)O(1).
參考鏈接:https://www.acwing.com/solution/AcWing/content/749/
*/
stack<int> m;
stack<int> s;
void push(int value) {
s.push(value);
if(m.empty() || m.top() >= value)
m.push(value);
}
void pop() {
if(m.top() == s.top()) m.pop();
s.pop();
}
int top() {
return s.top();
}
int min() {
return m.top();
}
};