演算法題2
1. 變態跳臺階
一隻青蛙一次可以跳上1級臺階,也可以跳上2級……它也可以跳上n級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
分析:
2. 矩形覆蓋
我們可以用2*1的小矩形橫著或者豎著去覆蓋更大的矩形。請問用n個2*1的小矩形無重疊地覆蓋一個2*n的大矩形,總共有多少種方法?
分析
如果豎著放,則問題縮減為對n-1塊的填充;如果橫著放,縮減為n-2塊的填充。
3. 給一個數a,計算平方根
分析:
牛頓法
4. 樹的子結構
輸入兩棵二叉樹A,B,判斷B是不是A的子結構。(ps:我們約定空樹不是任意一個樹的子結構)
分析:
public class Solution {
public boolean HasSubtree(TreeNode root1,TreeNode root2) {
if(root1 == null || root2 == null)
return false;
//要麼當前結點已經是子樹 要麼當前結點的左孩子或右孩子存在子樹
return IsSubtree(root1,root2) || HasSubtree(root1.left,root2) || HasSubtree(root1.right,root2);
}
public boolean IsSubtree(TreeNode root1,TreeNode root2){
if(root2 == null)
return true;
if(root1 == null)
return false;
if(root1.val == root2.val)
return IsSubtree(root1.left,root2.left) && IsSubtree(root1.right,root2.right);
else
return false;
}
}
5. 之字形列印樹
分析:
構建兩個棧,st2先壓左節點後壓右節點,st1先壓右節點後壓左節點。
6. 二叉搜尋樹與雙向連結串列
分析:
指標的引用:
當我們把一個指標做為引數傳一個方法時,其實是把指標的複本傳遞給了方法,也可以說傳遞指標是指標的值傳遞。
如果我們在方法內部修改指標會出現問題,在方法裡做修改只是修改的指標的copy而不是指標本身,原來的指標還保留著原來的值。
void restructList(TreeNode* root, TreeNode *(&preNode)) {
if (!root) {
return;
}
restructList(root->left, preNode);
if (preNode) {
root->left = preNode;
preNode->right = root;
}
else {
root->left = NULL;
}
preNode = root;
restructList(root->right, preNode);
}
TreeNode* Convert(TreeNode* pRootOfTree){
if (!pRootOfTree)
{
return pRootOfTree;
}
TreeNode *pre = NULL;
restructList(pRootOfTree,pre);
TreeNode *cur = pRootOfTree;
while (cur->left){
cur = cur->left;
}
return cur;
}
後面pre的改變要返回去給前一次的呼叫
7. 最小的k個數
分析:
- 快排的方法:
尋找到第k個位置正確的數,前面的數未最小的k個數。
若index>k-1,則向前找。
若index<k-1,則向後找。
- 堆排序方法:
建立一個k元素的大根堆,如果當前值比堆頂元素大則跳過,如果當前值比堆頂元素小則替換掉堆頂元素。
8. 陣列中的逆序對
分析:
先把陣列分隔成子陣列,先統計出子陣列內部的逆序對的數目,然後再統計出兩個相鄰子陣列之間的逆序對的數目。在統計逆序對的過程中,還需要對陣列進行排序,由於已經統計了這兩對子陣列內部的逆序對,因此需要把這兩對子陣列進行排序,避免在之後的統計過程中重複統計。,其實這個排序過程就是歸併排序的思路。
9. 醜數
把只包含因子2、3和5的數稱作醜數(Ugly Number)。例如6、8都是醜數,但14不是,因為它包含因子7。 習慣上我們把1當做是第一個醜數。求按從小到大的順序的第N個醜數。
分析:
第一個數為1,然後在1的基礎上分別乘2,3,5選最小的數。例如2為所得最小的數,則下一次比較的最小數時2的乘子為2,但是3,5的乘子依舊為1。
int GetUglyNumber_Solution(int index) { //返回第index個醜數,由2,3,5相乘組成的數
if (index < 7) {
return index;
}
vector<int> ans(index,0);
ans[0] = 1;
int index2 = 0;
int index3 = 0;
int index5 = 0;
for (int i = 1; i < index; ++i) {
ans[i] = std::min(ans[index2] * 2, std::min(ans[index3] * 3, ans[index5] * 5));
if (ans[i] == ans[index2] * 2)
++index2;
if (ans[i] == ans[index3] * 3)
++index3;
if (ans[i] == ans[index5] * 5)
++index5;
}
return ans[index - 1];
}
10. 和為S的連續正數序列
輸出所有和為S的連續正數序列。序列內按照從小至大的順序,序列間按照開始數字從小到大的順序。
分析:
令n為序列內元素的個數。
如果n為奇數,S%n=0,則可以劃分。
如果n為偶數,(S%n)*2==n,則可以劃分。
n的上限:(1+n)∗n/2=S,則可得$ n \le \sqrt{2S}$。
11. 約瑟夫換問題(圓圈中最後剩下的數)
每年六一兒童節,牛客都會準備一些小禮物去看望孤兒院的小朋友,今年亦是如此。HF作為牛客的資深元老,自然也準備了一些小遊戲。其中,有個遊戲是這樣的:首先,讓小朋友們圍成一個大圈。然後,他隨機指定一個數m,讓編號為0的小朋友開始報數。每次喊到m-1的那個小朋友要出列唱首歌,然後可以在禮品箱中任意的挑選禮物,並且不再回到圈中,從他的下一個小朋友開始,繼續0…m-1報數…這樣下去…直到剩下最後一個小朋友,可以不用表演,並且拿到牛客名貴的“名偵探柯南”典藏版(名額有限哦!!_)。請你試著想下,哪個小朋友會得到這份禮品呢?(注:小朋友的編號是從0到n-1)
分析:
原始 0 1 2 3 4 5 6 7 8 9
舊環 0 1 2 ~ 4 5 6 7 8 9
新環 6 7 8 ~ 0 1 2 3 4 5
舊環到新環的對映: (舊環中編號-最大報數值)%舊總人數
新環到舊環的對映: ( 新環中的數字 + 最大報數值 )% 舊總人數
12. 累加1+2+…
求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等關鍵字及條件判斷語句(A?B:C)。
分析:
利用A&&B的性質,前部分A不成立則不執行後部分B。
int Sum_Solution(int n) {
int result = n;
n && (result+=Sum_Solution(n-1));
return result;
}
13. 不用加減乘除做加法
寫一個函式,求兩個整數之和,要求在函式體內不得使用+、-、*、/四則運算子號。
分析:
a.兩個數相加,如果忽略掉進位相當於按位異或,010111 = 101。
b.進位項相當於兩個數按位與&並左移一位<<1,(010&111)<<1=0100。
c.如果進位項不等於0,則重複a.b過程,如果進位項為0,則結果為a步驟的結果。
int Add(int num1, int num2){//計算兩個數相加和,不能用加減乘除運算
int result = num1 ^ num2;
int carry = (num1&num2) << 1;
while (carry){
int tem = result;
result = carry ^ result;
carry = (carry&tem) << 1;
}
return result;
}
14. 陣列中重複的數字
在一個長度為n的數組裡的所有數字都在0到n-1的範圍內。 陣列中某些數字是重複的,但不知道有幾個數字是重複的。也不知道每個數字重複幾次。請找出陣列中任意一個重複的數字。 例如,如果輸入長度為7的陣列{2,3,1,0,2,5,3},那麼對應的輸出是第一個重複的數字2。
分析:
1.把當前序列當成是一個下標和下標對應值是相同的陣列;
2.遍歷陣列,判斷當前位的值和下標是否相等: 2.1. 若相等,則遍歷下一位; 2.2. 若不等,則將當前位置i上的元素和a[i]位置上的元素比較:若它們相等,則成功!若不等,則將它們兩交換。換完之後a[i]位置上的值和它的下標是對應的,但i位置上的元素和下標並不一定對應;重複2.2的操作,直到當前位置i的值也為i,將i向後移一位,再重複2.
bool duplicate(int numbers[], int length, int* duplication) {
for (int i = 0; i < length; ++i) {
while (numbers[i] != i)
{
if (numbers[i] == numbers[numbers[i]]) {
*duplication = numbers[i];
return true;
}
else {
swap(numbers[i], numbers[numbers[i]]);
}
}
}
return false;
}
15. 匹配正則表示式
請實現一個函式用來匹配包括’.‘和’‘的正則表示式。模式中的字元’.‘表示任意一個字元,而’'表示它前面的字元可以出現任意次(包含0次)。 在本題中,匹配是指字串的所有字元匹配整個模式。例如,字串"aaa"與模式"a.a"和"abaca"匹配,但是與"aa.a"和"ab*a"均不匹配。
bool matchCore(char *str, char *pattern) {
if (*str == '\0' && *pattern == '\0') {
return true;
}
if (*str != '\0' && *pattern == '\0') {
return false;
}
if (*str == '\0'&&*pattern != '\0'&&*(pattern + 1) != '*') {
return false;
}
if (*(pattern + 1) == '*') {
if (*pattern == *str || (*pattern == '.'&&*str != '\0')) {
return matchCore(str, pattern + 2) || matchCore(str + 1, pattern);
}
else {
return matchCore(str, pattern + 2);
}
}
if (*pattern == *str || (*pattern == '.'&&*str != '\0')) {
return matchCore(str + 1, pattern + 1);
}
return false;
}
16. 映象二叉樹
判斷一顆二叉樹是不是映象的。
bool assistSymmetrical(TreeNode* left, TreeNode* right) {
if (left == NULL) {
return right == NULL;
}
if (right == NULL) {
return false;
}
if (left->val != right->val) {
return false;
}
return assistSymmetrical(left->right, right->left) && assistSymmetrical(left->left, right->right);
}
bool isSymmetrical(TreeNode* pRoot)//判斷是否為映象二叉樹
{
if (pRoot == NULL) {
return true;
}
return assistSymmetrical(pRoot->left, pRoot->right);
}
17. 資料流中的中位數
如何得到一個數據流中的中位數?如果從資料流中讀出奇數個數值,那麼中位數就是所有數值排序之後位於中間的數值。如果從資料流中讀出偶數個數值,那麼中位數就是所有數值排序之後中間兩個數的平均值。我們使用Insert()方法讀取資料流,使用GetMedian()方法獲取當前讀取資料的中位數。
分析:
1.建立一個大根堆,一個小根堆。
2.當大根堆中元素數為小根堆元素+2時,將大根堆的堆頂取出,放入小根堆。
3.迴圈,如果大根堆堆頂大於小跟堆堆頂,則互換堆頂,直到大根堆堆頂小於等於小根堆堆頂。
4.若元素數為奇數,返回大根堆堆頂,如果為偶數,返回大根堆堆頂+小跟堆堆頂的平均值。
注意:建堆程式碼以及刪除元素程式碼。
class Solution {//資料流的中位數
public:
void Insert(int num){
dataStream.push_back(num);
}
double GetMedian(){
std::multiset<int, std::greater<int>> bigH;
std::multiset<int, std::less<int>> littleH;
int num = dataStream.size();
for (int i = 0; i < num; ++i) {
bigH.insert(dataStream[i]);
if (bigH.size() == littleH.size() + 2) {
int tem = *bigH.begin();
littleH.insert(tem);
bigH.erase(bigH.begin());
}
while (!bigH.empty()&&!littleH.empty()&&*bigH.begin()>*littleH.begin()){
int big = *bigH.begin();
bigH.erase(bigH.begin());
int little = *littleH.begin();
littleH.erase(littleH.begin());
littleH.insert(big);
bigH.insert(little);
}
}
if (num % 2 == 1) {
return *bigH.begin();
}
else {
return (*bigH.begin() + *littleH.begin()) / 2.0;
}
}
private:
vector<int> dataStream;
};
18. 之字形列印二叉樹
請實現一個函式按照之字形列印二叉樹,即第一行按照從左到右的順序列印,第二層按照從右至左的順序列印,第三行按照從左到右的順序列印,其他行以此類推。
分析:
設計兩個棧,st1先壓右節點再壓左節點,st2先壓左節點再壓右節點。
vector<vector<int> > Print(TreeNode* pRoot) {//之字形列印樹
vector<vector<int>> result;
if (pRoot == NULL) {
return result;
}
stack<TreeNode *> st1;
stack<TreeNode *> st2;
st1.push(pRoot);
while (!st1.empty() || !st2.empty()) {
vector<int> temVec;
if (!st1.empty()) {
while (!st1.empty()){
TreeNode *cur = st1.top();
st1.pop();
temVec.push_back(cur->val);
if (cur->left) {
st2.push(cur->left);
}
if (cur->right) {
st2.push(cur->right);
}
}
}
else {
while (!st2.empty()) {
TreeNode *cur = st2.top();
st2.pop();
temVec.push_back(cur->val);
if (cur->right) {
st1.push(cur->right);
}
if (cur->left) {
st1.push(cur->left);
}
}
}
result.push_back(temVec);
}
return result;
}
19. 把二叉樹列印成多行
分析:
設計兩個指標,一個儲存當前最新壓入佇列的節點,一個儲存當前列印行最後一個節點。
vector<vector<int> > Print(TreeNode* pRoot) {//按行列印二叉樹
vector<vector<int>> result;
if (pRoot == NULL) {
return result;
}
queue<TreeNode*> helpQe;
helpQe.push(pRoot);
TreeNode *rowLast = pRoot;
TreeNode *temLast = pRoot;
vector<int> rowVec;
while (!helpQe.empty()){
TreeNode *tem = helpQe.front();
helpQe.pop();
rowVec.push_back(tem->val);
if (tem->left != NULL) {
helpQe.push(tem->left);
temLast = tem->left;
}
if (tem->right != NULL) {
helpQe.push(tem->right);
temLast = tem->right;
}
if (rowLast == tem) {
rowLast = temLast;
result.push_back(rowVec);
rowVec.clear();
}
}
return result;
}
20. 序列化二叉樹
分析:
注意序列化若遇到NULL返回#,反序列化時,遇到#與值用完,都返回NULL。
void serializeHelp(TreeNode *root, string &str) {
if (root == NULL) {
str += "#!";
return;
}
str += (std::to_string(root->val) + "!");
serializeHelp(root->left, str);
serializeHelp(root->right, str);
}
char* Serialize(TreeNode *root) {
if (root == NULL) {
char *ans = new char;
*ans = '\0';
return ans;
}
string str;
serializeHelp(root, str);
char *result = new char[str.size() + 1];
for (int i = 0; i < str.size(); ++i) {
result[i] = str[i];
}
result[str.size()] = '\0';
return result;
}
TreeNode* deserializeHelp(queue<string> &nodeValue) {
if (nodeValue.empty()) {
return NULL;
}
string tem = nodeValue.front();
nodeValue.pop();
if (tem == "#") {
return NULL;
}
else {
TreeNode *newNode = new TreeNode(atoi(tem.c_str()));
newNode->left = deserializeHelp(nodeValue);
newNode->right = deserializeHelp(nodeValue);
return newNode;
}
}
TreeNode* Deserialize(char *str) {
if (*str == '\0') {
return NULL;
}
queue<string> nodeValue;
string tem;
while (*str!='\0'){
if (*str != '!') {
tem += *str;
++str;
}
else {
nodeValue.push(tem);
tem.clear();
++str;
}
}
tem = nodeValue.front();
nodeValue.pop();
TreeNode *head = new TreeNode(atoi(tem.c_str()));
head->left = deserializeHelp(nodeValue);
head->right = deserializeHelp(nodeValue);
return head;
}
21. 二叉搜尋樹的第k個節點
分析:
設定一個引用值k,用中序遍歷的方法,每次遍歷到一個節點做k–處理.若k=0則返回當前節點。引用的k值能夠在任何一層遞迴中判斷是否達到終止條件。
注意:終止條件及路徑返回的完整性。
TreeNode* kthNode(TreeNode* pRoot, int &k) {
if (pRoot == NULL) {
return NULL;
}
TreeNode *left = kthNode(pRoot->left, k);
if (k == 1) {
return left;
}
--k;
if (k == 1) {
return pRoot;
}
TreeNode *right = kthNode(pRoot->right, k);
if (k == 1) {
return right;
}
return NULL;
}
TreeNode* KthNode(TreeNode* pRoot, int k){//找到搜尋二叉樹中第k大的數
if (pRoot == NULL) {
return NULL;
}
int w = k + 1;
return kthNode(pRoot, w);
}
22. 滑動視窗的最大值
給定一個數組和滑動視窗的大小,找出所有滑動窗口裡數值的最大值。例如,如果輸入陣列{2,3,4,2,6,2,5,1}及滑動視窗的大小3,那麼一共存在6個滑動視窗,他們的最大值分別為{4,4,6,6,6,5}; 針對陣列{2,3,4,2,6,2,5,1}的滑動視窗有以下6個: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
分析:
時間複雜度可以做到O(N).
首先建立一個雙向佇列qmax記錄陣列的下標,若當前數為arr[i],放入規則為
1.如果qmax為空,將i放入qmax尾部。
2.如果qmax不為空,qmax隊尾下標為j,若arr[i]大於qmax的隊尾,則彈出qmax隊尾,直到qmax隊尾大於等於arr[i]或qmax為空。
3.判斷qmax隊首是否滿足視窗範圍要求,若j<=i-Wsize,則迴圈彈出qmax隊首。
4.qmax隊首元素為當前視窗的最大值。
注意:雙向佇列的各種操作。
vector<int> maxInWindows(const vector<int>& num, unsigned int size){//滑動視窗的最大值
vector<int> result;
if (num.size() < size) {
return result;
}
std::deque<int> qMax;
for (int i = 0; i < num.size(); ++i) {
while (!qMax.empty() && num[qMax.back()] <= num[i]) {
qMax.pop_back();
}
qMax.push_back(i);
while (!qMax.empty() && (qMax.front() + size <= i)) {
cout << qMax.front() << " " << i - size << endl;
qMax.pop_front();
}
if (i >= size - 1) {
result.push_back(num[qMax.front()]);
}
}
return result;
}
23. 矩陣中的路徑
請設計一個函式,用來判斷在一個矩陣中是否存在一條包含某字串所有字元的路徑。路徑可以從矩陣中的任意一個格子開始,每一步可以在矩陣中向左,向右,向上,向下移動一個格子。如果一條路徑經過了矩陣中的某一個格子,則之後不能再次進入這個格子。 例如 a b c e s f c s a d e e 這樣的3 X 4 矩陣中包含一條字串"bcced"的路徑,但是矩陣中不包含"abcb"路徑,因為字串的第一個字元b佔據了矩陣中的第一行第二個格子之後,路徑不能再次進入該格子。
分析:
遞迴尋找路徑,每次都檢查上下左右是否滿足。結果相或。設計一個引用的矩陣或向量來記錄元素是否被使用過,記得恢復。
bool findPath(vector<int> history, char *matrix, int rows, int cols, char* str, int index, int row, int col) {
if (str[index] == '\0') {
return true;
}
bool result = false;
char tem = str[index];
if (row - 1 >= 0 && matrix[col + (row - 1)*cols] == tem && history[col + (row - 1)*cols] != 1) {
history[col + (row - 1)*cols] = 1;
result |= findPath(history, matrix, rows, cols, str, index+1, row-1, col);
history[col + (row - 1)*cols] = 0;
}
if (row + 1 < rows && matrix[col + (row + 1)*cols] == tem && history[col + (row + 1)*cols] != 1) {
history[col + (row + 1)*cols] = 1;
result |= findPath(history, matrix, rows, cols, str, index + 1, row + 1, col);
history[col + (row + 1)*cols] = 0;
}
if (col - 1 >= 0 && matrix[col - 1 + row * cols] == tem && history[col - 1 + row * cols] != 1) {
history[col - 1 + row * cols] = 1;
result |= findPath(history, matrix, rows, cols, str, index + 1, row, col - 1);
history[col - 1 + row * cols] = 0;
}
if (col + 1 < cols && matrix[col + 1 + row * cols] == tem && history[col + 1 + row * cols] != 1) {
history[col + 1 + row * cols] = 1;
result |= findPath(history, matrix, rows, cols, str, index + 1, row, col + 1);
history[col + 1 + row * cols] = 0;
}
return result;
}
bool hasPath(char* matrix, int rows, int cols, char* str){//尋找矩陣中的路徑
if (matrix == NULL) {
return false;
}
vector<int> history(rows*cols, 0);
bool result = false;
char tem = str[0];
for (int i = 0; i < cols; ++i) {
for (int j = 0; j < rows; ++j) {
if (matrix[i + j * cols] == tem && history[i + j * cols] != 1) {
//cout << "col: " << i << " " << "row: " << j << " char:" << matrix[i + j * cols] << endl;
history[i + j * cols] = 1;
result |= findPath(history, matrix, rows, cols, str, 1, j, i);
history[i + j * cols] = 0;
}
}
}
return result;
}
24. 機器人的運動範圍
地上有一個m行和n列的方格。一個機器人從座標0,0的格子開始移動,每一次只能向左,右,上,下四個方向移動一格,但是不能進入行座標和列座標的數位之和大於k的格子。 例如,當k為18時,機器人能夠進入方格(35,37),因為3+5+3+7 = 18。但是,它不能進入方格(35,38),因為3+5+3+8 = 19。請問該機器人能夠達到多少個格子?
分析:
設計一個history矩陣或向量,記錄是否走個這個路徑。
設計一個遞迴函式,如果座標滿足閾值要求,則返回分別向上下左右能走的格子數+1,如果不滿足閾值要求或矩陣範圍要求,返回0,如果路徑已經被走過,返回0。
int twoNumberBitSum(int a, int b) {
int result = 0;
while (a>0){
result += a % 10;
a = a / 10;
}
while (b>0){
result += b % 10;
b = b / 10;
}
return result;
}
int findMovingCount(int threshold, int rows, int cols, vector<int> &history, int row, int col) {
if (twoNumberBitSum(row, col) > threshold || row < 0 || row >= rows || col < 0 || col >= cols) {
return 0;
}
if (history[col + cols * row] == 1) {
return 0;
}
history[col + cols * row] = 1;
return findMovingCount(threshold, rows, cols, history, row - 1, col) + findMovingCount(threshold, rows, cols, history, row + 1, col)+
findMovingCount(threshold, rows, cols, history, row, col - 1) + findMovingCount(threshold, rows, cols, history, row, col + 1) + 1;
}
int movingCount(int threshold, int rows, int cols){//機器人的運動範圍
if (threshold < 0) {
return 0;
}
vector<int> history(rows*cols, 0);
return findMovingCount(threshold, rows, cols, history, 0, 0);
}
25. 圖的連通分量
把一個圖的最大連通子圖稱為它的連通分量,連通分量有如下特點:
1)是子圖;
2)子圖是連通的;
3)子圖含有最大頂點數。
“最大連通子圖”指的是無法再擴充套件了,不能包含更多頂點和邊的子圖。顯然,對於連通圖來說,它的最大連通子圖就是其本身,連通分量也是其本身。
26. 圖的最短路徑演算法
深度優先搜尋
void dfs(int cur, int dest, int curLen, vector<vector<int>> edge, int &minLen, vector<int> &mark, vector<int> &nowPath, vector<int> &resultPath)
{
if(curLen > minLen) return;
if(cur == dest){
if(minLen > curLen){
resultPath = nowPath;
minLen = curLen;
}
return;
}
for(int i = 0; i < n; ++i){
if(mark[i] == 0 && edge[cur][i] != 0 && edge[cur][i] != inf){
mark[i] = 1;
nowPath.push_back(i);
newLen = curLen + edge[cur][i];
dfs(i, dest, newLen, edge, minLen, mark, nowPath, resultPath)
nowPath.pop_back(i);
mark[i] = 0;
}
}
}
Dijkstra 迪傑斯特拉演算法(解決單源最短路徑),時間複雜度O(N*logN)
基本思想:每次找到離源點(如1號結點)最近的一個頂點,然後以該頂點為中心進行擴充套件,最終得到源點到其餘所有點的最短路徑。
基本步驟:
1,設定標記陣列book[]:將所有的頂點分為兩部分,已知最短路徑的頂點集合P和未知最短路徑的頂點集合Q,很顯然最開始集合P只有源點一個頂點。book[i]為1表示在集合P中;
2,設定最短路徑陣列dst[]並不斷更新:初始狀態下,令dst[i] = edge[s][i] (s為源點,edge為鄰接矩陣),很顯然此時dst[s]=0,book[s]=1。此時,在集合Q中可選擇一個離源點s最近的頂點u加入到P中。並依據以u為新的中心點,對每一條邊進行鬆弛操作(鬆弛是指由結點s–>j的途中可以經過點u,並令dst[j]=min{dst[j], dst[u]+edge[u][j]}),並令book[u]=1;
3,在集合Q中再次選擇一個離源點s最近的頂點v加入到P中。並依據v為新的中心點,對每一條邊進行鬆弛操作(即dst[j]=min{dst[j], dst[v]+edge[v][j]}),並令book[v]=1;
4,重複3,直至集合Q為空。
void dfs(int n, vector<vector<int>> edge, int k){
vector<int> book(n, 0);
vector<int> dst(n, 0);
book[k] = 1;
for(int i = 0; i < n; ++i){
dst[i] = edge[k][i];
}
for(int m = 0; m < n-1; ++m){
int minDst = INT32_MAX;
int minLabel = -1;
for(int i = 0; i< n; ++i){
if(book[i] == 0 && dst[i] < minDst){
minDst = dst[i];
minLabel = i;
}
}
book[minLabel] = 1;
for(int i = 0; i < n; ++i){
if(book[i] == 0 && dst[i] > dst[minLabel] + edge[minLabel][i]){
dst[i] = dst[minLabel] + edge[minLabel][i];
}
}
}
}
Floyd弗洛伊德演算法(解決多源最短路徑):時間複雜度O(n^ 3),空間複雜度O(n^2)
基本思想:最開始只允許經過1號頂點進行中轉,接下來只允許經過1號和2號頂點進行中轉…允許經過1~n號所有頂點進行中轉,來不斷動態更新任意兩點之間的最短路程。即求從i號頂點到j號頂點只經過前k號點的最短路程。
分析如下:
1,首先構建鄰接矩陣Floyd[n+1][n+1],假如現在只允許經過1號結點,求任意兩點間的最短路程,很顯然Floyd[i][j] = min{Floyd[i][j], Floyd[i][1]+Floyd[1][j]}.
2,接下來繼續求在只允許經過1和2號兩個頂點的情況下任意兩點之間的最短距離,在已經實現了從i號頂點到j號頂點只經過前1號點的最短路程的前提下,現在再插入第2號結點,來看看能不能更新更短路徑,故只需在步驟1求得的Floyd[n+1][n+1]基礎上,進行Floyd[i][j] = min{Floyd[i][j], Floyd[i][2]+Floyd[2][j]};…
3,很顯然,需要n次這樣的更新,表示依次插入了1號,2號…n號結點,最後求得的Floyd[n+1][n+1]是從i號頂點到j號頂點只經過前n號點的最短路程。故核心程式碼如下:
vector<vector<int>> floyd = edge
for(int k = 0; k < n; ++k){
for(int i = 0; i < n; ++i){
for(int j = 0; j < n; ++j){
if(floyd[i][j] < floyd[i][k] + floyd[k][j]){
floyd[i][j] = floyd[i][k] + floyd[k][j];
}
}
}
}
Bellman-Ford演算法
貝爾曼-福特演算法,它的原理是對圖進行V-1次鬆弛操作,得到所有可能的最短路徑。其優於Dijkstra演算法的方面是邊的權值可以為負數、實現簡單,缺點是時間複雜度過高,高達O(VE)。
第一,初始化所有點。每一個點儲存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
第二,進行迴圈,迴圈下標為從1到n-1(n等於圖中點的個數)。在迴圈內部,遍歷所有的邊,進行鬆弛計算。
第三,遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況:
d(v)>d(u) + w(u,v)
則返回false,表示途中存在從源點可達的權為負的迴路。
之所以需要第三部分的原因,是因為,如果存在從源點可達的權為負的迴路。則應為無法收斂而導致不能求出最短路徑。
可知,Bellman-Ford演算法尋找單源最短路徑的時間複雜度為O(VE).
class Edge{
int src, dest, weight;
};
class Graph{
int v,e;
Edge *edge;
};
void BF(int k, Graph *graph){
int v = graph->v;
int e = graph->e;
vector<int> dst(v, INT32_MAX);
dst[k] = 0;
for(int k = 1; k <= v-1; ++k){
for(int i = 0; i < e; ++i){
int st = graph->edge[i].src;
int end = graph->edge[i].dest;
if(v[end] > v[st] + graph->edge[i].weight){
v[end] = v[st] + graph->edge[i].weight;
}
}
}
for(int i = 0; i < e; ++i){
int st = graph->edge[i].src;
int end = graph->edge[i].dest;
if(v[end] > v[st] + graph->edge[i].weight){
return false;
}
}
return true;
}
27. 最短路徑問題
給你n個點,m條無向邊,每條邊都有長度d和花費p,給你起點s,終點t,要求輸出起點到終點的最短距離及其花費,如果最短距離有多條路線,則輸出花費最少的。
costMatrix[n][n], lenMatrix[n][n]
分析:
設計兩個向量,一個儲存最短距離dst,一個儲存最少花費spend。
void dfs(int n, int k, vector<vector<int>> edge, vector<vector<int>> cost){
vector<int> spend(n, 0);
vector<int> dst(n, 0);
vector<int> book(n, 0);
book[k] = 1;
for(int i = 0;i < n; ++i){
spend[i] = cost[k][i];
dst[i] = edge[k][i];
}
for(int m = 0; m < n-1; ++m){
int minLen = INT32_MAX;
int minSpend = INT32_MAX;
int minLabel = -1;
for(int i = 0; i < n; ++i){
if(book[i] == 0 && (minLen > dst[i] || (minLen == dst[i] && minSpend > cost[i]))){
minLabel = i;
minLen = dst[i];
minSpend = cost[i];
}
}
book[minLabel] = 1;
for(int i = 0; i < n; ++i){
if(book[i] == 0 && (dst[i] > dst[minLabel] + edge[minLabel][i] ||
(dst[i] == dst[minLabel] + edge[minLabel][i] && spend[i] >spend[minLabel] + cost[minLabel][i]))){
dst[i] = dst[minLabel] + edge[minLabel][i];
spend[i] = spend[minLabel] + cost[minLabel][i];
}
}
}
}
28. 旋轉連結串列
給定一個連結串列,旋轉連結串列,將連結串列每個節點向右移動 k 個位置,其中 k 是非負數。
輸入: 1->2->3->4->5->NULL, k = 2
輸出: 4->5->1->2->3->NULL
解釋:
向右旋轉 1 步: 5->1->2->3->4->NULL
向右旋轉 2 步: 4->5->1->2->3->NULL
輸入: 0->1->2->NULL, k = 4
輸出: 2->0->1->NULL
解釋:
向右旋轉 1 步: 2->0->1->NULL
向右旋轉 2 步: 1->2->0->NULL
向右旋轉 3 步: 0->1->2->NULL
向右旋轉 4 步: 2->0->1->NULL
分析:先判斷連結串列的長度len,k=k%len
,設定一個快指標,先走k步,再設定一個慢指標更快指標同時向後推,直到快指標為最後一個元素。此時慢指標的下一個元素為新的頭指標。
ListNode* rotateRight(ListNode* head, int k) {
if (!head) return head;
ListNode *fast = head;
int num = 0;
while (fast)
{
fast = fast->next;
num++;
}
int m = k % num;
fast= head;
for (int i = 1; i <= m; i++) {
fast = fast->next;
if (fast == NULL) {
fast = head;
}
}
ListNode *slow = head;
while (fast->next != NULL) {
fast = fast->next;
slow = slow->next;
}
ListNode *newHead = slow->next;
if (newHead == NULL) {
newHead = head;
}
fast->next = head;
slow->next = NULL;
return newHead;
}
29. 矩陣置零
給定一個 m x n 的矩陣,如果一個元素為 0,則將其所在行和列的所有元素都設為 0。請使用原地演算法。
分析:使用兩個變數記錄首行或者首列是否含有0,然後用首行或者首列來記錄該行或列是否需要置0.
30. 搜尋二維矩陣
編寫一個高效的演算法來判斷 m x n 矩陣中,是否存在一個目標值。該矩陣具有如下特性:
每行中的整數從左到右按升序排列。
每行的第一個整數大於前一行的最後一個整數。
分析:先用二分搜尋列,再用二分搜尋行。
30. 等差數列劃分
函式要返回陣列 A 中所有為等差陣列的子陣列個數。
A = [1, 2, 3, 4]
返回: 3, A 中有三個子等差陣列: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。
分析:動態規劃。儲存當前為結尾所能組成的等差數列的個數。例如上為help=[0,0,1,2]。然後將help矩陣相加。
int numberOfArithmeticSlices(vector<int>& A) {//等差數列劃分
int n = A.size();
if (n < 3) return 0;
vector<int> help(n, 0);
int key = A[1] - A[0];
int ans = 0;
for (int i = 2; i < n; ++i) {
if (A[i] - A[i - 1] == key) {
help[i] = help[i - 1] + 1;
}
else {
key = A[i] - A[i - 1];
}
ans += help[i];
}
return ans;
}