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;
}