设计模式
单例模式
它的核心结构只包含一个被称为单例的特殊类。它的目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
有很多地方都需要这样的功能模块,如系统的日志输出,操作系统只能有一个窗口管理器,一台PC连接一个键盘等。
核心:
- 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
- 线程安全(可以通过加锁实现,但用局部静态变量更好)
- 禁止赋值和拷贝
- 用户通过接口获取实例:使用 static 类成员函数
- 避免内存泄漏(可以使用智能指针)
饿汉模式:单例类定义的时候就进行实例化
懒汉模式:第一次用到类的实例的时候才回去实例化
饿汉模式
#include <iostream>
class Singleton
{
public:
~Singleton(){
std::cout<<"destructor called!"<<std::endl;
}
//禁用拷贝和赋值构造,避免用户调用拷贝和辅助构造
Singleton(const Singleton&)=delete;
Singleton& operator=(const Singleton&)=delete;
//静态函数 返回静态局部变量得引用
//静态局部变量只对定义自己的函数体始终可见
static Singleton& get_instance()
{
//返回创建得静态对象 声明周期到程序结束
//对于局部静态变量 保证了线程安全 保证了内存不泄露
//1.变量在代码第一次执行到变量声明的地方时初始化。
//2.初始化过程中发生异常的话视为未完成初始化,未完成初始化的话,需要下次有代码执行到 相同位置时再次初始化。
//3.在当前线程执行到需要初始化变量的地方时,如果有其他线程正在初始化该变量,则阻塞当前线程,直到初始化完成为止。
//4.如果初始化过程中发生了对初始化的递归调用,则视为未定义行为。
static Singleton instance;
return instance;
}
private:
Singleton(){
std::cout<<"constructor called!"<<std::endl;
}
};
int main(int argc, char *argv[])
{
Singleton& instance_1 = Singleton::get_instance();
Singleton& instance_2 = Singleton::get_instance();
return 0;
}
剑指offer
面试题1:重载赋值运算符(字符串、运算符重载)
note:
1.深拷贝与浅拷贝
浅拷贝会直接复制指针,两个对象的数据指针指向同一块内存,两个对象是联动的,且析构时内存会存在重复释放的问题。
2.拷贝构造函数要传递常量的引用const &
https://www.cnblogs.com/codingmengmeng/p/5871254.html
const是为了安全 ,&是因为如果值传递会自动调用拷贝构造函数创建临时对象,陷入无限递归调用拷贝构造函数
3.重载赋值运算符需要注意的点:
3.1赋值运算符要返回对象的引用 为了能够连等 节省开销 避免拷贝构造函数和析构函数的多余调用
3.2要深拷贝
3.3参数要传递自身的引用
3.4要释放对象原来自己的内存
3.5要判断传入参数是否跟自己是同一个实例,避免3.4把内存整没了
4.strlen在返回时不包括\0
5.vs编译器上调不通,liunx下g++没问题
//#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<cstdio>
using namespace std;
#include<string>
#include<cstring>
#include<stdio.h>
class CMyString
{
public:
CMyString(const char* pData = nullptr);
CMyString(const CMyString& str);
~CMyString(void);
void print();
CMyString& operator=(const CMyString& str);
private:
char* m_pData;
};
CMyString::CMyString(const char * pData)
{
if (pData == nullptr)
{
this->m_pData = new char(1);
m_pData[0] = '\0';
}
else
{
int len = strlen(pData);
this->m_pData = new char(len+1);
strcpy(this->m_pData, pData);
}
}
CMyString::CMyString(const CMyString& str)
{
int len = strlen(str.m_pData);
this->m_pData = new char(len+1);
strcpy(this->m_pData, str.m_pData);
}
CMyString::~CMyString(void)
{
delete[] this->m_pData;
}
CMyString& CMyString::operator=(const CMyString& str)
{
if (this == &str)
{
return *this;
}
else
{
delete[] this->m_pData;
int len = strlen(str.m_pData);
this->m_pData = new char(len+1);
strcpy(this->m_pData, str.m_pData);
return *this;
}
}
void CMyString::print()
{
printf("%s\n",this->m_pData);
}
void test()
{
const char* text = "hello world";
CMyString str1(text);
str1.print();
CMyString str2;
str2=str1;
str2.print();
CMyString str3(str2);
str3=str2=str1;
str3.print();
}
int main()
{
test();
return 0;
}
面试题3.求数组中重复的数字(哈希集合、数组)
/*
找出数组中重复的数字。
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字
*/
//1.先排序 再for一遍 相邻判断 o(nlogn)+o(n)=o(nlogn)
//2.for一遍存放到 哈希set 先查找是否存在 再插入 o(n)+o(1)+o(1) 空间o(N)
//*/
//int find_first_same_number(vector<int> &nums)
//{
// unordered_set<int> hash_set;
// for (int i = 0; i < nums.size(); i++)
// {
// if (hash_set.count(nums[i]))
// {
// return nums[i];
//
// }
// else
// {
// hash_set.insert(nums[i]);
// }
// }
// return 0;
//}
//
//
//
//
//
//int main()
//{
// vector<int> nums = { 2,3,1,0,2,5,3 };
// int ret=find_first_same_number(nums);
// cout << ret << endl;
// return 0;
//}
面试题4 二维数组中的查找(二维数组、线性查找)
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
#include<iostream>
#include<vector>
using namespace std;
//判断target跟右上角的元素,若小于则剔除右边所有的列,若大于则剔除所在行
bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
if (matrix.empty())
{
return false;
}
int n = matrix[0].size();
int m = matrix.size() ;
int i = 0;//列数
int j = n-1;//行数
while (i<m&&j>=0)
{
if (target == matrix[i][j])
{
return true;
}
else if(target < matrix[i][j])
{
j--;
}
else if(target > matrix[i][j])
{
i++;
}
}
return 0;
}
int main()
{
vector<vector<int>> matrix
{
{1},
{1}
};
int ret=findNumberIn2DArray(matrix, 0);
cout << ret << endl;
}
可以用二分查找每一行,但不能行列查找都用二分 一个要遍历全部的才能保证不漏掉
面试题5 替换空格(字符串)
请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
剑指offer还提供一种思路,但是要保证原字符串后面有足够的空间,for一遍计算加上空格的总长度后,一个指针指向原字符串的结尾,一个指针指向替换后字符串的末尾,双指针从尾到头复制
#include<iostream>
using namespace std;
/*
1.空间换时间
额外开辟一个足够大的string tmp
一个指针负责for s 遇到空格赋值%20
一个指针负责tmp
note:节省空间的思路 先for一遍判断有几个空格,然后分配总长度
2.更优秀的解法
判断字符串总长度
3.用stl api
*/
string replaceSpace(string s)
{
int i = 0;//s
int j = 0;//tmp
char* tmp = new char[10000];
//memset(tmp, '0', 10000);
while (i < s.size())
{
if (s[i] == ' ')
{
tmp[j] = '%';
tmp[++j] = '2';
tmp[++j] = '0';
j++;
i++;
}
else
{
tmp[j++] = s[i++];
}
}
tmp[j] = '\0';
//s = tmp;
string result(tmp);
delete[] tmp;
return result;
}
//stl 真香
string replaceSpace1(string s)
{
int i = 0;
string result;
while (i < s.size())
{
if (s[i] == ' ')
{
result.append("%20");
i++;
}
else
{
result.push_back(s[i]);
i++;
}
}
return result;
}
//stl不要脸
string replaceSpace2(string s)
{
int i = 0;
while (i < s.size())
{
if (s[i] == ' ')
{
s.replace(i, 1, "%20");
}
i++;
}
return s;
}
int main()
{
string s = "We are happy.";
string result;
result=replaceSpace2(s);
cout << result << endl;
return 0;
}
面试题6 从尾至头打印链表(反转链表、栈)
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
允许修改链表。可反转链表后打印
class Solution {
public:
ListNode* reverse_sort(ListNode* head)
{
if (!head || !head->next)
{
return head;
}
ListNode* p = reverse_sort(head->next);
head->next->next = head;
head->next = nullptr;
return p;
}
vector<int> reversePrint(ListNode* head)
{
ListNode* p=reverse_sort(head);
vector<int> vec;
while (p)
{
vec.push_back(p->val);
p = p->next;
}
return vec;
}
};
不修改链表 用栈实现
class Solution {
public:
vector<int> reversePrint(ListNode* head)
{
stack<ListNode*> nodes_stack;
ListNode* p = head;
while (p)
{
nodes_stack.push(p);
p = p->next;
}
vector<int> vec;
ListNode* temp_node;
while (!nodes_stack.empty())
{
temp_node = nodes_stack.top();
nodes_stack.pop();
vec.push_back(temp_node->val);
}
return vec;
}
};
不修改链表 用递归实现
class Solution {
public:
vector<int> vec;
vector<int> reversePrint(ListNode* head)
{
if (!head)
{
return {};
}
reversePrint(head->next);
vec.push_back(head->val);
return vec;
}
};
面试题7 重建二叉树(二叉树、前序遍历、中序遍历)(没悟)
输入某二叉树的前序遍历和中序遍历的结果,请重建该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。
递归函数的功能就是通过前序遍历和中序遍历 建立根节点并开启左右子树分别递归
1.前序遍历的首个元素就是根节点
2.在中序遍历中搜索根节点的索引,就可以知道根节点的左子树有多少个元素,右子树有多少个元素。
3.将前序遍历划分为[ 根节点 | 左子树 | 右子树 ]
这道题递归有点想不明白
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution
{
public:
TreeNode* recur(vector<int>& preorder, vector<int>& inorder,int pre_root,int in_left,int in_right)
{
if (in_left > in_right)
{
return nullptr;
}
TreeNode* node = new TreeNode(preorder[pre_root]);//前序遍历的第一个数就是根节点的值 建立根节点
int k = hash[preorder[pre_root]];//通过hash找到该根节点在中序遍历中的位置索引
//则 k 左边是左子树的中序遍历,右边是右子树的中序遍历;
//假设左子树的中序遍历的长度是 L,
//则在前序遍历中,根节点后面的 L 个数,是左子树的前序遍历,剩下的数是右子树的前序遍历;
node->left = recur(preorder, inorder, pre_root + 1, in_left, k - 1);
node->right = recur(preorder, inorder, pre_root + k - in_left + 1, k + 1, in_right);
return node;
}
unordered_map<int, int> hash;
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder)
{
for (int i = 0; i < inorder.size(); i++)
{
hash[inorder[i]] = i;
}
// 传入参数:前序,中序,前序序列左边界,中序序列左边界,中序序列右边界
return recur(preorder, inorder,0,0,inorder.size()-1);
}
};
自己写的一个好理解的版本
/**
* Definition for binary tree
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
//22:40
class Solution
{
public:
//递归 返回重建后的头节点
//每次递归建立当前根节点 并开启左右子树递归
//
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin)
{
return fun(pre,vin,0,pre.size()-1,0,vin.size()-1);
}
TreeNode* fun(vector<int> pre,vector<int> vin,int pre_l,int pre_r,int vin_l,int vin_r)
{
if(pre_l>pre_r||vin_l>vin_r)//由于前序和中序的个数应该相同 所以要相等应该一起相等
{
return nullptr;
}
//前序的第一个元素为根节点
TreeNode* root=new TreeNode(pre[pre_l]);
//在中序中找到根节点的位置
int temp=vin_l;
while(vin[temp]!=root->val)
{
temp++;
}
//记录中序中左子树元素的个数
int len=temp-vin_l;
//在前序中找到左子树的右端点
int pre_tmp=pre_l+len;
//这里输入是有可能越界的,但在递归判断时剪掉了
//递归开启左右
root->left=fun(pre,vin,pre_l+1,pre_tmp,vin_l,temp-1);
root->right=fun(pre,vin,pre_tmp+1,pre_r,temp+1,vin_r);
return root;
}
};
自己写的哈希加速
/**
* Definition for binary tree
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
//22:40
//23:27
class Solution
{
public:
//递归 返回重建后的头节点
//每次递归建立当前根节点 并开启左右子树递归
//
unordered_map<int,int> hash_map;
TreeNode* reConstructBinaryTree(vector<int> pre,vector<int> vin)
{
if(pre.size()==0||vin.size()==0) return nullptr;
//哈希加速
for(int k=0;k<=vin.size()-1;k++)
{
hash_map[vin[k]]=k;
}
return fun(pre,vin,0,pre.size()-1,0,vin.size()-1);
}
TreeNode* fun(vector<int> pre,vector<int> vin,int pre_l,int pre_r,int vin_l,int vin_r)
{
if(pre_l>pre_r||vin_l>vin_r)//由于前序和中序的个数应该相同 所以要相等应该一起相等
{
return nullptr;
}
//前序的第一个元素为根节点
TreeNode* root=new TreeNode(pre[pre_l]);
//在中序中找到根节点的位置
int temp=hash_map[root->val];
// //在中序中找到根节点的位置
// int temp=vin_l;
// while(vin[temp]!=root->val)
// {
// temp++;
// }
//记录中序中左子树元素的个数
int len=temp-vin_l;
//在前序中找到左子树的右端点
int pre_tmp=pre_l+len;
//这里输入是有可能越界的,但在递归判断时剪掉了
//递归开启左右
root->left=fun(pre,vin,pre_l+1,pre_tmp,vin_l,temp-1);
root->right=fun(pre,vin,pre_tmp+1,pre_r,temp+1,vin_r);
return root;
}
};
面试题8 二叉树的下一个节点
当前节点在中序遍历里的下一个节点,二叉搜索树中比该节点大的最小的元素,
分情况
1.有右子树:答案为右子树最左侧的节点 (迭代寻找右子树最左侧的点
2.没有右子树:
1.该节点为father的左孩子 则father就是他的后继
2.该节点为father的右孩子 则一直要往上找 直到第一个 当前的点是father的左儿子 则father就是后继
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode* father;
TreeNode(int x) : val(x), left(NULL), right(NULL), father(NULL) {}
};
/*
分情况
1.有右子树:答案为右子树最左侧的节点 (迭代寻找右子树最左侧的点
2.没有右子树:
1.该节点为father的左孩子 则father就是他的后继
2.该节点为father的右孩子 则一直要往上找 直到第一个 当前的点是father的左儿子 则father就是后继
*/
class Solution {
public:
TreeNode* inorderSuccessor(TreeNode* p)
{
//有右子树
if (p->right)
{
p = p->right;
while (p->left) p = p->left;
return p;
}
//没有右子树
//根节点
if (!p->father) return p->right;
//该节点为father的左孩子
if (p == p->father->left)
{
p = p->father;
return p;
}
// 该节点为father的右孩子
if (p == p->father->right)
{
while (p->father)
{
if (p == p->father->left)
{
return p->father;
}
p = p->father;
}
return nullptr;
}
}
};
面试题9 用两个栈实现队列(栈、队列)
stack1用于插入 stack2用于弹出
插入时,直接插入stack1
删除时,如果stack2为空,就先把stack1中的数逐个弹出并插入stack2,再删除
//栈 先进后出
//队列 后进先出
/**
* Your CQueue object will be instantiated and called as such:
* CQueue* obj = new CQueue();
* obj->appendTail(value);
* int param_2 = obj->deleteHead();
*/
/*
stack1用于插入 stack2用于弹出
删除时:
1.若stack2为空,则将stack1的元素逐个弹出并压入stack2
2.若stack2不为空,则直接将stack2顶部的元素弹出
*/
class CQueue {
public:
void appendTail(int value)
{
stack1.push(value);
}
int deleteHead()
{
if (stack2.empty())
{
while (stack1.size() > 0)
{
int data = stack1.top();
stack1.pop();
stack2.push(data);
}
}
if (stack2.empty())//仍然为空
return -1;
int deletepar = stack2.top();
stack2.pop();
return deletepar;
}
private:
stack<int> stack1, stack2;
};
面试题10 (1)斐波那契数列(动态规划)
写一个函数,输入 n
,求斐波那契(Fibonacci)数列的第 n
项(即 F(N)
)。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
注意点:
直接递归会存在大量重复的计算
重点是学会迭代的写法 自下而上(也就是动态规划)
dp[i]=dp[i-1]+dp[i-2]
取余规则 f(n)⊙p=[f(n−1)⊙p+f(n−2)⊙p]⊙p
class Solution {
public:
/*
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
*/
int fib(int n)
{
if (n == 1)
{
return 1;
}
else if (n == 0)
{
return 0;
}
else
{//从下往上 先算 f(2)
long long fib_one = 1;//F(N - 1)
long long fib_two = 0;//F(N - 2)
long long fib_SUM = 0;
for (int i = 2; i <= n; i++)
{
fib_SUM = (fib_one + fib_two)%1000000007;
fib_two = fib_one;
fib_one = fib_SUM;
}
}
}
};
面试题 10(2) 青蛙跳台阶(动态规划)
一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
f(n)=f(n-1)+f(n-2)
跟斐波那契一样得
class Solution {
public:
int numWays(int n)
{
if (n <= 1)
return 1;
else
{
/*1.把n级台阶时的跳法看成n的函数f(n)
2.当n>2时,就有两种不同的选择 一是第一次只跳一级 此时跳法数目等于后面剩下的f(n-1)
二是第一次跳二级 此时跳法数目等于后面剩下的f(n-2)
3.所以f(n)=f(n-1)+f(n-2)
*/
//以下是斐波那契数列算法
//从下往上 先算 f(2)
long long fib_one = 1;//F(N - 1)
long long fib_two = 1;//F(N - 2)
long long fib_SUM = 0;
for (int i = 2; i <= n; i++)
{
fib_SUM =( fib_one + fib_two)% 1000000007;
fib_two = fib_one;
fib_one = fib_SUM;
}
return fib_SUM;
}
}
};
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TllGlN6r-1629084912950)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210524112705330.png)]
面试题10(3)变态跳台阶
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
常规做法
class Solution {
public:
//f(n)=f(n-1)+f(n-2)+...f(0)
int jumpFloorII(int number)
{
if(number==0) return 1;
if(number==1) return 1;
vector<int> dp(number+1,0);
dp[0]=1;
dp[1]=1;
for(int i=2;i<=number;i++)
{
for(int j=0;j<i;j++)
{
dp[i]+=dp[j];
}
}
return dp[number];
}
};
奇妙的做法
因为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)
int jumpFloorII(int number) {
if( number <= 1) return number;
return pow(2, number-1);
}
面试题11 旋转数组的最小数字(二分法)
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。
核心思想:
二分查找数组下标索引
注意点:如果相等逐个缩小范围
边界条件
明确二分查找的目得 确定答案位于mid的哪一侧,由于没有目标值,我们可以考虑跟端点值相比
class Solution {
public:
int minArray(vector<int>& numbers)
{
if (numbers.size() <= 0)
return -1;
else if (numbers.size() == 1)
return numbers[0];
int p1 = 0;//前一个递增数组最小得数
int p2 = numbers.size() - 1;后一个递增数组最大的数
while (p1 < p2)//结束条件
{
int mid = (p1 + p2) >> 1;
//在第二个递增数组中
if (numbers[mid] < numbers[p2])
{
p2 = mid;
}
//在第一个递增数组中
else if (numbers[mid] >numbers[p2])
{
p1 = mid+1;//c
}
else
{
p2 = p2 - 1;//遇到相同元素--
}
}
return numbers[p1];
}
};
二刷还是没写对
一种写法
//10:06
class Solution {
public:
int minNumberInRotateArray(vector<int> rotateArray)
{
if(rotateArray.size()==0) return 0;
//找满足条件的一个数
int l=0,r=rotateArray.size()-1;
while(l<r)
{
int mid=l+r>>1;
if(rotateArray[mid]>rotateArray[r])
{
l=mid+1;
}
else if(rotateArray[mid]<rotateArray[r])
{
r=mid;
}
else
{
r=r-1;
}
}
return rotateArray[l];
}
};
第二种写法
int minNumberInRotateArray(vector<int> rotateArray) {
if( rotateArray.size() == 0) return 0;
int low = 0, high = rotateArray.size()-1;
while(low + 1 < high){
int mid = low + (high - low)/2;
if(rotateArray[mid] < rotateArray[high]) high = mid;//说明右边有序,那就向左边走
else if(rotateArray[mid] == rotateArray[high]) high = high-1;// 这种情况跟是特例只能一个一个的判断
else
low = mid;
}
return min(rotateArray[low], rotateArray[high]);
}
面试题12 矩阵中的路径(dfs典型题、回溯、二维数组)
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
核心思想:
首先枚举所有起点,再枚举所有方向
#include<iostream>
using namespace std;
#include<vector>
/*
这算法叫深度优先搜索(DFS)
有点暴力解的意思 把所有能走的路都走了一遍,通过递归实现,递归的时候同时判断是否还能走下去,走不下去就返回false(剪枝)
*/
//
class Solution {
public:
bool exist(vector<vector<char>>& board, string word)
{
row = board.size();
col = board[0].size();
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
//搜索得开始位置
if (dfs(board, word, i, j, 0))
return true;
}
}
return false;
}
private:
int row, col;
// board:网格, i:当前行, j:当前列, word:待匹配字符串, k:当前匹配到字符串word的位置
bool dfs(vector<vector<char>>& board, string word, int i, int j, int k)
{
//这里要剪枝
if (i >= row || i<0 || j>=col || j < 0 || board[i][j] != word[k])//不匹配或走到边界
return false;
//匹配到最后一个元素
if (k == word.size() - 1) return true;//匹配成功
board[i][j] = '\0';//走过的节点置为0 避免走重复的路
//向上下左右四个方向继续递归
bool res = dfs(board, word, i + 1, j, k + 1) || dfs(board, word, i , j+1, k + 1)
|| dfs(board, word, i - 1, j, k + 1) || dfs(board, word, i , j-1, k + 1);
//因为是一层一层向下递归来进行字符串匹配的,回溯的时候要“释放“这个单元格 恢复现场
board[i][j] = word[k];//返回的时候把矩阵复原
//因为只代表此次搜索过程中,该元素已访问过,当初始i j变化时,又开始了另一次搜索过程
return res;
}
};
面试题13 机器人的运动范围(dfs典型题、bfs典型题)
这题两种做法都可以学习一下
建议使用bfs 因为dfs有可能会栈溢出
传递数组一定要记得&传递 不然就不是全局的数组了
深度优先搜索(DFS): 可以理解为暴力法模拟机器人在矩阵中的所有路径。DFS 通过递归,先朝一个方向搜到底,再回溯至上个节点,沿另一个方向搜索,以此类推。
剪枝: 在搜索中,遇到数位和超出目标值、此元素已访问,则应立即返回,称之为 可行性剪枝
BFS/DFS : 两者目标都是遍历整个矩阵,不同点在于搜索顺序不同。DFS 是朝一个方向走到底,再回退,以此类推;BFS 则是按照“平推”的方式向前搜索。
BFS 实现: 通常利用队列实现广度优先遍历。
题目:
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
深度优先搜索
class Solution
{
public:
int movingCount(int m, int n, int k)
{
vector<vector<int>> visited(m,vector<int>(n,0));//记录是否被访问过 访问过置1
row = m;
col = n;
return dfs(0, 0, k,visited);
}
private:
/*
i 行 j 列
*/
int row, col;
bool check(int i,int j,int k)
{
int sum=0;
if (i != 0)
{
sum += i / 10 + i % 10;
}
if (j != 0)
{
sum += j / 10 + j % 10;
}
if (sum > k)
return false;
else
return true;
}
//终止条件 1.数组索引越界 2.超出目标值k 3.当前元素已访问
//返回从这个i,j为起点机器人所能走到得格子数
int dfs(int i, int j, int k, vector<vector<int>>& visited)
{
//退出递归 到达边界 访问过 不满足条件
if (i < 0 || i >= row || j < 0 || j >= col ||(visited[i][j]==1)|| !check(i, j, k)) return 0;
//继续递归
visited[i][j] = 1;//标记访问过了
//先执行前一个dfs等待执行完之后再执行后一个 相当于一个方向走到底再回溯到原节点走另一个方向
int ret = 1+dfs(i, j + 1, k, visited) + dfs(i+1, j , k, visited);
//这里不用回溯恢复现场 因为是统计能到达得格子数量,上一条路走过得格子这次不能再走了
return ret;
}
};
广度优先搜索算法
https://blog.csdn.net/qq_37482202/article/details/89513877
利用队列来实现 先把第一个点插入队列 然后取出,再将能去到的点插入队列,一直到队列中没有元素
int bfs(int m, int n, int k)
{
vector<vector<int>> visited(m, vector<int>(n, 0));
vector<vector<int>> dir = { {-1,0},{1,0},{0,1},{0,-1} };
queue<vector<int>> q;
q.push({ 0,0 });
visited[0][0] = 1;//走过的标记为1
int count = 1;
int tpx,tpy, x, y, i;
while (!q.empty())
{
tpx = q.front()[0];
tpy = q.front()[1];
q.pop();
//遍历这个点的四个方向
for(i=0;i<4;i++)
{
x = tpx + dir[i][0];
y = tpy + dir[i][1];
//满足条件就把这个点插进队列
if (x >= 0 && x < m && y >= 0 && y < n && !visited[x][y] && check(x, y, k))
{
q.push({ x,y });
visited[x][y] = true;
count++;
}
}
}
return count;
}
面试题14 剪绳子(动态规划)
题目:
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
使用动态规划的几个特点
1.求一个问题的最优解
2.整体问题的最优解是依赖各个子问题的最优解
3.大问题分解为若干个子问题,这些子问题之间还有相互重叠的更小的子问题
4.为了避免重复计算子问题,可以从上往下分析问题,从下往上求解问题,从解决小问题开始,把已经解决的子问题的最优解存储下来。
目得是计算dp[n]的值,从底向上计算dp[i]的值
dp[i]表示剪长度为i的绳子所能得到的最大乘积,第一层for循环遍历计算dp[i],第二层遍历总长度为i时,每一种切法的值,然后在遍历的过程中记录最大值
class Solution {
public:
//len =n
//递归公式 f(n)=max(f(i)*f(n-i))
//使用额外的空间存储已经得到的结果
//前四个数字特殊处理
//2 <= n <= 58
int cuttingRope(int n)
{
if (n == 2)
return 1;
if (n == 3)
return 2;
int* product = new int[n + 1];//开辟额外空间记录
product[1] = 1;
product[2] = 2;//意思是2和3的时候没必要切
product[3] = 3;
//从i=4开始计算 计算1*(4-1) 2*(4-2)
//这里用到一个小技巧 1*3 和3*1 是一样的 所以j只需要走到i/2就可以了
//
for (int i = 4; i <= n; i++)
{
int max = 0;
//遍历长度为i的绳子 所有的剪法 这里只要遍历到i的一一半 因为1 3和3 1是一样的
//要记录该剪法的最大值
for (int j = 1; j <= i/2; j++)
{
int products = product[j]* product[i-j];
if (products > max)
{
max = products;
}
product[i] = max;
}
}
int result = product[n];
delete[]product;
return result;
这里如果n的输入过大会超出int的范围 涉及到一个大数取余的问题
// long result=b;
// while (timesof3--)
// {
// result = result * 3 % 1000000007;
// }
}
};
面试题15:二进制中1的个数(位运算)
编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为 汉明重量).)。
n-1可以把最右边一个1变成0 最右边1右边的0都变成1 n&n-1即可将最右边的1变成0 这样循环的次数就是1的数量 循环结束的条件是n为0
位的基础操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qUNTsUV0-1629084912951)(数据结构与算法.assets/image-20210603102035658.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e9s074xc-1629084912952)(数据结构与算法.assets/image-20210603102050652.png)]
常规解法 利用1 去判断n的每一位
class Solution {
public:
/*
先判断最右边的数字是否为1 &1
再将1左移一位 判断次低位是否为1
设置一个count记录1出现的次数
*/
int hammingWeight(uint32_t n)
{
int count=0;
unsigned int flag = 1;
while (flag)
{
if (n & flag)
{
count++;
}
flag=flag << 1;
}
return count;
}
};
效率高的解法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gydxogts-1629084912953)(数据结构与算法.assets/image-20210603110604010.png)]
/*
n &= n - 1 : 消去数字 n 最右边的 1 。
直到n的1全部被消完
*/
int hammingWeight2(uint32_t n)
{
int count = 0;
while (n)
{
n = n & (n - 1);
count++;
}
return count;
}
二刷:(又写错了)
这个题要注意负数右移,最高位是要保证为1的,所以不能用逐个右移去判断负数中1的个数,但是呢可以不移动数字,利用0x01去判断每一位,然后0x01左移,但这是暴力解法
//16:48
class Solution {
public:
//每次判断最后一位是否是1 然后右移这个数
int NumberOf1(int n)
{
int sum=0;
int mark=0x01;
while(mark!=0)
{
if(mark&n)
{
sum++;
}
mark<<=1;
}
return sum;
}
};
对于上一种解法中,无用操作是,如果当前位是0
, 还是会做判断,然后一位一位的移动。
如果,给你一种超能力,你一下可以对从右向左的第一位1
直接判断,遇到0
直接略过,那效率是不是很快。
//16:48
class Solution {
public:
//每次判断最后一位是否是1 然后右移这个数
int NumberOf1(int n)
{
int sum=0;
while(n!=0)
{
n=n&(n-1);//消除掉最右边的1
sum++;
}
return sum;
}
};
面试题16 数值的整数次方(快速幂、数学推导)
现 pow(x, n) ,即计算 x 的 n 次幂函数(即,xn)。不得使用库函数,同时不需要考虑大数问题。
遇到求乘方想到二次幂
核心思想:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gl1v5Gsq-1629084912954)(数据结构与算法.assets/image-20210710112450495.png)]
将十进制幂转换为二进制的每一位乘以对应的权重
#include<iostream>
using namespace std;
/*
-100.0 < x < 100.0
-231 <= n <= 231-1
-104 <= xn <= 104*/
class Solution {
public:
/*
底数为0
指数为负
*/
double myPow(double x, int n)
{
double res = 1.0;
long b=(long)n;
if (b == 0) return 1;
if (b < 0)
{
x = 1 / x;
b = -b;
}
/*每次在循环中计算x得n次方
指数n用二进制表示
每次判断n得最后一位,并进行右移去掉最后一位
*/
while (b>0)
{
if ((b & 1) == 1)//如果二进制得这一位不等于0 就要乘以对应得权重
{
res = res * x;//分解出来的每一项相乘
}
x = x * x;//x x^2 x^4
b= b >> 1;
}
return res;
}
};
二刷
暴力破解
//18:56
//19:09
class Solution
{
public:
//dp[n]=dp[n-1]*base
//o(exponent)
double Power(double base, int exponent)
{
double dp=1;
int flag=1;
if(exponent==0) return 1.0;
if(base==0) return 0.0;
if(exponent<0)
{
flag=-1;
exponent=-exponent;
}
for(int i=1;i<=exponent;i++)
{
dp=dp*base;
}
if(flag==-1) return 1/dp;
else return dp;
}
};
快速幂
//18:56
//19:09
class Solution
{
public:
//dp[n]=dp[n-1]*base
//o(exponent)
double Power(double base, int exponent)
{
double sum=1;//总和
int flag=1;
double x=base;//base 的偶数次方
if(exponent==0) return 1.0;
if(base==0) return 0.0;
if(exponent<0)
{
flag=-1;
exponent=-exponent;
}
while(exponent!=0)
{
//获得二进制的一位
int temp=exponent&1;
//如果这一位是1
if(temp)
{
sum*=x;
}
//更新
x=x*x;
exponent>>=1;
}
if(flag==-1) return 1.0/sum;
else return sum;
}
};
面试题17:打印从1到最大的n位数(大数问题、dfs全排列、字符串模拟加法)(没悟)
不考虑大数问题的解法:
class Solution {
public:
vector<int> printNumbers(int n)
{
int max = pow(10, n);
vector<int> res(max);
for (int i = 0; i < max; i++)
{
res[i] = i;
}
}
};
字符串模拟数字:(好难的)
class Solution {
public:
vector<int> output;
vector<int> printNumbers(int n)
{
// 以下注释的前提:假设 n = 3
if(n <= 0) return vector<int>(0);
string s(n, '0'); // s最大会等于999,即s的长度为n
while(!overflow(s)) inputNumbers(s);
return output;
}
bool overflow(string& s)
{
// 本函数用于模拟数字的累加过程,并判断是否越界(即 999 + 1 = 1000,就是越界情况)
bool isOverFlow = false;
int carry = 0; // carry表示进位
for(int i=s.length()-1; i>=0; --i)
{
int current = s[i] - '0' + carry; // current表示当前这次的操作
if(i == s.length() - 1) current ++; // 如果i此时在个位,current执行 +1 操作
if(current >= 10)
{
// 假如i已经在最大的那一位了,而current++之后>=10,说明循环到头了,即999 + 1 = 1000
if(i == 0) isOverFlow = true;
else
{
// 只是普通进位,比如current从9变成10
carry = 1;
s[i] = current - 10 + '0';
}
}
else
{
// 如果没有进位,更新s[i]的值,然后直接跳出循环,这样就可以回去执行inputNumbers函数了,即往output里添加元素
s[i] = current + '0';
break;
}
}
return isOverFlow;
}
void inputNumbers(string s)
{
// 本函数用于循环往output中添加符合传统阅读习惯的元素。比如001,我们会添加1而不是001。
bool isUnwantedZero = true; // 判断是否是不需要添加的0,比如001前面的两个0
string temp = "";
for(int i=0; i<s.length(); ++i)
{
if(isUnwantedZero && s[i] != '0') isUnwantedZero = false;
if(!isUnwantedZero) temp += s[i];
}
output.push_back(stoi(temp));
}
};
还有一个全排列递归的方法
若考虑大数问题,则首先需要将数字转成字符串避免溢出,然后全排列字符串的第0位到第n-1位。 存储结果时需去掉字符串前几位的0(0099没有意义,应为99)再放入结果。
class Solution {
private:
vector<int> res;
string s;
void savenum()
{
int p = 0;
//去除前导0
while(s[p] == '0' && p < s.size()) p++;
//转换为整型插入
if(p < s.size()) res.push_back(stoi(s.substr(p)));
}
//dfs全排列
void dfs(int& n,int index)
{
if(index == n)
{
//固定完所有的位就保存
savenum();
return;
}
//每一个位都有十种选择
for(int i = 0;i < 10;i++)
{
s[index] = '0' + i;
dfs(n,index+1);
}
}
public:
vector<int> printNumbers(int n) {
s.resize(n,'0');
dfs(n,0);
return res;
}
};
面试题18 删除链表的节点(链表)
题目:
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
记得设置一个哑节点 会比较好处理头节点需要删除的情况
#include<iostream>
using namespace std;
struct ListNode
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
ListNode* deleteNode(ListNode* head, int val)
{
ListNode* dummyHead = new ListNode(1);
dummyHead->next = head;
// if (head->next == nullptr && head->val == val) return nullptr;
ListNode* cur = dummyHead;
while (cur&&cur->next)
{
if (cur->next->val == val)
{
cur->next = cur->next->next;
break;
}
cur = cur->next;
}
return dummyHead->next;
}
};
面试题19 正则表达匹配(动态规划、递归)(先跳过)
好难 先跳过 动态规划的问题
请实现一个函数用来匹配包含 '. ’ 和 ’ * ’ 的正则表达式。模式中的字符 ’ . ’ 表示任意一个字符,而 ’ * ’ 表示它前面的字符可以出现任意次(含0次)。在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab * ac * a"匹配,但与"aa.a"和"ab*a"均不匹配。
这里先学习一下c++ 正则表达式API吧
#include <regex>
class Solution {
public:
bool isMatch(string s, string p) {
regex pattern(p);//匹配的规则(正则表达式)
smatch subMatch;//匹配的结果
return regex_match(s, subMatch, pattern);//用于将目标串和正则表达式匹配,返回一个 bool 值,true 为匹配,false 为不匹配。
}
};
面试题20 表示数值的字符串(字符串)
题目:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1Jtrikf2-1629084912955)(数据结构与算法.assets/image-20210710153148755.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NAmsGU98-1629084912956)(数据结构与算法.assets/image-20210710153221385.png)]
首先写两个函数 用来跳过无符号整数和无符号整数。、
1.跳过前面可能有的空格
2.跳过小数点前可能有的整数,并返回一个标志位
3.如果出现’.’,扫描小数部分,小数部分为无符号整数,
4.如果出现e或E,扫描数字的指数部分,可能为以’+‘或’-'开头的整数
5.去掉末尾的空格,
6.判断标志位 & 是否到达结尾
标志位numeric是一直在带着更新的
#include<iostream>
using namespace std;
class Solution {
public:
bool isNumber(string s)
{
if (s.empty()) return false;
while (s[index] == ' ')//去掉可能存在字符串头部的' '
++index;
bool numeric = scanInterger(s);//扫描小数点前面整数部分,可能为以'+'或'-'开头的整数
if (s[index] == '.')//如果出现'.',扫描小数部分,小数部分为无符号整数
{
++index;
//1、小数点前面可以没有整数部分,如 .123
//2、小数点后面可以没有数字,如 233.
//3、小数点前后可以都有数字,如 233.123
numeric = scanUnsignedInterger(s)||numeric;
}
if (s[index] == 'e' || s[index] == 'E')//如果出现e或E,扫描数字的指数部分,可能为以'+'或'-'开头的整数
{
++index;
numeric = numeric && scanInterger(s);
// e的前后必须有数字
// 1、当e或E前面没有数字时,整个字符串不能表示数字,如 .e1、e1;
// 2、当e或E后面没有整数时,整个字符串不能表示数字,如 12e、12e+5.4
}
while (s[index] == ' ' && index != s.size())//去掉末尾的' '
++index;
return numeric && index == s.size();//如果标志位为false 或者此时的index没有到字符串的结尾就返回false
}
private:
int index = 0;
bool scanInterger(string& s)//去掉有符号整数部分
{
if (s[index] == '+' || s[index] == '-')
{
++index;
}
//去掉可能存在的符号
return scanUnsignedInterger(s);
}
//扫描无符号整数部分
bool scanUnsignedInterger(string& s)
{
int before = index;
//如果接下来有0-9的数字就返回ture 否则返回false
while (index != s.size() && s[index] >= '0' && s[index] <= '9')
++index;
return index > before;
}c++
};
面试题21 调整数组顺序使奇数位于偶数之前(双指针)
题目:
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
简单双指针
这里有一个优化点 利用奇数二进制位最后一位是1的性质判断奇偶性能大大降低运行时间
#include<iostream>
using namespace std;
#include<vector>
class Solution {
public:
//偶数返回1
bool jugde(int n)
{
// if (n%2 == 0)
if(n&1==1)
return false;
else
return true;
}
//使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
vector<int> exchange(vector<int>& nums)
{
//双指针交换
int left = 0;
int right = nums.size() - 1;
while (left < right)
{
while (!jugde(nums[left])&&left< nums.size() - 1)//跳过所有的奇数
{
left++;
}
while (jugde(nums[right])&&right>0)//跳过所有的偶数
{
right--;
}
if (left < right)
{
swap(nums[left], nums[right]);
}
}
return nums;
}
};
int main()
{
Solution s;
vector<int> nums = { 1,3,5 };
s.exchange(nums);
cout << nums[0] << endl;
}
如果要求保证奇数和奇数,偶数和偶数之间的相对位置不变。就不能用双指针了
//19:50
class Solution {
public:
//类似冒泡的思想
//前一个数是偶数 后一个数是奇数 就交换
//每轮可以把一个偶数放到最后
//冒泡是稳定的
void reOrderArray(vector<int> &array)
{
bool change = false;
for(int i=0;i<array.size()-1;i++)
{
for(int j=array.size()-1;j>0;j--)
{
//if((array[j]%2==0)&&(array[j+1]%2==1))
if(((array[j-1]&1)==0)&&((array[j]&1)==1))
{
swap(array[j],array[j-1]);
change = true;
}
}
if (!change)
{
return;
}
}
}
};
面试题22 链表中倒数第k个节点(双指针、哈希表)
二刷:双指针应该是最优解
//先让fast先走k-1步 slow再从头开始走 就是两个指针之间相隔k-1 最后fast到结尾 slow就在倒数第K个节点的位置 返回slow
#include<iostream>
using namespace std;
struct ListNode
{
int val;
ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};
class Solution {
public:
//先遍历一遍计算有多长 再计算要找的节点正数是多少 然后再遍历一遍找到他 0(N)
//遍历一遍的做法:
//先让fast先走k-1步 slow再从头开始走 就是两个指针之间相隔k-1 最后fast到结尾 slow就在倒数第K个节点的位置 返回slow
ListNode* getKthFromEnd(ListNode* head, int k)
{
if (head == nullptr || k <1) return nullptr;
ListNode* fast = head;
ListNode* slow = head;
int count = k - 1;
//先让fast先走k-1步
while (count--)
{
//如果k>总长度 返回nullptr
if (fast->next != nullptr)
fast = fast->next;
else
return nullptr;
}
//一起走
while (fast->next != nullptr)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
哈希解法 时间复杂度100%
ListNode* getKthFromEnd2(ListNode* head, int k)
{
if (head == nullptr || k < 1) return nullptr;
unordered_map<int, ListNode*> hash_map;
int pos = 0;//用于遍历存储
// int end;//总长度
int result;//倒数第k
//遍历一遍把每一个pos和节点存进哈希表
while (head!= nullptr)
{
pos++;
hash_map.insert(make_pair(pos, head));
head = head->next;
}
result = pos - k + 1;
if (result < 0) return nullptr;
unordered_map<int, ListNode*>::iterator it = hash_map.find(result);
ListNode* re=it->second;
return it->second;
}
面试题23 链表中环的入口节点(快慢指针、链表中的环)
这题剑指offer中好像没有,没法debug,说一下思路吧
(找到一个环内的节点、计算环中节点的个数)
1.定义快指针一次走两步,慢指针每次走一步,如果存在环,两者一定在环中相遇,然后记下这个节点的值,一边继续向前一边计数,直到再次回到这个节点就可以得到环中节点数n了。
(知道环中节点的个数n后,让快指针先走n步,慢指针再开始走,最终两个指针会在环的入口相遇)
2.在知道环中有多少个节点的前提下,还是让快指针先走n步,然后慢指针再开始走,那么最终会在环的入口相遇(两个指针间隔n)
思路:从头开始 快指针一次走两步 慢指针一次走一步 如果有环他两一定会在环中相遇
设m为头到环入口的路程 x为环入口到相遇点的路程 n为整个环的路程
此时快指针走的路程 m+x+kn
慢指针:m+x
那么将快指针放到头部,一次一步再走m到环的入口,慢指针也走m,
此时快指针走的路程 m+x+kn+m
慢指针:m+x+m
两者路程的差是kn,又相遇了,且为环的入口
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
//15:33
//16:06
//16:11
//16:25
class Solution
{
public:
ListNode *detectCycle(ListNode *head)
{
if(!head||!head->next||!head->next->next) return nullptr;
ListNode* fast=head->next->next;
ListNode* slow=head->next;
while(fast!=slow&&fast!=nullptr&&fast->next!=nullptr)
{
fast=fast->next->next;
slow=slow->next;
}
if(!fast||!fast->next) return nullptr;
//cout<<fast->val<<slow->val<<endl;
fast=head;
//cout<<fast->val<<slow->val<<endl;
while(fast!=slow)
{
// cout<<"a"<<endl;
fast=fast->next;
slow=slow->next;
}
return fast;
}
};
面试题24 反转链表(迭代、三指针)
题目:
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
关键:哑节点、三指针
class Solution {
public:
ListNode* reverseList(ListNode* head)
{
if (head == nullptr) return nullptr;
//temp用来保存后一个节点的地址
//pre指向前一个节点,初始指向null
//cur指向当前节点 ,初始指向head
ListNode* pre = nullptr;
ListNode* cur = head;
ListNode* temp;
while (cur)
{
temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
};
二刷:看到了一种新做法
头插法 挺秀的
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
//头插法
class Solution {
public:
ListNode* ReverseList(ListNode* pHead) {
ListNode* dummy=nullptr;
ListNode* temp;
while(pHead)
{
temp=pHead;
pHead=pHead->next;
temp->next=dummy;
dummy=temp;
}
return temp;
}
};
面试题 25 合并两个排序的链表(双指针)
题目:输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的。
class Solution {
public:
//双指针 比较大小
//一个哑节点
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
{
ListNode* dummy=new ListNode(1);
ListNode* result =dummy;
//直到一条走完
while (l1 && l2)
{
if (l1->val < l2->val)
{
dummy->next = l1;
dummy = dummy->next;
l1 = l1->next;
}
else
{
dummy->next = l2;
dummy = dummy->next;
l2 = l2->next;
}
}
if (!l1)
dummy->next = l2;
else
dummy->next = l1;
return result->next;
}
};
二刷 递归解法
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) :
val(x), next(NULL) {
}
};*/
//14:44
class Solution {
public:
//递归解法
//递归函数返回合并后的头节点
ListNode* Merge(ListNode* pHead1, ListNode* pHead2)
{
if(pHead1==nullptr) return pHead2;
if(pHead2==nullptr) return pHead1;
if(pHead1->val<pHead2->val)
{
pHead1->next=Merge(pHead1->next,pHead2);
return pHead1;
}
else{
pHead2->next=Merge(pHead1,pHead2->next);
return pHead2;
}
}
};
面试题26 树的子结构(前序遍历、对称性递归)
题目:输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
分两步 先遍历A树 找到与B树的根节点值相同的节点 然后再继续比较该更节电是否满足条件
#include<iostream>
using namespace std;
/*
输入两棵二叉树A和B,判断B是不是A的子结构。(约定空树不是任意一个树的子结构)
B是A的子结构, 即 A中有出现和B相同的结构和节点值。
*/
struct TreeNode
{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
//遍历A树 找到与B树的根节点值相同的节点
bool isSubStructure(TreeNode* A, TreeNode* B)
{
if (!A || !B) return false;
preorder(A, B);
return flag;
}
//比较是否相等
private:
int flag = 0;
void preorder(TreeNode* A, TreeNode* B)
{
//递归结束条件
if (!A) return;
if (A->val == B->val)
{
if (equal(A, B))
{
flag = 1;
return;
}
}
preorder(A->left, B);
preorder(A->right, B);
return;
}
bool equal(TreeNode* A, TreeNode* B)
{
//递归结束条件
//到分支的末尾
//如果B为空 A不为空返回true 反过来则不行
///如果b为空
if (!B) return true;
//如果b不为空 a为空
if (!A) return false;
//如果相等就继续比较左右 如果只有一方空
if (A->val == B->val)
{
//递归检查左右子树是否相同
int flag1 = equal(A->left, B->left);
int flag2 = equal(A->right, B->right);
return flag1 && flag2;
}
else
{
return false;
}
}
};
这里如果输入数据是double类型的话,可以参考剑指offer中的方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T4CwrKpr-1629084912956)(数据结构与算法.assets/image-20210609193307738.png)]
二刷递归
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2)
{
if(!pRoot1||!pRoot2) return false;
return fun(pRoot1,pRoot2);
}
bool fun(TreeNode* pRoot1, TreeNode* pRoot2) {
if(pRoot1==nullptr&&pRoot2==nullptr) return true;
if(pRoot1!=nullptr&&pRoot2==nullptr) return true;
if(pRoot1==nullptr&&pRoot2!=nullptr) return false;
if(pRoot1->val==pRoot2->val)
{
int flag= fun(pRoot1->left,pRoot2->left)&&fun(pRoot1->right,pRoot2->right);
if(flag) return true;
}
return fun(pRoot1->left,pRoot2)||fun(pRoot1->right,pRoot2);
}
};
面试题27 二叉树的镜像(递归、前序遍历)
请完成一个函数,输入一个二叉树,该函数输出它的镜像。(左右翻转)
前序遍历到每一个节点
利用temp交换左右
class Solution
{
public:
//递归到每一个节点
//利用两个temp交换左右
TreeNode* mirrorTree(TreeNode* root)
{
preorder(root);
return root;
}
private:
//前序遍历
void preorder(TreeNode* root)
{
//递归终止条件
if (!root) return;
//交换左右
TreeNode* temp = root->left;
root->left = root->right;
root->right = temp;
//递归交换左右
preorder(root->left);
preorder(root->right);
}
};c++
二刷:没啥问题 递归
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
//16:29
class Solution {
public:
//递归函数作用 :输入一个头节点 翻转左右
TreeNode* mirrorTree(TreeNode* root)
{
if(root==nullptr) return nullptr;
TreeNode* temp=root->left;
root->left=root->right;
root->right=temp;
mirrorTree(root->left);
mirrorTree(root->right);
return root;
}
};
面试题28 对称的二叉树(递归)
题目:请实现一个函数,用来判断一棵二叉树是不是对称的。如果一棵二叉树和它的镜像一样,那么它是对称的。
容易想到的解法 效率比较低
把两种遍历的结果存下来再比较是否相同
///
数值型转字符串
c++ to_string()可以把一个数值型转换为字符型
c sprintf()
sprintf_s(arr, 216, “%f”, dValue);
string型转数值型:
int 型 stoi string转int
double型 stod string转double
///
class Solution {
public:
//前序遍历(中左右)与前序对称遍历( 先中 再右 再左)相同则对称
//定义一个vector存储遍历的结果
bool isSymmetric(TreeNode* root)
{
preorded1(root);
preorded2(root);
//比较nums1和nums2是否相等
if (nums1.size() != nums2.size()) return false;
int a = nums1.size();
for (int i = 0; i < a; i++)
{
if (nums1[i] != nums2[i]) return false;
}
return true;
}
private:
vector<string> nums1;
vector<string> nums2;
//前序 null的位置用0表示
void preorded1(TreeNode* root)
{
if (!root)
{
nums1.push_back("nullptr");
return;
}
//
nums1.push_back(to_string(root->val));
preorded1(root->left);
preorded1(root->right);
}
//前序对称
void preorded2(TreeNode* root)
{
if (!root)
{
nums2.push_back("nullptr");
return;
}
//
nums2.push_back(to_string(root->val));
preorded2(root->right);
preorded2(root->left);
}
};
好一点的解法
利用递归推进
判断当前点是否相同、递归判断左右
class Solution
{
public:
//利用递归推进
bool isSymmetric(TreeNode* root)
{
return root == nullptr ? true : recur(root->left, root->right);
}
bool recur(TreeNode* left,TreeNode* right)
{
//左右都为空
if (left == nullptr && right == nullptr) return true;
//要么都为空 要么就都不为空且值相等
if (left == nullptr || right == nullptr || left->val != right->val) return false;
//当前是点是对称的
//左节点的右和右节点的左相同 左节点的左和右节点的右相同
return recur(left->left, right->right) && recur(left->right, right->left);
}
};
面试题29 顺时针打印矩阵(二维数组、模拟路径)
题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
模拟打印矩阵的路径 设置上下左右四个边界 并且维护他们
class Solution
{
public:
// 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
//设置上下左右四个边界 并且维护他们
//设置一个矩阵用于返回
vector<int> spiralOrder(vector<vector<int>>& matrix)
{
//闭区间
int row = matrix.size();
if(row==0) return {};
int col = matrix[0].size();
int left = 0;
int right = matrix[0].size()-1;
int up = 0;
int down = matrix.size()-1;
vector<int> ret;
//元素够了就退出
while (ret.size() < row*col)
{
//向右
for (int i = left; i <= right; i++)
{
ret.push_back(matrix[up][i]);
if (ret.size() == (row * col)) return ret;
}
//更新up
up++;
//向下
for (int i = up; i <= down; i++)
{
ret.push_back(matrix[i][right]);
if (ret.size() == (row * col)) return ret;
}
//更新right
right--;
//向左
for (int i = right; i >= left; i--)
{
ret.push_back(matrix[down][i]);
if (ret.size() == (row * col)) return ret;
}
//更新down
down--;
//向上
for (int i = down; i >= up; i--)
{
ret.push_back(matrix[i][left]);
if (ret.size() == (row * col)) return ret;
}
//更新left
left++;
}
return ret;
}
};
二刷:还是要打印才能debug出来
class Solution {
public:
//16:42
//15:15
//模拟 设置四周边界 向中键夹逼
vector<int> printMatrix(vector<vector<int> > matrix)
{
//设置初始边界
int left = 0, right = matrix[0].size() - 1;
int top = 0, bottom = matrix.size() - 1;
vector<int> res;
while (1)
{
//向右
for (int i = left; i <= right; i++)
{
//cout << matrix[top][i] << endl;
res.push_back(matrix[top][i]);
}
top++;
if (top > bottom) break;
//向下
for (int i = top; i <= bottom; i++)
{
//cout << matrix[i][right] << endl;
res.push_back(matrix[i][right]);
}
right--;
if (left > right) break;
//向左
for (int i = right; i >= left; i--)
{
//cout << matrix[bottom][i] << endl;
res.push_back(matrix[bottom][i]);
}
bottom--;
if (top > bottom) break;
//向上
for (int i = bottom; i >= top; i--)
{
// cout << matrix[i][left] << endl;
res.push_back(matrix[i][left]);
}
left++;
if (left > right) break;
}
return res;
}
};
面试题 30 包含min函数的栈(辅助栈)
题目:
定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
定义一个辅助栈 每次push时都同时向辅助栈中push当前的最小值
辅助栈的顶部始终为当前的最小值
#include<iostream>
using namespace std;
#include<stack>
#include <assert.h>
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(x);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->min();
*/
class MinStack {
public:
//正常栈
stack<int> s1;
//辅助栈
stack<int> s2;
/** initialize your data structure here. */
MinStack()
{
}
//向正常栈中插入元素时 同时把当前的最小值压入辅助栈 辅助栈栈顶为当前的最小值
//从正常栈中pop元素时 同时pop辅助栈的栈顶元素
void push(int x)
{
//如果是第一个元素
//新插入的元素小于min_num
if (s2.size() == 0 || x < s2.top())
{
s1.push(x);
s2.push(x);
}
// 新插入的元素大于min
else
{
s1.push(x);
s2.push(s2.top());
}
}
void pop()
{
assert(s1.size() > 0 && s2.size() > 0);
s1.pop();
s2.pop();
}
int top()
{
assert(s1.size() > 0 && s2.size() > 0);
return s1.top();
}
int min()
{
assert(s1.size() > 0 && s2.size() > 0);
return s2.top();
}
};
面试题31 栈的压入、弹出序列(模拟栈的操作
#include<iostream>
using namespace std;
#include<vector>
#include<stack>
//输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序
//假设压入栈的所有数字均不相等
class Solution
{
public:
//定义一个辅助栈来模拟入栈和出栈的过程
//入栈操作,每次按照压栈序列的顺序执行 使用for循环模拟
//出栈:每次入栈后循环判断“栈顶元素=弹出序列当前元素”是否成立,成立则弹出,弹出序列索引++
bool validateStackSequences(vector<int>& pushed, vector<int>& popped)
{
stack<int> s;
int index = 0; //弹出序列索引
for (int i = 0; i < pushed.size(); i++)
{
s.push(pushed[i]);
while (!s.empty()&&s.top() == popped[index])
{
s.pop();
index++;
}
}
//如果最后为空 就说明成功
return s.empty();
}
};
二刷 做的稍微有点麻烦
//13:19
class Solution
{
public:
//将压入序列逐步入栈直到等于弹出序列的第一个数
bool IsPopOrder(vector<int> pushV,vector<int> popV)
{
int index_pushV=0;
int index_popV=0;
stack<int> s;
s.push(pushV[index_pushV]);
index_pushV++;
while(1)
{
if(!s.empty()&&(s.top()==popV[index_popV]))
{
/// cout<<"pop "<<s.top()<<endl;
s.pop();
index_popV++;
if(index_popV==popV.size()) break;
}
else{
if(index_pushV==pushV.size()) break;
s.push(pushV[index_pushV]);
// cout<<"push "<<pushV[index_pushV]<<endl;
index_pushV++;
}
//cout<<index_pushV<<index_popV<<endl;
}
if(index_popV==popV.size()) return true;
else return false;
}
};
面试题32 (1)从上到下打印二叉树(层序遍历BFS)
//从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
//层序遍历 这玩意叫BFS 广度优先搜索
//每打印一个节点,就把他的子节点(如果有)放在一个队列的末尾
//然后从队列头部中取出一个节点,打印他,重复上述过程
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
struct TreeNode
{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
//从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
//层序遍历 这玩意叫BFS 广度优先搜索
//每打印一个节点,就把他的子节点(如果有)放在一个队列的末尾
//然后从队列头部中取出一个节点,打印他,重复上述过程
vector<int> levelOrder(TreeNode* root)
{
if (!root) return {};
queue<TreeNode*> q;
//先插入根节点
q.push(root);
vector<int> ret;
//如果队列为空就结束
while (!q.empty())
{
//打印这个节点
ret.push_back(q.front()->val);
//如果该节点左子树不为空 就插入队列
if (q.front()->left) q.push(q.front()->left);
//如果该节点右子树不为空 就插入队列
if (q.front()->right) q.push(q.front()->right);
//pop该节点
q.pop();
}
return ret;
}
};
面试题32 (2) 分行从上到下打印二叉树(层序遍历BFS拓展)
这里实际上就是 利用一个temp数组把这一层的节点存下来 节点的个数恰好时每次主循环进来时队列中剩余的元素个数
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
struct TreeNode
{
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
//从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
//层序遍历 这玩意叫BFS 广度优先搜索
//每打印一个节点,就把他的子节点(如果有)放在一个队列的末尾
//然后从队列头部中取出一个节点,打印他,重复上述过程
vector<vector<int>> levelOrder(TreeNode* root)
{
if (!root) return {};
queue<TreeNode*> q;
//先插入根节点
q.push(root);
vector<vector<int>> ret;
//如果队列为空就结束
while (!q.empty())
{
int cnt = q.size();//每次外层循环开始时,队列中
//定义一个辅助verctor同来存储这一层的节点
vector<int> temp;
//把这一层的节点插入到temp中
while (cnt--)
{
temp.push_back(q.front()->val);
//如果该节点左子树不为空 就插入队列
if (q.front()->left) q.push(q.front()->left);
//如果该节点右子树不为空 就插入队列
if (q.front()->right) q.push(q.front()->right);
//pop该节点
q.pop();
}
ret.push_back(temp);
}
return ret;
}
};
面试题32 (3) 之字形打印二叉树(层序遍历扩展、反转数组、双端队列)
题目:请实现一个函数按照之字形顺序打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右到左的顺序打印,第三行再按照从左到右的顺序打印,其他行以此类推。
这道题解法很多
可以用双端队列deque实现 参考这个https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/solution/cli-yong-dequeshuang-duan-dui-lie-hao-shi-0ms-ji-b/
可以用两个栈实现 参考剑指offer官方题解
取巧一点得话 用reverse函数
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
//从上到下打印出二叉树的每个节点,同一层的节点按照从左到右的顺序打印。
//层序遍历 这玩意叫BFS 广度优先搜索
//每打印一个节点,就把他的子节点(如果有)放在一个队列的末尾
//然后从队列头部中取出一个节点,打印他,重复上述过程
//定义一个flag来判断是奇数层还是偶数层 根节点计为第一层
//如果是偶数层就反转后输出
//reverse函数功能是逆序(或反转),多用于字符串、数组、容器。头文件是#include <algorithm>
// reverse函数用于反转在[first, last)范围内的顺序(包括first指向的元素,不包括last指向的元素),reverse函数无返回值
vector<vector<int>> levelOrder(TreeNode* root)
{
if (!root) return {};
queue<TreeNode*> q;
//先插入根节点
q.push(root);
vector<vector<int>> ret;
int flag = 1;
//如果队列为空就结束
while (!q.empty())
{
int cnt = q.size();//每次外层循环开始时,队列中
//定义一个辅助verctor同来存储这一层的节点
vector<int> temp;
//把这一层的节点插入到temp中
while (cnt--)
{
temp.push_back(q.front()->val);
//如果该节点左子树不为空 就插入队列
if (q.front()->left) q.push(q.front()->left);s
//如果该节点右子树不为空 就插入队列
if (q.front()->right) q.push(q.front()->right);
//pop该节点
q.pop();
}
if (flag % 2 == 0)//为偶数 反转后输出
{
reverse(temp.begin(), temp.end());
ret.push_back(temp);
}
else
{
ret.push_back(temp);
}
flag = !flag;
}
return ret;
}
};
面试题33:二叉搜索树的后序遍历序列(二叉搜索树、递归分治)
题目:输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。如果是则返回 true
,否则返回 false
。假设输入的数组的任意两个数字都互不相同。
处理一颗二叉树的遍历序列,可以先找到二叉树的根节点,再基于根节点把整棵树的遍历序列拆分成左子树对应的序列和右子树对应的序列,接下来再递归处理对应的子序列
利用递归拆分子问题
检查是否满足二叉搜索树的性质
这里注意二叉搜索树的性质是左子树的每个节点都小于根,右子树的每个节点都大于根
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
//二叉搜索树的性质
/* 若其左子树存在,则其左子树中每个节点的值都不大于该节点值;
若其右子树存在,则其右子树中每个节点的值都不小于该节点值。
2.没有键值相等的节点
*/
/*
输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历结果。
如果是则返回 true,否则返回 false。假设输入的数组的任意两个数字都互不相同。
*/
//后序遍历是左右根 所以最后一个节点是根
//又因为是搜索树 左小于根小于右
//所以根前面得序列中,存在一个拐点 前半部分是小于根得,后半是大于的
//通过递归实现
//先判断根节点 的左子树和右子树是否满足条件 再递归分别判断左子树 右子树
//
vector<int>res;
bool verifyPostorder(vector<int>& postorder)
{
res = postorder;
return dfs(0, res.size() - 1);
}
bool dfs(int left, int right)//闭区间
{
if (left >= right) return true;
int root = res[right];
int k = left;
while (res[k] < root) k++;//跳过所有的左子树 同时保证左子树区间的正确性
//每次判断 以当前k为界限的左右子树是否满足要求,由于左子树在划分区间时已经保证正确,就只用检查右子树是否正确
for (int i = k; i < right; i++)
{
if (res[i] < root) return false;
}
return dfs(left, k - 1) && dfs(k, right - 1);
}
};
二刷 :基本写出来了 边界粗心了
//14:21
class Solution {
public:
//二叉搜索树性质
//左子树全部元素<中<右子树全部元素
//后序遍历:左右中
//通过根节点划分左右
bool VerifySquenceOfBST(vector<int> sequence) {
if(sequence.size()==0) return false;
return fun(sequence,0,sequence.size()-1);
}
//判断区间内是否是一个二叉搜索树
bool fun(vector<int>& sequence,int left,int right) {
if(left>=right) return true;
//左子树右端点+1
int left_temp=left;
while(sequence[left_temp]<sequence[right])
{
left_temp++;
}
//判断
for(int i=left_temp;i<right;i++)
{
if(sequence[i]<sequence[right]) return false;
}
bool flag1=fun(sequence,left,left_temp-1);
bool flag2=fun(sequence,left_temp,right-1);
return flag1&&flag2;
}
};
面试题34 二叉树中和为某一值得路径(回溯法典型例题)
题目:输入一棵二叉树和一个整数,打印出二叉树中节点值的和为输入整数的所有路径。从树的根节点开始往下一直到叶节点所经过的节点形成一条路径。
注意恢复现场的技巧
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
//感觉是dfs的题目
//temp用来记录当前路径 如果满足条件就插入vector
vector<vector<int>> pathSum(TreeNode* root, int target)
{
//空
if (root == nullptr) return {};
dfs(root, target);
return ret;
}
private:
vector<vector<int>> ret;
vector<int> temp;
void dfs(TreeNode* root,int target)
{
if (root == nullptr) return;
int remain = target - root->val;
temp.push_back(root->val);
//满足条件
if (remain == 0&&!root->left&&!root->right)
{
ret.push_back(temp);
}
dfs(root->left, remain);
dfs(root->right, remain);
//恢复现场
temp.pop_back();
return;
}
};
二刷 自己写了一个 不一定对
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
//15:38
//回溯
class Solution {
public:
vector<vector<int>> res;
vector<vector<int> > FindPath(TreeNode* root,int expectNumber)
{
vector<int> path;
// path.push_back(root->val);
fun(path,root,expectNumber);
sort(res.begin(),res.end());
return res;
}
void fun(vector<int> & path,TreeNode* root,int target)
{
if(root==nullptr) return ;
path.push_back(root->val);
//是叶子节点
if(root->left==nullptr&&root->right==nullptr)
{
if(target==root->val) res.push_back(path);
return ;
}
fun(path,root->left,target-root->val);
path.pop_back();
// path.push_back(root->right->val);
fun(path,root->right,target-root->val);
path.pop_back();
return ;
}
};
面试题35 复杂链表的复制(哈希表、链表)
题目:请实现 copyRandomList 函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
主要的难点是如何知道random是指向后面的哪个指针,所以先遍历一遍,把每个节点记录下来
法一:利用哈希表来减少查找random得复杂度
法二:原地修改 拼接 + 拆分
考虑构建 原节点 1 -> 新节点 1 -> 原节点 2 -> 新节点 2 -> …… 的拼接链表,如此便可在访问原节点的 random 指向节点的同时找到新对应新节点的 random 指向节点。
- 当访问原节点
cur
的随机指向节点cur.random
时,对应新节点cur.next
的随机指向节点为cur.random.next
。
#include<iostream>
using namespace std;
#include<unordered_map>
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
/*请实现 copyRandomList 函数,复制一个复杂链表
在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
*/
class Solution {
public:
//法一:
/*1.遍历原链表创立各新节点,并把原链表的节点作为键 新链表节点作为值 存放在hash_map中
2.构建新链表的next和random指向,
3/返回新链表的头节点
*/
Node* copyRandomList(Node* head)
{
if (head == nullptr) return nullptr;
unordered_map<Node*, Node*> hash_map;
Node* cur = head;
//1.遍历原链表创立各新节点,并把原链表的节点作为键 新链表节点作为值 存放在hash_map中
while (cur)
{
hash_map[cur] = new Node(cur->val);
cur = cur->next;
}
//2.构建新链表的next和random指向,
cur = head;
while (cur)
{
hash_map[cur]->next = hash_map[cur->next];
hash_map[cur]->random = hash_map[cur->random];
cur = cur->next;
}
return hash_map[head];
}
//法二:
/*
// 1. 复制各节点,并构建拼接链表
2.构建各新节点的 random 指向
3. 拆分两链表
*/
Node* copyRandomList2(Node* head)
{
if (head == nullptr) return nullptr;
// 1. 复制各节点,并构建拼接链表
Node* cur = head;
while (cur)
{
Node* tmp = new Node(cur->val);
tmp->next = cur->next;
cur->next = tmp;
cur = cur->next->next;
}
// 2.构建各新节点的 random 指向
cur = head;
while (cur)
{
if (cur->random != nullptr)
{
cur->next->random = cur->random->next;
}
cur = cur->next->next;
}
// 3. 拆分两链表
Node* pre = head;//原链表
Node* res = head->next;//新链表头
Node* cur= head->next;//新链表
while (cur->next)
{
pre->next = pre->next->next;
cur->next = cur->next->next;
pre = pre->next;
cur = cur->next;
}
pre->next = nullptr;
return res;
}
};
二刷:还是没做出来
哈希表解法 重点是先记录老链表与新链表之间的映射
/*
struct RandomListNode {
int label;
struct RandomListNode *next, *random;
RandomListNode(int x) :
label(x), next(NULL), random(NULL) {
}
};
*/
//20:38
//首先遍历一遍创建新节点并且在哈希表中简历老节点到新节点的映射
//再连接
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead) {
unordered_map<RandomListNode*, RandomListNode*> hash_map;
RandomListNode* temp=pHead;
while(temp)
{
hash_map[temp]=new RandomListNode(temp->label);
temp=temp->next;
}
temp=pHead;
while(temp)
{
hash_map[temp]->next=hash_map[temp->next];
hash_map[temp]->random=hash_map[temp->random];
temp=temp->next;
}
return hash_map[pHead];
}
};
如果不能用辅助空间,那就在原链表上修改,对于每一个节点都复制一个同样的在该节点后面,然后再构建随机指针,再拆分。
面试题36 二叉搜索树与双向链表(中序遍历、双向链表)
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
考虑使用中序遍历访问树的各节点 cur ;并在访问每个节点时构建 cur 和前驱节点 pre 的引用指向;中序遍历完成后,最后构建头节点和尾节点的引用指向即可。
#include<iostream>
using namespace std;
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node() {}
Node(int _val) {
val = _val;
left = NULL;
right = NULL;
}
Node(int _val, Node* _left, Node* _right) {
val = _val;
left = _left;
right = _right;
}
};
/*
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。
要求不能创建任何新的节点,只能调整树中节点指针的指向。
*/
//二叉搜索树 左<根<右
//中序遍历 左->根->右
class Solution {
public:
Node* treeToDoublyList(Node* root) {
if (root == nullptr) return nullptr;
dfs(root);
head->left = pre;
pre->right = head;
return head;
}
private:
Node* pre, * head;
void dfs(Node* cur) {
if (cur == nullptr) return;
dfs(cur->left);
//中序遍历访问每个节点时构建cur和和pre的指向
if (pre != nullptr) pre->right = cur;
//当 pre 为空时: 代表正在访问链表头节点,记为 head
else head = cur;
cur->left = pre;
//更新
pre = cur;
dfs(cur->right);
}
};
二刷:没有思路
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};*/
class Solution {
public:
//暴力解:用数组存储中序遍历结果(有序的),再修改指针
//由于中序遍历二叉树搜索树 正好是按顺序遍历的,所以可以利用这个性质
//定义一个pre节点和一个cur节点
TreeNode* Convert(TreeNode* pRootOfTree)
{
fun(pRootOfTree);
//此时pre是最后一个节点
// pre->right=head;
// head->left=pre;
return head;
}
TreeNode* head=nullptr;
TreeNode* pre=nullptr;
void fun(TreeNode* root)
{
if(root==nullptr) return;
fun(root->left);
if(pre==nullptr) head=root;//双向链表的第一个节点
else
{
pre->right=root;
root->left=pre;
}
pre=root;
fun(root->right);
}
};
面试题37 序列化二叉树(前序遍历、字符串)
题目:
你需要设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
题目要求的 序列化 和 反序列化 是 可逆操作 。因此,序列化的字符串应携带 完整的二叉树信息 。
序列化方法:使用层序遍历BFS实现,并且把越过子节点的null也打印出来,从队列中拿出的null就不重新放进队列中了。
反序列化方法:利用队列按层构建二叉树,借助一个指针 i
指向节点 node
的左、右子节点,每构建一个 node
的左、右子节点,指针 i
就向右移动 11 位。
下面的解法是前序遍历,都是一样的只要把null记录下来
/
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
string res;
// Encodes a tree to a single string.
string serialize(TreeNode* root) {
dfs_s(root, res);
return res;
}
void dfs_s(TreeNode* root, string &res){
if(!root) {
res += "null ";//如果当前节点为空,保存null和一个空格
return ;
}
res += to_string(root->val) + ' ';//如果当前节点不为空,保存数字和一个空格
dfs_s(root->left, res);
dfs_s(root->right, res);
}
// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
int u = 0; //用来保存当前的字符串遍历的位置
return dfs_d(data, u);
}
TreeNode* dfs_d(string data, int &u){//这里的u是传的引用,不是值传递
if (u == data.size()) return NULL; //如果已经达到了字符串的尾端,则退出。
int k = u;
while(data[k] != ' ') k++; //k记录当前数字的位数如134是个三位数的数字,56是个两位数的数字,退出的时候,k指向了字符的中间的空格,所以回到下个字符的首部需要加1.
if(data[u] == 'n') {//如果当前字符串是“null”
u = k+1;//回到下一个数字的首部,注意是u = k+1, 不是u = u+1;
return NULL;//表示这次构造的是一个null节点,并没有孩子节点,所以跳过后面的递归
}
int val = 0;
//如果数字是负的
if(data[u] == '-'){
for (int i = u+1; i < k; i++) val = val * 10 + data[i] - '0';
val = -val;
}
else{
//如果是数字是正的
for (int i = u; i < k; i++) val = val * 10 + data[i] - '0';
}
u = k + 1;//回到下个数字的首部
//递归算法总是先写退出条件,然后才递归调用。
auto root = new TreeNode(val);
root->left = dfs_d(data, u);
root->right = dfs_d(data, u);
return root;
}
};
面试题38 字符串的排列(全排列问题、回溯)
题目:输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
这里是经典回溯算法中的排列问题
https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw
递归想不出来啊
全排列问题
#include<iostream>
using namespace std;
#include<vector>
#include<set>
//输入一个字符串,打印出该字符串中字符的所有排列
//你可以以任意顺序返回这个字符串数组,但里面不能有重复元素
class Solution
{
public:
vector<string> permutation(string s)
{
dfs(s, 0);
return res;
}
private:
vector<string> res;
//固定一个字母,求后面部分的全排列
//然后固定另一个字母 重复操作
//x就是这次要固定字符串的哪一位
void dfs(string s, int x)
{
if (x == s.size() - 1)
{
//
res.push_back(s);//一种排列结束后 添加排列方案
return;
}
set<char> st;
for (int i = x; i < s.size(); i++)
{
//如果做过第一的字母就不要再做第一了
if (st.find(s[i]) != st.end()) continue; 重复,因此剪枝
st.insert(s[i]);//把做过第一的字母插入set中 避免重复
//把一个字母移到未固定序列的开头
swap(s[i], s[x]);// 交换,将 s[i] 固定在第 x 位
dfs(s, x + 1);// 开启固定第 x + 1 位字符
//固定到最后一位就可以p
swap(s[i], s[x]); // 恢复交换 其实不恢复也可以过吧
}
}
};
二刷:还是不熟练 标准回溯解法
class Solution {
public:
//全排列
vector<string> Permutation(string str)
{
string temp;
if(str.size()==0) return {};
sort(str.begin(),str.end());
// unordered_set<char> hash_set;
vector<bool> used(str.size(),false);
dfs(temp,str,used);
return res;
}
vector<string> res;
void dfs(string &temp,string &str,vector<bool>& used)
{
if(temp.size()==str.size())
{
res.push_back(temp);
return ;
}
for(int i=0;i<str.size();i++)
{
if(i>=1&&str[i]==str[i-1]&&used[i-1]==false) continue;
if(used[i]==true) continue;
// cout<<str[i]<<endl;
temp.push_back(str[i]);
used[i]=true;
dfs(temp,str,used);
// cout<<"pop "<<*temp.rbegin()<<endl;
used[i]=false;
temp.pop_back();
}
}
};
面试题39 数组中出现超过一半的数字(摩尔投票法)
排序
class Solution
{
public:
/*数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
*/
//排序 + for计数 0(n) 直接输出排序后数组的中位数
//哈希 for 存进哈希表
int majorityElement(vector<int>& nums)
{
int size = nums.size();
if (size == 0) return 0;
if (size == 1) return nums[0];
int mid = size / 2 + 1;
sort(nums.begain(), nums.end());
return nums[mid];
}
};
哈希表
//维护一个最大值
int majorityElement1(vector<int>& nums)
{
//键是元素 值是个数
unordered_map<int, int> hash_map;
int size = nums.size();
assert(size>0);
int max_count = 0;
int res;
for (int i = 0; i < size; i++)
{
hash_map[nums[i]]++;
if (hash_map[nums[i]] > max_count)
{
max_count = hash_map[nums[i]];
res = nums[i];
}
}
return res;
}
Boyer-Moore 投票算法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bezwEtbT-1629084912957)(数据结构与算法.assets/image-20210618151841781.png)]
//投票
int majorityElement2(vector<int>& nums)
{
int res;//众数
int count;//次数
int size = nums.size();
assert(size > 0);
res = nums[0];
count = 1;
for (int i = 1; i < size; i++)
{
if (count == 0) res = nums[i];
if (res == nums[i])
{
count++;
}
else
{
count--;
}
}
return res;
}
二刷:投票法写出来了
其实时间复杂度的话,哈希表可能更快,因为可以提前返回
class Solution {
public:
//投票法 跟前面相同就+1 不同就减1 如果减到0了 下一个就重新置1
int MoreThanHalfNum_Solution(vector<int> numbers) {
int res=numbers[0];
int nums=1;
for(int i=1;i<numbers.size();i++)
{
if(nums==0)
{
res=numbers[i];
nums=1;
continue;
}
if(numbers[i]==res)
{
nums++;
}
else{
nums--;
}
}
return res;
}
};
面试题40 最小的k个数(优先队列(最大堆)的应用、快排的应用)
1.优先队列(最大堆)
默认的优先队列 priority_queue q是从大到小排列的
快排在划分的时候 那个用来分界的元素如果等于k,就可以返回了
头文件:#include<queue>
操作:
top 访问队头
empty 队列是否为空
size 返回队列元素个数
push 插入元素到队尾
pop 弹出队头
swap 交换内容
升序队列
priority_queue <int,vector<int>,greater<int> > q;
降序队列 从大到小
priority_queue <int,vector<int>,less<int> >q;//简写 priority_queue <int> q;
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
#include<assert.h>
//输入整数数组 arr ,找出其中最小的 k 个数。
//例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
class Solution
{
public:
//维护一个自动排序的数据结构 优先队列 从大到小 我们的目的是最后在这个留下最小的四个数
//先插入k个数 然后后面的数依次与队头比较 如果比对头小 就把对头出队 把该数据入队
//最后把优先队列中的数据输出到数组
/* top 访问队头
empty 队列是否为空
size 返回队列元素个数
push 插入元素到队尾
pop 弹出队头
swap 交换内容
*/
vector<int> getLeastNumbers(vector<int>& arr, int k)
{
assert(k <= arr.size());
vector<int> res;
priority_queue<int> q;
for (int i = 0; i < k; i++)
{
q.push(arr[i]);
}
for (int i = 0; i < arr.size(); i++)
{
if (arr[i] < q.top())
{
q.pop();
q.push(arr[i]);
}
}
int size = q.size();
for (int i = 0; i < size; i++)
{
res.push_back(q.top());
q.pop();
}
return res;
}
};
2.快排变形 效率高
class Solution {
private:
vector<int> res;
public:
vector<int> getLeastNumbers(vector<int>& arr, int k) {
if(arr.empty() || k == 0) {return {};}
return quickSelection(arr, 0, arr.size() - 1, k - 1); // 注意第 k 个数对应的下标是 k - 1
}
vector<int> quickSelection(vector<int>& arr, int left, int right, int index) {
// partition函数将一个区间内所有小于下标为 j 的数放在 j 左边,大于下标为 j 的数放在 j 右边
int j = partition(arr, left, right);
if(j == index) { // 若 j 刚好等于 k - 1,将 arr[0] 至 arr[j] 输入 res
for(int i = 0; i < j + 1; ++i) {res.push_back(arr[i]);}
return res;
}
// 若 j 小于 k - 1,将区间变成 [j + 1, right];反之,区间变成 [left, j - 1]
return j < index ? quickSelection(arr, j + 1, right, index) : quickSelection(arr, left, j - 1, index);
}
int partition(vector<int>& arr, int left, int right) {
int value = arr[left];
int i = left, j = right + 1;
while(true) {
while(++ i <= right && arr[i] < value); // 找到从左往右第一个大于等于 value 的下标
while(-- j >= left && arr[j] > value); // 找到从右往左第一个小于等于 value 的下标
if(i >= j) {break;} // 如果找不到,说明已经排好序了,break
swap(arr[i], arr[j]); // 如果找到了,交换二者
}
swap(arr[left], arr[j]); // arr[j]是小于 value 的,这一步使得所有小于下标为 j 的数都在 j 左边
return j;
}
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
};
二刷:快排还是不熟练
能用优先队列最简单了,维护一个四个数的大根堆(最大的在顶部) 然后逐个插入 最后留下的四个数就是最小的
快排解法
class Solution {
public:
//优先队列
//快排变形 哨兵的下标>=k 就返回
vector<int> GetLeastNumbers_Solution(vector<int> input, int k) {
quick_sort(input, 0, input.size()-1, k);
vector<int> res;
res.assign(input.begin(),input.begin()+k);
return res;
}
void quick_sort(vector<int>& input,int left,int right,int k)
{
if(left>=right) return;
int i=left-1,j=right+1;
int mid=left+right>>1;
int temp=input[mid];
while(i<j)
{
do i++;while(input[i]<temp);
do j--;while(input[j]>temp);
if(i<j) swap(input[i],input[j]);
}
if(j>=k-1) return quick_sort(input, left, j,k);
else return quick_sort(input, j+1, right,k);
}
};
面试题41 数据流中的中位数(最大堆和最小堆的应用)
题目:如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
核心思想:始终保持最大堆保存较小的一半,最小堆保存较大的一半
#include<iostream>
using namespace std;
#include<queue>
#include<assert.h>
//定义一个最大堆(堆顶为最大值)左边 和一个最小堆(堆顶为最小值) 右边
//一个数据过来,如果两个堆的元素数量相等时 最大堆插入一个元素 反之最小堆插入一个元素(不是直接插入)
//为了保证最大堆中所有的数据都是小于最小堆的
//定义一个规则,所有本来要插入最大堆的,都必须先插入最小堆 再从最小堆堆顶pop一个元素擦汗如最大堆 反之同理
//查询:如果两个堆的元素数量相等时 返回最大堆最大的元素 否则返回最大堆最大+最小堆最小的元素/2
class MedianFinder
{
public:
priority_queue<int, vector<int>, less<int> > q_left;//最大堆 位于左边
priority_queue<int, vector<int>, greater<int> > q_right;//最小堆 位于右边
/** initialize your data structure here. */
MedianFinder()
{
}
void addNum(int num)
{
if (q_left.size() == q_right.size())//左边堆插入一个元素
{
//先插入右边
q_right.push(num);
//把此时右边最小的插入左边
q_left.push(q_right.top());
//右边最小的出队
q_right.pop();
}
else
{
//先插入左边
q_left.push(num);
//把此时左边最大的的插入右边
q_right.push(q_left.top());
//左边最大的出队
q_left.pop();
}
}
double findMedian()
{
assert(q_left.size() != 0);
if (q_left.size() == q_right.size())
{
return (q_left.top() + q_right.top()) / 2.0;
}
else
{
return q_left.top()/1.0;
}
}
};
/**
* Your MedianFinder object will be instantiated and called as such:
* MedianFinder* obj = new MedianFinder();
* obj->addNum(num);
* double param_2 = obj->findMedian();
*/
面试题42 连续子数组的最大和(动态规划)
题目:输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
dp[i]为以i结尾的连续子数组最大和
利用循环从小到大计算dp
动态规划
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
/*输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
*/
class Solution
{
public:
//暴力 求出以每个元素开头数组的所有情况 不满足时间复杂度
//定义一个变量s为 以s前一个变量结尾的数组的和的最大值
//更新s:s>0 s+x ; s<0 x
/*
(动态规划)
s这个变量中存储的是 以前一个数结尾的子数组中,和最大的是多少
如果s <= 0,那么就将s置为0,因为可能存在负数,不能将负收益的s加进来
如果s > 0,就让s += x。
因为是求最大值,所以res的初值置为 无穷小INT_MIN。
同时,每一次迭代,都要更新res,也就是res = max(res, s)。最后返回的res就是 最大值。
*/
int maxSubArray(vector<int>& nums)
{
int res = INT_MIN;//记录了此时的最大值
int s = 0;//s这个变量中存储的是 以前一个数结尾的子数组中,和最大的是多少
for (int i = 0; i < nums.size(); i++)
{
if (s <= 0) s = nums[i];//如果s小于0 则不如不要前面的 直接取当前元素
else s +=nums[i];//s大于0 则累加上
res = max(res, s);//更新res 记录出现过的最大的s
}
return res;
}
};
二刷:动态规划简单题 就是容易往滑动窗口上想,那样就复杂很多了
//15:39
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
vector<int> dp(array.size(),0);
dp[0]=array[0];
int max=INT_MIN;
for(int i=1;i<array.size();i++)
{
if(dp[i-1]<0) dp[i]=array[i];
else dp[i]=dp[i-1]+array[i];
if(dp[i]>max) max=dp[i];
}
return max;
}
};
滚动数组优化
//15:39
class Solution {
public:
int FindGreatestSumOfSubArray(vector<int> array) {
// vector<int> dp(array.size(),0);
// dp[0]=array[0];
int dp1=array[0];
int dp2;
int max=INT_MIN;
for(int i=1;i<array.size();i++)
{
if(dp1<0) dp2=array[i];
else dp2=dp1+array[i];
if(dp2>max) max=dp2;
dp1=dp2;
}
return max;
}
};
面试题 43 1-n整数中1出现的次数(不会做)
#include<iostream>
using namespace std;
/*输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
*/
class Solution
{
public:
//遍历 n 累计次数
//通过%10判断末尾是否是1,然后除10,去掉最后一位继续判断下一位 如果检测到了1 就次数加1 继续检测下一个数
//将 11 ~ nn 的个位、十位、百位、...的 11 出现次数相加,即为 11 出现的总次数
//求每一位是1时 有多少次 相当于把这一位固定为1求其他位有多少种情况
//例如
/*
case 1: cur=0
2 3 0 4
千位和百位可以选00 01 02....22 十位可以取到1( 形如[00|01..|22]1[0-9] 都是<2304 ) 个位可以选0-9 共有 23 * 10 中排列
当千位和百位取23,如果十位取1 那就是形如 231[0-9] > 2304,所以当千位和百位取23,十位只能能取0,个位取0-4即 2300 2301 2302 2303 2304
但是2301不应该算进来,这个1是 单独 出现在个位的(而11,121,111这种可以被算多次)
即 23*10
case 2: cur=1
2 3 1 4
千位和百位可以选00 01 02....22 十位可以取到1 个位可以选0-9 共有 23 * 10 中排列
当千位和百位取23,十位取1,个位可以去0-4 即 2310-2314共5个
即 23 *10 + 4 +1
case 3: cur>1 即2-9
2 3 2 4
千位和百位可以选00 01 02....22 十位可以取到1(形如 [00|01...|22]1[0-9] 都是<2324) 个位可以选0-9 共有 23 * 10 中排列
当千位和百位取23,十位取1,个位可以去0-9 即 2310-2319共10个 (其中2311,被计算了两次,分别是从个位和十位分析得到的1次)
即 23 *10 + 10
*/
int countDigitOne(int n)
{
//首先让 digit = 1(表示个位)
long digit = 1;// digit 需为 long 型,因为比如 n 是 INT_MAX,digit 会在最后一次循环越界
int high = n / 10;//表示个位左边的全部
int cur = n % 10; //表示个位上的数
int low = 0;//表示个位右边的全部,也就是没有
int res = 0;
while (high != 0 || cur != 0)
{
//接下来是根据n当前位的数字分情况讨论
if (cur == 0)
{
res += high * digit;
}
else if(cur==1)
{
res += high * digit + low + 1;
}
else
{
res += (high + 1) * digit;
}
low += cur * digit;//更新低位
cur = high % 10;//更新当前位
high /= 10;//更新高位
digit *= 10;//更新位因子
}
return res;
}
};
二刷:依旧不会
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hKWeCAyf-1629084912958)(数据结构与算法.assets/image-20210815213656175.png)]
y总代码
class Solution {
public:
int NumberOf1Between1AndN_Solution(int n) {
if(!n) return 0;
vector<int> number;
while(n) number.push_back(n % 10) , n /= 10;//取出每一位
int res = 0;
//计算每一位是1的所有情况
//abcdef
//假设在讨论c
for(int i = number.size() - 1; i >= 0; i--)
{
auto left = 0, right = 0, t = 1;//讨论的这一位左边的数和右边的数
for(int j = number.size() - 1; j > i; j--) left = left * 10 + number[j];//计算i左边的数 ab
for(int j = i - 1; j >= 0; j--) right = right * 10 + number[j], t *= 10;//计算i右边的数下·def
res += left * t; //00-(ab-1) *000-999
//ab
if(number[i] == 1) res += right + 1; //c==1
else if(number[i] > 1) res += t; //c>1
}
return res;
}
};
面试题44 数字序列中某一位的数字(数位计算、求整、求余)
题目:数字以0123456789101112131415…的格式序列化到一个字符序列中。在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。
请写一个函数,求任意第n位对应的数字。
/*
1.确定结果是在几位数中(循环执行 nn减去 一位数、两位数、… 的数位数量 count ,直至 n ≤count 时跳出)
2.确定是几位数的第几个数
3.属于那个数的第几位
*/
数位计算题目 类似智力题?
#include<iostream>
using namespace std;
#include<string>
/*数字以0123456789101112131415…的格式序列化到一个字符序列中。
在这个序列中,第5位(从下标0开始计数)是5,第13位是1,第19位是4,等等。
请写一个函数,求任意第n位对应的数字。
*/
/*
暴力 拼接成字符串再查找
10+20+30
*/
/*
1.确定结果是在几位数中
2.确定是几位数的第几个数
3.属于那个数的第几位
*/
class Solution
{
public:
int findNthDigit(int n)
{
long long i = 1;///i表示是几位数
int s = 9;//s表示位数共有多少个
int base = 1;//,base表示位数的起始值
while (n > i * s) , i * s表示当前位数总共占多少位
{
n -= i * s;//n先减掉个位数 再减掉10位数 。。
i++;//1 2 3 4 5
s *= 10;//9, 90, 900, 9000, 90000
base *= 10;//10 100 1000 10000
}
int number = base + (n + i - 1) / i - 1; //求位数的第几个数, 1000 - 9 - 180 = n , n / 3 + base - 1(考虑0故减1), 向上取整 n + i - 1。
int r = n % i ? n % i : i; // 除不尽就是第几位,除尽力了就是最后一位。
for (int j = 0; j < i - r; j++) number /= 10; //求数的第i - r位,取出第i - r位。
return number % 10;
}
};
面试题 45 把数组排成最小的数(自定义排序、改写快排)
自定义sort函数中的compare规则
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<string>
/*
输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个
*/
//自己定义一个比较两个数大小的函数 也就是比较mn和nm哪个大
class Solution
{
public:
string minNumber(vector<int>& nums)
{
string res;
sort(nums.begin(), nums.end(), compare);
for (int& num : nums)
{
res += to_string(num);
}
return res;
}
private:
//把两个数转成字符串 并且比较mn和nm两种拼接策略哪个更小
static bool compare(int& x, int& y)
{
string temp_xy= to_string(x)+ to_string(y);
string temp_yx = to_string(y) + to_string(x);
/*c++ 中使用> < 默认从从字符串第一位开始比,比如两个字符串s1和s2
如果s1[0] < s2[0] 就s1小(比较字符的ASCII值)
如果相等,就判断s1[1]和s2[1] 一直到某个字符串没有下一位为止*/
return temp_xy < temp_yx;
}
};
改写快排
class Solution {
public:
string minNumber(vector<int>& nums) {
vector<string> strs;
for(int i = 0; i < nums.size(); i++)
strs.push_back(to_string(nums[i]));
quick_sort(strs, 0, strs.size() - 1);
string res;
for(string s : strs)
res.append(s);
return res;
}
private:
void quick_sort(vector<string>& nums, int left, int right)
{
if (left >= right)
return;
int i = left - 1, j = right + 1 ;
// srand(time(0)); //设置时间种子 取系统时间
//rand()产生一个0-32767的随机数 所以randoms为left-right之间的随机数
//int randoms = rand() % (right - left + 1) + left;//在left和right中取一个随机数
int x_index = left + right >> 1;
string x = nums[x_index];
while (i < j)
{
do i++; while (nums[i]+x < x+nums[i]);
do j--; while (nums[j]+x > x+nums[j]);
if (i < j)swap(nums[i], nums[j]);
}
quick_sort(nums, left, j), quick_sort(nums, j + 1, right);
}
};
二刷:没做出来 想不到
class Solution {
public:
string PrintMinNumber(vector<int> numbers) {
string res;
vector<string> temp;
for(auto &s:numbers)
{
temp.push_back(move(to_string(s)));
}
sort(temp.begin(),temp.end(),[](string &a,string &b){return a+b<b+a;});
for(auto& s :temp)
{
res+=s;
}
return res;
}
};
面试题46 把数字翻译成字符串(递归、动态规划)
题目:给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
这个问题本质是一个青蛙跳台阶的题目,我们可以用f(n) 表示以第 n位结尾的前缀串翻译的方案数,f(n)可以由f(n-1)得到 或者当并且第 n-1 位和第n位形成的数字x 满足 10≤x≤25时,也可以由f(n-2)得到
#include<iostream>
using namespace std;
#include<string>
/*
给定一个数字,我们按照如下规则把它翻译为字符串:
0 翻译成 “a” ,1 翻译成 “b”,……,11 翻译成 “l”,……,25 翻译成 “z”。
一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
*/
class Solution
{
public:
/*0-9 对应 a b c d e f g h i j
10-25 i-z
跳1步和跳2步的问题? 去掉 超过>25的两位数
*/
int translateNum(int num)
{
if (num < 0) return 0;
string nums =to_string(num);
return fib(nums);
}
//f(n)=f(n-1)+f(n-2)
//自下往上 用迭代实现
//设f(n)为从以nums[n]结尾的字符串的翻译方法种类(nums[0]-num[n])
int fib(string &nums)
{
int n = nums.size();
if (n == 1) return 1;
int f_one = 1;//f(0)
int f_two;//f(1)
//判断f2是否10-25
int digit1 = nums[1]-'0';
int digit2= nums[0]-'0';
int converted = digit2 * 10 + digit1;
if (converted >= 10 && converted <= 25)
{
f_two = 2;//f(1)
}
else
{
f_two = 1;//f(1)
}
int f_n = f_two;;//f(n)
//f(n)为result
for (int i = 2; i < n; i++)
{
//判断f(N)是等于f(n-1)+f(n-2)还是f(n-1)
int digit1 = nums[i] - '0';
int digit2 = nums[i - 1] - '0';
int converted = digit2 * 10 + digit1;
if (converted >= 10 && converted <= 25)
{
f_n = f_one + f_two;//f(n)=f(n-1)+f(n-2)
}
else
{
f_n = f_two;//f(n)=f(n-1)
}
f_one = f_two;
f_two = f_n;
}
return f_n;
}
};
很强的递归简写
class Solution {
public:
int translateNum(int num)
{
//递归结束条件
if (num < 10) return 1;
//判断最后两位是否位于10到25之间
int re = num % 100;
if (re >= 10 && re <= 25)
return translateNum(num / 10) + translateNum(num / 100);
else
return translateNum(num / 10);
}
};
面试题47 礼物的最大价值(经典动态规划)
这题直接递归可以但会超时
递归加记录数组 (不推荐)
class Solution {
int maxValue(vector<vector<int>>& grid)
{
int row = grid.size();//行‘
if (row == 0) return 0;
int col = grid[0].size();//列
vector<vector<bool>> flag(row, vector<bool>(col, false));
return find(grid, grid.size() - 1, grid[0].size() - 1, flag);
}
int find(vector<vector<int>>& grid, int i, int j, vector<vector<bool>>& log)
{
if (i == 0 && j == 0) return grid[0][0];
if (i > grid.size() - 1 || i<0 || j>grid[0].size() - 1 || j < 0)
return -1 ;//避免越界;返回没有越界的值,(礼物价值大于0)
if (log[i][j] == true)
return grid[i][j];//递归过的礼物直接返回
grid[i][j] = grid[i][j] + max(find(grid, i - 1, j, log), find(grid, i, j - 1, log)); //每次取左边和上边的较大值
log[i][j] = true;
return grid[i][j];
}
};
做法是基于循环的动态规划
经典dp
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2nIkF0il-1629084912959)(数据结构与算法.assets/image-20210713102218485.png)]
#include<iostream>
using namespace std;
#include<algorithm>
#include<vector>
/*在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。
你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。
给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
*/
class Solution
{
public:
//优化 递归存在大量重复计算 直接会超时’
//定义一个二维数组dp保存到达坐标为(i,j)的格子时能拿到的礼物价值总和的最大值
//由于dp[i][j]= max(dp[i][j - 1], dp[i - 1][j])+grid[i][j];
//但第一行和第一列只可能从他前面的一个数得到 所以先初始化第一行和第一列
//相当于先从前开始计算 逐渐推后面的
int maxValue(vector<vector<int>>& grid)
{
int row = grid.size();//行
if (row == 0) return 0;
int col = grid[0].size();//列
vector<vector<int>> dp(row, vector<int>(col));
dp[0][0] = grid[0][0];
for (int j = 1; j < col; j++) // 初始化第一行
dp[0][j] = grid[0][j]+dp[0][j - 1];
for (int i = 1; i < row; i++) // 初始化第一列
dp[i][0] = grid[i][0]+dp[i - 1][0];
for (int i = 1; i < row; i++)
{
for (int j = 1; j < col; j++)
{
dp[i][j] =max(dp[i][j - 1], dp[i - 1][j])+grid[i][j];
}
}
return dp[row - 1][col - 1];
}
};
优化 在原数组上修改 意义不大的样子
#include<iostream>
using namespace std;
#include<algorithm>
#include<vector>
/*在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。
你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。
给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
*/
class Solution
{
public:
//经典dfs 求出所有的可能 取最大值/
//优化 递归存在大量重复计算 直接会超时’
//定义一个二维数组dp保存到达坐标为(i,j)的格子时能拿到的礼物价值总和的最大值
//由于dp[i][j]= max(dp[i][j - 1], dp[i - 1][j])+grid[i][j];
//但第一行和第一列只可能从他前面的一个数得到 所以先初始化第一行和第一列
//相当于先从前开始计算 逐渐推后面的
int maxValue(vector<vector<int>>& grid)
{
int m = grid.size(), n = grid[0].size();
for (int j = 1; j < n; j++) // 初始化第一行
grid[0][j] += grid[0][j - 1];
for (int i = 1; i < m; i++) // 初始化第一列
grid[i][0] += grid[i - 1][0];
for (int i = 1; i < m; i++)
for (int j = 1; j < n; j++)
grid[i][j] += max(grid[i][j - 1], grid[i - 1][j]);
return grid[m - 1][n - 1];
}
};
面试题48 最长不包含重复字符的子字符串(动态规划+哈希表、双指针滑动窗口)
题目:请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
滑动窗口维护当前不重复子字符串 开哈希表判断是否重复 哈希表中存放每个字符最后一次出现的位置索引
#include<iostream>
#include<string>
using namespace std;
#include<vector>
#include<unordered_map>
#include <algorithm>
/*
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
*/
//暴力解法
//两个for 一个指向字符串的开头 一个指向字符串的结尾 找到所有的子串 判断不重复子串的最大长度(哈希)
class Solution
{
public:
/*动态规划+哈希表
dp[j]为以字符s[j]结尾的 “最长不重复子字符串” 的长度,i为与j重复的左边最近的字符 如果没有i设为-1
dp[j]=dp[j-1]+1 , dp[j-1]<j-i 重复字符i在区间之外
dp[j]=j-i ,dp[j-1]>=j-i 重复字符i在区间之内
哈希表存储各字符最后一次出现的索引位置 键:字符 值 字符最后一次出现的索引
*/
int lengthOfLongestSubstring(string s)
{
if (s.size() < 1) return 0;
//定义一个dp数组
vector<int> dp;
dp.resize(40001);
//定义一个哈希表
unordered_map<char, int> hash_map;
for (int j = 0; j < s.size(); j++)
{
if (j == 0)
{
dp[0] = 1;
// hash_map.insert(make_pair(s[j], j));
hash_map[s[j]]=j;
continue;
}
auto it = hash_map.find(s[j]);
int i; //i为与j重复的左边最近的字符
if (it == hash_map.end()) i = -1;
else i = it->second;
//更新dp
//dp[j-1]<j-i
if ( dp[j - 1] < j - i)
{
dp[j] = dp[j - 1] + 1;
}
else
{
dp[j] = j - i;
}
//把当前字符及索引记录到哈希表中
hash_map[s[j]] = j;
// hash_map.insert(make_pair(dp[j], j));
}
int max = *max_element(dp.begin(), dp.end());
return max;
}
};
由于只要求最大值 ,所以可以不要把每一个dp[j]得值都记录下来
class Solution
{
public:
/*动态规划+哈希表
dp[j]为以字符s[j]结尾的 “最长不重复子字符串” 的长度,i为与j重复的左边最近的字符 如果没有i设为-1
dp[j]=dp[j-1]+1 , dp[j-1]<j-i 重复字符i在区间之外
dp[j]=j-i ,dp[j-1]>=j-i 重复字符i在区间之内
哈希表存储各字符最后一次出现的索引位置 键:字符 值 字符最后一次出现的索引
*/
int lengthOfLongestSubstring(string s)
{
if (s.size() < 1) return 0;
//定义一个dp数组
/* vector<int> dp;
dp.resize(40001);*/
int dp;//只存放的值
//定义一个哈希表
unordered_map<char, int> hash_map;
int max = 1;
for (int j = 0; j < s.size(); j++)
{
if (j == 0)
{
dp = 1;
hash_map.insert(make_pair(s[j], j));
continue;
}
auto it = hash_map.find(s[j]);
int i; //i为与j重复的左边最近的字符
if (it == hash_map.end()) i = -1;
else i = it->second;
//更新dp
//dp[j-1]<j-i
if ( dp < j - i)
{
dp = dp + 1;
}
else
{
dp = j - i;
}
//把当前字符及索引记录到哈希表中
hash_map[s[j]] = j;
// hash_map.insert(make_pair(dp[j], j));
if (dp > max) max = dp;
}
return max;
}
};
双指针加滑动窗口
//双指针 滑动窗口维护求最大子串 定义一个max记录当前的最大长度
//开一个哈希表记录每个字符最后一次出现的位置索引
int lengthOfLongestSubstring(string s)
{
if (s.size() < 1) return 0;
//定义一个哈希表
unordered_map<char, int> hash_map;
int result = 1;
int left = 0;//左指针
int right = 0;//右指针
for (; right < s.size()-1; right++)
{
auto it = hash_map.find(s[right]);
//如果当前right到达的元素在哈希中发现重复的 就把left指向重复元素索引的下一个
if (it != hash_map.end())
left = max(left,it->second);
//滑过的字符存放在哈希中
hash_map[s[right]] = right;
//计算当前长度并更新
int len = right - left;
if (len > result) result = len;
}
return result;
}
面试题49 丑数(动态规划、三指针)
题目:我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
这个好理解
https://leetcode-cn.com/problems/chou-shu-lcof/solution/zhu-shi-xing-ti-jie-dong-tai-gui-hua-jia-bhx2/
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
/*
我们把只包含质因子 2、3 和 5 的数称作丑数(Ugly Number)。求按从小到大的顺序的第 n 个丑数。
*/
class Solution {
public:
//由于丑数=较小的一个丑数乘以2 3 5得到
//定义三个指针分别指向包含2因子的序列 包含3因子的序列 包含5因子的序列
int nthUglyNumber(int n)
{
int a=0, b=0, c = 0;
vector<int> dp(n);
dp[0] = 1;
for (int i = 1; i < n; i++)
{
dp[i] = min(min(dp[a] * 2, dp[b] * 3), dp[c] * 5);
if (dp[i] == dp[a]*2) a++;
if (dp[i] == dp[b]*3) b++;
if (dp[i] == dp[c]*5) c++;
}
return dp[n - 1];
}
};
面试题50 第一个只出现一次的字符(字符串、哈希表)
题目:在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。 s 只包含小写字母。
#include<iostream>
using namespace std;
#include<unordered_map>
class Solution
{
public:
//遍历整个数组 用哈希存放遍历过的数字 如果重复则计数加1
//从开头开始 遍历查找
char firstUniqChar(string s)
{
unordered_map<char, int> hash_map;
for (int i=0; i < s.size(); i++)
{
auto it = hash_map.find(s[i]);
if (it == hash_map.end()) hash_map[s[i]] = 1;
else hash_map[s[i]]++;
}
for (int i = 0; i < s.size(); i++)
{
if (hash_map[i] == 1) return s[i];
}
return ' ';
}
};
优化 由于字符只有256 所以可以开一个256的数组 来代替哈希表 减少空间消耗
#include<iostream>
using namespace std;
#include<unordered_map>
class Solution
{
public:
//遍历整个数组 用哈希存放遍历过的数字 如果重复则计数加1
//从开头开始 遍历查找
//优化 用一个数组模拟哈希表 以字符ASCALL为键 以每个字符出现的次数为值
char firstUniqChar(string s)
{
if(s.size()==0) return ' ';
// unordered_map<char, int> hash_map;
vector<int> hash_map2;
hash_map2.resize(256);
for (int i=0; i < s.size(); i++)
{
hash_map2[s[i]]++;
}
for (int i = 0; i < s.size(); i++)
{
// cout<<hash_map2[s[i]]<<endl;
if (hash_map2[s[i]] == 1) return s[i];
}
return ' ';
}
};
面试题 51 数组中的逆序对(归并排序、分治算法)
这个题虽然是一道hard题但实际上只需要在归并排序的基础上加两行代码就行了
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
class Solution
{
public:
/*
在数组中的两个数字,如果前面一个数字大于后面的数字,
则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。
*/
/*
暴力 每拿到一个数就查找后面的数字是否小于他
每拿到一个数 就对后面的数排序 然后二分查找
*/
int count = 0;
int reversePairs(vector<int>& nums)
{
merge_sort(nums,0, nums.size() - 1);
return count;
}
private:
//分治 左闭右闭
void merge_sort(vector<int> &nums,int left,int right)
{
if (left >= right) return;
int mid = left + (right - left) / 2;//下取整
//统计左半边+右半边+当前的逆序对个数
merge_sort(nums,left,mid);
merge_sort(nums, mid+1, right);
merge(nums, left, mid, right);
}
//合并两个有序数组 左闭右闭
//(left,mid) (mid+1,right)
void merge(vector<int>& nums, int left, int mid, int right)
{
//左数组开始
int i = left;
//右数组开始
int j = mid + 1;
//辅助数组
int* temp = new int[right - left + 1];
//用于合并的原数组索引
int k = 0;
while (i <= mid && j <= right)
{
//第一个数组中的元素先归并
if (nums[i] <= nums[j])
{
temp[k++] = nums[i++];
}
//第二个数组中的元素先归并
else
{
temp[k++] = nums[j++];
//右边要归上去的这个元素比左子数组当前元素至末尾元素都小 都构成逆序对
count += mid - i + 1;
}
}
while (i<=mid) temp[k++] = nums[i++];
while (j <= right) temp[k++] = nums[j++];
//把temp数组复制回去
for (int i = 0; i < right - left + 1 ; i++)
{
nums[left + i] = temp[i];
}
}
};
面试题52 双链表得第一个公共节点(双指针、哈希表)
还有一个优美得解法
两个链表长度分别为L1+C、L2+C, C为公共部分的长度,按照楼主的做法: 第一个人走了L1+C步后,回到第二个人起点走L2步;第2个人走了L2+C步后,回到第一个人起点走L1步。 当两个人走的步数都为L1+L2+C时就两个家伙就相爱了
//定义两个指针分别指向两个链表的头节点
//遍历链表1 用哈希表记录链表1的每个节点的地址
//遍历链表2 逐个找是否有相等的节点
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
if (!headA && !headB) return nullptr;
unordered_set<ListNode*> hash_set;
while (headA)
{
hash_set.insert(headA);
headA = headA->next;
}
while (headB)
{
auto it=hash_set.find(headB);
if (it != hash_set.end()) return headB;
headB = headB->next;
}
return nullptr;
}
//先分别走完 统计个数
//让较长得那个指针先走长度差 然后再分别同时开始走 比较相同得
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB)
{
if (!headA && !headB) return nullptr;
int len_a = 0;
int len_b = 0;
ListNode* temp_a = headA;
ListNode* temp_b = headB;
while (temp_a)
{
temp_a = temp_a->next;
len_a++;
}
while (temp_b)
{
temp_b = temp_b->next;
len_b++;
}
if (len_a >= len_b)
{
for (int i = 0; i < len_a - len_b; i++)
{
headA = headA->next;
}
}
else
{
for (int i = 0; i < len_b - len_a; i++)
{
headB = headB->next;
}
}
while (headA && headB)
{
if (headA == headB) return headA;
headA = headA->next;
headB = headB->next;
}
return nullptr;
}
面试题53 在排序数组中查找数字 1(二分查找)
#include<iostream>
using namespace std;
#include<vector>
class Solution
{
public:
/*统计一个数字在排序数组中出现的次数。
*/
//二分查找那个数字出现的左端点
int search(vector<int>& nums, int target)
{
int left = 0;
int right = nums.size();
int mid;
int ret=0;
int ret_left;
int ret_right;
//退出条件为left==right
//找左边界
while (left < right)
{
mid = left + (right - left) / 2;
if (nums[mid] >= target)
{
right = mid;
}
else
{
left = mid + 1;
}
}
ret_left = left;
left = 0;
right = nums.size();
//退出条件为left==right
//找右边界
while (left < right)
{
mid = left + (right - left) / 2;
if (nums[mid] > target)
{
right = mid;
}
else
{
left = mid + 1;
}
}
ret_right = right-1;
/* while (nums[left] == target)
{
ret++;
left++;
}*/
return ret_right-ret_left+1;
}
};
面试题 53 0-n-1中缺失得数字(二分查找)
#include<iostream>
using namespace std;
#include<vector>
class Solution
{
public:
/*
一个长度为n-1的递增排序数组中的所有数字都是唯一的,
并且每个数字都在范围0~n-1之内。
在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
*/
//索引比数字小就说明前面缺失了 相等就是没有缺失
//还是二分查找
int missingNumber(vector<int>& nums)
{
int left = 0;
int right = nums.size();
while (left < right)
{
int mid = left + (left - right)/2;
while (left < right)
{
if (mid < nums[mid])
{
right = mid;
}
else//mid=nums[mid]
{
left = mid + 1;
}
}
return left;
}
}
};
面试题54 二叉搜索树得第k大节点(中序遍历)
#include<iostream>
using namespace std;
#include<vector>
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
/*
给定一棵二叉搜索树,请找出其中第k大的节点。
*/
//二叉搜索树性质 左边最小 中间 右边 且没有键值相同得节点
//暴力 递归遍历搜索树得所有节点
//中序遍历倒序
//剪枝
int ret;
int kthLargest(TreeNode* root, int k)
{
int count = 0;
bool flag = false;
fun(root, count,k,flag);
return ret;
}
void fun(TreeNode* root, int& count,int k,bool& flag)
{
if (!root||flag) return;
fun(root->left,count,k,flag);
count++;
if (count == k)
{
bool flag = true;
ret = root->val;
return;
}
fun(root->right,count,k,flag);
}
};
面试题55 二叉树的深度(深度优先搜索、后序遍历)
遍历所有的点 搞一个计数记录走过多长 前序遍历
#include<iostream>
using namespace std;
#include<vector>
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
/*
输入一棵二叉树的根节点,求该树的深度。
从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,
最长路径的长度为树的深度。
*/
int max = 0;
int maxDepth(TreeNode* root)
{
fun(root, 0);
return max;
}
void fun(TreeNode* root, int count)
{
if (!root) return;
count++;
if (count > max) max = count;
fun(root->left, count);
fun(root->right, count);
}
};
深度优先搜索、后序遍历
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
/*
输入一棵二叉树的根节点,求该树的深度。
从根节点到叶节点依次经过的节点(含根、叶节点)形成树的一条路径,
最长路径的长度为树的深度。
*/
int maxDepth(TreeNode* root)
{
if (!root) return 0;
int left = maxDepth(root->left);
int right = maxDepth(root->right);
return max(left, right) + 1;
}
};
面试题56 数组中数字出现的数字(位运算)
#include<iostream>
using namespace std;
#include<vector>
class Solution
{
public:
/*
一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。
请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
*/
//异或的性质 两个数字异或的结果a^b是将 a 和 b 的二进制每一位进行运算,得出的数字。
//运算的逻辑是如果同一位的数字相同则为 0,不同则为 1
//任何数和本身异或则为 0 任何数和 0 异或是 本身
//异或满足交换律
vector<int> singleNumbers(vector<int>& nums)
{
//先对所有数字进行一次异或,得到两个出现一次的数字的异或值。
int k = 0;
for (int i = 0; i < nums.size(); i++)
{
k ^= nums[i];
}
// cout<<k<<endl;
// cout<<"a"<<endl;
//在异或结果中找到任意为 1 的位。
int m = 1;//辅助变量 找到为1的最低位
while (1)
{
// cout<<(k & m)<<endl;
if ((k & m)) break;
m = m<<1;//循环左移
}
// cout<<m<<endl;
// cout<<"b"<<endl;
//根据这一位对所有的数字进行分组。目的是为了把两个不同的数分到不同的组
//(相同的数由于那一位相同自然回分到一组)
int x = 0;//第一组
int y = 0;//第二组
//在每个组内进行异或操作,得到两个数字。
for (int i = 0; i < nums.size(); i++)
{
if (m & nums[i])//这一位为1
{
x ^= nums[i];
}
else
{
y ^= nums[i];
}
}
return { x,y };
}
};
面试题 56数组中出现的次数 2(位运算、哈希表)
位运算
7 : 0B0111
7 : 0B0111
7 : 0B0111
^^^
|||
出现数 333 // 从上向下,可以发现[3个7]每位都出次了 3 次
那这样时再增加一个数。
7 : 0B0111
7 : 0B0111
7 : 0B0111
4 : 0B0100
^^^
|||
出现数 433 // 从上向下,只有第 3位bit 出现了 4次
%%%
333 // 接下来,把现各个位出现的次数,按 3取余
‖‖‖
100 // 取余结果
int singleNumbers(vector<int>& nums)
{
int res = 0;
//对于每一位都遍历所有的数
for (int i = 0, sub = 0; i < 32; ++i, sub = 0)
{
for (auto& n : nums) sub += ((n >> i) & 1);//加上每一位的值
if (sub % 3) res |= (1 << i);//如果某一位的数满足条件 就把这一位记录下来
}
return res;
}
哈希表
class Solution {
public:
int singleNumber(vector<int>& nums) {
unordered_map<int, int> dict;
for (auto &i : nums) ++dict[i];
for (auto &[k, v] : dict)
if (v == 1) return k;
return 0;
}
};
面试题57 和为S的两个数(双指针)
#include<iostream>
using namespace std;
#include<vector>
/*
输入一个递增排序的数组和一个数字s,
在数组中查找两个数,使得它们的和正好是s。
如果有多对数字的和等于s,则输出任意一对即可。
*/
class Solution
{
public:
//双指针 一个指向头一个指向尾
vector<int> twoSum(vector<int>& nums, int target)
{
int i = 0;//左指针
int j = nums.size()-1;//右指针
while (i < j)
{
if (nums[i] + nums[j] == target)
{
return { nums[i],nums[j] };
}
else if (nums[i] + nums[j] > target)
{
j--;
}
else
{
i++;
}
}
return { -1,-1 };
}
};
面试题57 和为s的连续正数序列(滑动窗口、双指针)
暴力解法 求以每个数开头的序列和
vector<vector<int>> findContinuousSequence(int target)
{
//1.连续正整数
vector<vector<int>> ret;
//f(n)为以n结尾的连续正整数序列
//f(n)为以n开头的连续正整数序列
//n一定小于target
for (int i = 0; i <target; i++)
{
int sum = 0;
for (int j = i; j < target; j++)
{
if (sum == target)
{
vector<int> res;
int left = i;
int right = j;
while (left <= right)
{
res.push_back(left++);
}
ret.push_back(res);
}
else if (sum < target)
{
sum += j;
}
else
{
break;
}
}
}
return ret;
}
优化解法 滑动窗口
滑动窗口左右边界都只能往右滑
//滑动窗口
//
vector<vector<int>> findContinuousSequence(int target)
{
int i = 1;//滑动窗口左边界初始值
int j = 2;//右边界
int sum =3;//滑动窗口中数字和
vector<vector<int>> ret;
while (i < j&&i<(target+1)/2)
{
if (sum == target)
{
vector<int> res;
int left = i;
int right = j;
while (left <= right)
{
res.push_back(left++);
}
ret.push_back(res);
//左边界右移
i++;
sum -= i;
continue;
}
else if (sum > target) //左边界右移
{
i++;
sum -= i;
}
else//右边界左移
{
j++;
sum += j;
}
}
return ret;
}
面试题58 反转单词顺序(栈、字符串)
栈
#include<iostream>
using namespace std;
#include<string>
#include<stack>
class Solution
{
public:
/*
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。
为简单起见,标点符号和普通字母一样处理。
例如输入字符串"I am a student. ",则输出"student. a am I"。
*/
//栈 先进后出
string reverseWords(string s)
{
int len = s.size();
if (len == 0) return "";
stack<string> word_stack;
string word;
for (int i = 0; i < len; i++)
{
word.push_back(s[i]);
if (s[i] == ' ' || i == s.size() - 1)
{
if (i == s.size() - 1) word.push_back(' ');
// cout << word << endl;
word_stack.push(word);
word.clear();
}
}
s.clear();
// cout << "---" << endl;
// cout << word_stack.size() << endl;
int len_stack = word_stack.size();
for (int i = 0; i < len_stack; i++)
{
// cout << word_stack.top() << endl;
s += word_stack.top();
word_stack.pop();
}
// cout<<s<<endl;
// cout<<"----"<<endl;
int i = 0;//开头索引
int j = s.size() - 1;//结尾索引
// cout<<j<<endl;
//去掉开头多余的空格
while (s[i] == ' ' && i <= j) i++;
// cout<<i<<endl;
if (i == j) return " ";
// cout<<"----"<<endl;
//去掉结尾多余空格
while (s[j] == ' ' && j > 0) j--;
// cout<<s<<endl;
string ret;
for (int k = i; k <= j; k++)
{
ret.push_back(s[k]);
if (s[k] == ' ' && s[k + 1] == ' ')//k是空格 并且k+1也是空格
{
while (s[k + 1] == ' ') k++;//跳过之后的空格
}
}
return ret;
}
};
写的简单一点的解法
#include<iostream>
using namespace std;
#include<string>
#include<stack>
class Solution
{
public:
string reverseWords(string s)
{
if (s.empty())
return "";
s += " ";
string temp = "";
vector<string> res;
//去掉了所有的空格 遇到空格就插入
for (char ch : s)
{
if (ch == ' ')
{
if (!temp.empty())
{
res.push_back(temp);
temp.clear();
}
}
else
temp += ch;
}
s.clear();
reverse(res.begin(), res.end());
for (string &str : res)
s += str + ' ';
s.pop_back();
return s;
}
};
面试题58 左旋转字符串(字符串)
#include<iostream>
using namespace std;
class Solution
{
public:
/*
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。
请定义一个函数实现字符串左旋转操作的功能。
比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
*/
//开一个新的string 把后面的先放到前面,前面的放到末尾
string reverseLeftWords(string s, int n)
{
string ret;
for (int i = n; i < s.size(); i++)
{
ret.push_back(s[i]);
}
for (int i = 0; i < n; i++)
{
ret.push_back(s[i]);
}
return ret;
}
};
面试题59 队列的最大值(双端队列、滑动窗口)
#include<iostream>
using namespace std;
#include<vector>
#include<deque>
/*
给定一个数组 nums 和滑动窗口的大小 k,请找出所有滑动窗口里的最大值。
*/
class Solution {
public:
/*
暴力 每一个滑动窗口 判断最大值
*/
//定义一个双端队列 队列中存的是数组的下标 队首就是当前滑窗内的最大元素
//每次插入一个新元素前先判断是否要剔除队列中的元素
//1.由于队列中存放的是数组的下标 所以先判断对头元素是否已经超出滑动窗口的范围 超出就剔除
//2.由于队列中比新加入数小的就没有机会再成为最大值 所以要把比这个数小的数都剔除
//插入这个新元素的下标
//只要i>=k-1 就要把答案记下来
vector<int> maxSlidingWindow(vector<int>& nums, int k)
{
vector<int> res;
deque<int> q;
for (int i = 0; i < nums.size(); i++)
{
//剔除不在新滑动窗口中的元素 如果没有元素就不剔除
while (q.size() && q.front() <= i - k) q.pop_front();
//剔除比新元素小的元素 两个一样的数也要剔除
while (q.size() && nums[q.back()] <= nums[i]) q.pop_back();
q.push_back(i);
//
if (i >= k - 1)
res.push_back(q.front());
}
return res;
}
};
面试题59 队列的最大值 2(双端队列)
#include<iostream>
using namespace std;
#include<queue>
#include<deque>
/*
请定义一个队列并实现函数 max_value 得到队列里的最大值
要求函数max_value、push_back 和 pop_front 的均摊时间复杂度都是O(1)。
若队列为空,pop_front 和 max_value 需要返回 -1
*/
class MaxQueue
{
public:
//一个新来的数字进来后 里面所有比他小的数字就不可能先于他成为最大值
//定义一个队列存放正常的元素
//定义一个队列存放最大值元素 维护了一个单调递减的队列
deque<int> max_q;//双端队列
queue<int> q;//单端队列
MaxQueue()
{
}
int max_value()
{
if (max_q.size() == 0) return - 1;
return max_q.front();
}
void push_back(int value)
{
q.push(value);
//去掉所有小于等于value的元素 他们没有机会成为最大值了
while (max_q.size()!=0&&max_q.back() < value) max_q.pop_back();
max_q.push_back(value);
}
int pop_front()
{
if (q.size() == 0) return -1;
int ret = q.front();
q.pop();
if (ret == max_q.front()) max_q.pop_front();
return ret;
}
};
面试题60 n个骰子的点数(动态规划)
动态规划主要是要定义dp数组的含义 、状态转移方程、边界条件处理
#include<iostream>
using namespace std;
#include<vector>
/*
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,
其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
*/
class Solution
{
public:
//求所有骰子点数之和的值的概率 从小到大排列
//动态规划
//状态方程dp[i][j] 表示i个骰子点数为j出现的次数
//状态转移方程 dp[i][j]=dp[i-1][j-1]+dp[i-1][j-1]+....dp[i-1][j-6]
//也就是第n个骰子点数为1-6的情况之和
//状态方程的初始值
vector<double> dicesProbability(int n)
{
vector<double>res;
vector<vector<int>> dp(n + 1, vector<int>(6 * (n + 1),0));
//边界处理 i=1 时 j怎么取值
for (int j = 1; j <= 6; j++)
{
dp[1][j] = 1;
}
//
for (int i = 2; i <= n; i++)
{
for (int j = i; j <= 6 * i; j++)
{
//动态转移
for (int k = 1; k <= 6; k++)
{
if (j - k <= 0) break;
dp[i][j] += dp[i-1][j - k];
}
}
}
//所有的排列总次数
int all = pow(6, n);
//返回n个骰子时 所有的可能
for (int i = n; i <= 6 * n; i++)
{
res.push_back(dp[n][i] * 1.0 / all);
}
return res;
}
};
这题可以用一维dp数组代替二维数组进行优化 但我不会
面试题61 扑克牌中的顺子(遍历、set)
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
/*
从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。
2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
*/
class Solution
{
public:
//A=1 J=11 q=12 k=13 0可以变成任何数字
//排序 从1开始找下一个数 没有就用掉一个0
//如果是连续的,那么除0为外的最大值和最小值
bool isStraight(vector<int>& nums)
{
sort(nums.begin(), nums.end());
int a = 0;//0的个数
while (nums[a] == 0) a++;
bool flag = true;
// cout<<a<<endl;
//只需要计算每个数之间的差值
for (int i = a; i < 4; i++)
{
//相等就没机会了
if (nums[i + 1] == nums[i]) return false;
//不连续 差值超过1
if ((nums[i + 1] - nums[i]) > 1 )
{
a -= nums[i + 1] - nums[i];
}
else
{
continue;
}
//0用完了
if (a < 0) return false;
}
return flag;
}
};
还有一种思路就是遍历一遍把每个数存入哈希set中 如果有重复直接返回 遍历的同时记录最大值和最小值 如果max−min<5则可以构成顺子
面试题62 圆圈中最后剩下的数字(约瑟夫环问题、动态规划、数学)
这找规律呢
#include<iostream>
using namespace std;
/*
0,1,···,n-1这n个数字排成一个圆圈,从数字0开始
每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。
求出这个圆圈里剩下的最后一个数字。
例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,
则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。
*/
//环形链表模拟圆
//定义f(n,m)为n个数,每次删掉第m个数最后剩下的数字
//f(n,m)=(f(n-1,m)+m)%n
//f(n, m)指n个人,报第m个编号出列最终编号
/*
f(10,3)=(f(9,3)+3)%10
f(9,3)=(f(8,3)+3)%9
……
f(2,3)=(f(1,3)+3)%2
f(1,3)=0
*/
//旧序列删除一个数(m-1)后剩余的 m,m+1,m+2....0,1,2... m-2
//定义一个新序列有n-1个数 0,1,2...............n-2
//n-2+m%n
class Solution
{
public:
int lastRemaining(int n, int m)
{
int value = 0;
for (int i = 2; i <= n; i++)
{
value = (value + m) % i;
}
return value;
}
};
面试题63 股票的最大利润(动态规划)
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
class Solution
{
public:
/*
假设把某股票的价格按照时间先后顺序存储在数组中,
请问买卖该股票一次可能获得的最大利润是多少?
*/
//按照时间顺序,
//数组后面的数减前面数的值最大
//暴力 计算出每一个数组后面的数减前面数的值
//维护一个最小值 后面进来的数只需要比较与最小值的差值就可以了
int maxProfit(vector<int>& prices)
{
int min = prices[0];//当前序列的最小值
int ret = 0;//最大利润
for (int i = 1; i < prices.size(); i++)
{
if (prices[i] < min)
{
min = prices[i];
}
else
{
int temp = prices[i] - min;
ret = max(ret, temp);
}
}
return ret;
}
};
面试题64 求1+2+…+n(逻辑短路、智力)
#include<iostream>
using namespace std;
class Solution
{
/*
求 1+2+...+n ,
要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
*/
public:
//当n为0就会直接返回,不会计算后面一项
int sumNums(int n)
{
n&& (n += sumNums(n - 1));
return n;
}
};
面试题 65 不用加减乘除做加法(全加器、位运算、位运算加法模板)
不太会位运算啊
class Solution {
public:
int add(int a, int b) {
while (b) {
// 无进位求和
int not_carry = a ^ b;
// 求进位数(这里负数会报错,转成无符号)
// 求出的进位数要左移一位,
// 例如:进位数是1,左移一位才是10
int carry = ((unsigned int)(a & b) << 1);
// 一直循环,直到不需要进位为止
a = not_carry;
b = carry;
}
return a;
}
};
面试题66 构建乘积数组(表格分区、双向遍历)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXC4xdhl-1629084912960)(数据结构与算法.assets/image-20210708104953829.png)]
#include<iostream>
using namespace std;
#include<vector>
/*
给定一个数组 A[0,1,…,n-1],请构建一个数组 B[0,1,…,n-1],
其中 B[i] 的值是数组 A 中除了下标 i 以外的元素的积,
即 B[i]=A[0]×A[1]×…×A[i-1]×A[i+1]×…×A[n-1]。不能使用除法。
*/
class Solution
{
public:
//用除法:算出总和,再除
//每一项先计算i之前的乘积 再计算i之后的乘积
//相当于b数组分两部分来构建
vector<int> constructArr(vector<int>& a)
{
if (a.size() == 0) return {};
vector<int> b;
b.resize(a.size());
b[0] = 1;
// cout<<"-"<<endl;
//统计i之前的乘积
for (int i = 1; i < a.size(); i++)
{
b[i] = b[i - 1] * a[i - 1];
}
//cout<<"--"<<endl;
int tmp = 1;
//计算i之后的乘积
for (int i = a.size()-2; i >= 0; i--)
{
tmp = tmp * a[i + 1];
b[i] *= tmp;
// cout<<tmp<<endl;
}
//cout<<"---"<<endl;
return b;
}
};
面试题67 把字符串转换成整数(字符串、鲁棒性)
#include<iostream>
using namespace std;
#include<string>
/*
写一个函数 StrToInt,实现把字符串转换成整数这个功能。不能使用 atoi 或者其他类似的库函数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。
当我们寻找到的第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字组合起来,作为该整数的正负号;假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成整数。
该字符串除了有效的整数部分之后也可能会存在多余的字符,这些字符可以被忽略,它们对于函数不应该造成影响。
注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换。
在任何情况下,若函数不能进行有效的转换时,请返回 0。
说明:
假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
*/
class Solution
{
public:
//丢弃无用的开头空格字符
//正负号
//有效连续整数后的字符被忽略
//第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,返回0
//超过INT_MAX INT_MIN
int strToInt(string str)
{
int flag = 1;//正负号标志位
long long ret = 0;//结果
int i = 0;
//丢弃无用的开头空格字符
while (str[i] == ' ')i++;
//可能存在的正负号
if (str[i] == '+') i++;
else if (str[i] == '-')
{
flag = -1;
i++;
}
//整数部分处理
//INT_MAX 2147483647
while (str[i] >= '0' && str[i] <= '9')
{
if ((ret == INT_MAX / 10 && str[i] > '7') || ret > INT_MAX / 10)
{
if (flag == 1) ret = INT_MAX;
else if (flag == -1) ret = INT_MIN;
return ret;
}
ret = ret * 10 + str[i] - '0';
// cout<<ret<<endl;
/*if (flag * ret > INT_MAX || flag * ret < INT_MIN)
{
if (flag == 1) ret = INT_MAX;
else if (flag == -1) ret = INT_MIN;
return ret;
}*/
i++;
}
return flag * ret;
}
};
面试题68 二叉搜索树的最近公共祖先(二叉搜索树)
#include<iostream>
using namespace std;
class Solution
{
public:
/*\
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。最近公共祖先节点可以为节点本身
*/
//如果左和右都小于父 则均位于左子树
//如果左小于根 根小于右 就找到了
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
TreeNode* temp=root;
while (1)
{
//cout<<root->val<<endl;
//p q都小于根 都位于左子树
if (temp->val > p->val && temp->val > q->val)
{
// cout<<"---"<<endl;
// cout<<temp->val<<endl;
// cout<<p->val<<endl;
// cout<<q->val<<endl;
// cout<<"---"<<endl;
temp=temp->left;
}
//p q都大于根 都位于右子树
else if (temp->val < p->val && temp->val < q->val)
{
temp=temp->right;
}
//根位于pq之间
else if ((temp->val - p->val) * (temp->val - q->val)<=0)
{
// cout<<"s"<<endl;
break;
}
}
return temp;
}
};
这题还可以两次遍历,利用数组记录从父节点遍历到该节点经过的路径,然后从后往前比较两个路径
面试题68 二叉树的最近公共祖先2(回溯法、记录路径模板题)
超出时间限制的做法 //求根节点到指定节点的路径 存放到path中
#include<iostream>
using namespace std;
#include<vector>
class Solution
{
public:
/*
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
所有节点的值都是唯一的。
*/
//那递归遍历
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
if(root==nullptr||p==nullptr||q==nullptr) return nullptr;
TreeNode* dfs_root = root;
vector<TreeNode*> path_p;
vector<TreeNode*> path_q;
vector<TreeNode*> temp;
//vector<int> temp1;
int flag = 0;
dfs(dfs_root, path_p, p, temp, flag);
flag = 0;
dfs_root = root;
dfs(dfs_root, path_q, q, temp, flag);
int i = 0;
//两个数组的最后一个重复的数
while (path_p[i]->val == path_q[i]->val)
{
i++;
if(i==path_p.size()||i==path_q.size()) break;
}
return path_p[i-1];
}
//求根节点到指定节点的路径 存放到path中
int dfs(TreeNode* root, vector<TreeNode*>& path, TreeNode* node, vector<TreeNode*> temp, int& flag)
{
if (root == nullptr) return 0;
else if (flag == 1)
{
return 0;
}
temp.push_back(root);
if (root->val == node->val)
{
flag = 1;
path = temp;
}
dfs(root->left, path, node, temp, flag);
dfs(root->right, path, node, temp, flag);
return 0;
}
};
在二叉树中,找到求根节点到指定节点的路径优化版本(模板)
#include<iostream>
using namespace std;
#include<vector>
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution
{
public:
/*
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
所有节点的值都是唯一的。
*/
bool flag = false;
//那递归遍历
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
TreeNode* dfs_root = root;
vector<TreeNode*> path_p;
vector<TreeNode*> path_q;
dfs(dfs_root, path_p, p);
flag = false;
dfs_root = root;
dfs(dfs_root, path_q,q);
int i = 0;
//两个数组的最后一个重复的数
while (path_p[i] == path_q[i])
{
i++;
if (i == path_p.size() || i == path_q.size()) break;
}
return path_p[i-1];
}
//前序遍历
//求根节点到指定节点的路径 存放到path中
void dfs(TreeNode* root, vector<TreeNode*>& path, TreeNode* node)
{
if (root == nullptr||flag) return;
//存放递归经过的路径
path.push_back(root);
if (root== node)
{
flag = true;
return;
}
dfs(root->left, path, node);
dfs(root->right, path, node);
//如果找到了就直接返回
if (flag == true) return;
//如果每没找到就弹出该元素 关键!
path.pop_back();
}
};
后序遍历 先遍历左右 在遍历到父节点时根据左右的返回值信息判断是否找到了p或q
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
if (root == nullptr || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left == nullptr) return right;
if (right == nullptr) return left;
return root;
}
STL
哈希表的底层实现
哈希表原理 : 使用一个下标范围比较大的数组来存储元素,把输入的键通过哈希函数得到一个值,这个值就是数组下标(成为桶),也就是存放这个键对应值的地方。
但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,总的来说,“直接定址”与“解决冲突”是哈希表的两大特点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-niDrolh2-1629084912961)(数据结构与算法.assets/image-20210714172217846.png)]
二维数组
动态初始化一个二维数组
//c++ 创建一个动态二维数组
int** result = new int* [row];
for (int i = 0; i < row; i++)
{
result[i] = new int[col];
}
return dfs(grid, 0, 0);
//stl
vector<int> w(col,-1);
vector<vector<int>> result1(row,vector<int>(col,-1));
vector<vector<int>> result2(row, w);
环形队列
只有在顺序存储结构的前提下有实现环形队列的必要性,链式不需要
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BM2YbcX6-1629084912961)(数据结构与算法.assets/image-20210728224456222.png)]
贴一个网上的实现代码吧
/* 队列的顺序存储结构(循环队列) */
#define MAX_QSIZE 5 /* 最大队列长度+1 */
typedef struct
{
QElemType *base; /* 初始化的动态分配存储空间 */
int front; /* 头指针,若队列不空,指向队列头元素 */
int rear; /* 尾指针,若队列不空,指向队列尾元素的下一个位置 */
}SqQueue;
/* 循环队列的基本操作(9个) */
void InitQueue(SqQueue *Q)
{ /* 构造一个空队列Q */
Q->base=malloc(MAX_QSIZE*sizeof(QElemType));
if(!Q->base) /* 存储分配失败 */
exit(OVERFLOW);
Q->front=Q->rear=0;
}
void DestroyQueue(SqQueue *Q)
{ /* 销毁队列Q,Q不再存在 */
if(Q->base)
free(Q->base);
Q->base=NULL;
Q->front=Q->rear=0;
}
void ClearQueue(SqQueue *Q)
{ /* 将Q清为空队列 */
Q->front=Q->rear=0;
}
Status QueueEmpty(SqQueue Q)
{ /* 若队列Q为空队列,则返回TRUE;否则返回FALSE */
if(Q.front==Q.rear) /* 队列空的标志 */
return TRUE;
else
return FALSE;
}
int QueueLength(SqQueue Q)
{ /* 返回Q的元素个数,即队列的长度 */
return(Q.rear-Q.front+MAX_QSIZE)%MAX_QSIZE;
}
Status GetHead(SqQueue Q,QElemType *e)
{ /* 若队列不空,则用e返回Q的队头元素,并返回OK;否则返回ERROR */
if(Q.front==Q.rear) /* 队列空 */
return ERROR;
*e=Q.base[Q.front];
return OK;
}
Status EnQueue(SqQueue *Q,QElemType e)
{ /* 插入元素e为Q的新的队尾元素 */
if((Q->rear+1)%MAX_QSIZE==Q->front) /* 队列满 */
return ERROR;
Q->base[Q->rear]=e;
Q->rear=(Q->rear+1)%MAX_QSIZE;
return OK;
}
Status DeQueue(SqQueue *Q,QElemType *e)
{ /* 若队列不空,则删除Q的队头元素,用e返回其值,并返回OK;否则返回ERROR */
if(Q->front==Q->rear) /* 队列空 */
return ERROR;
*e=Q->base[Q->front];
Q->front=(Q->front+1)%MAX_QSIZE;
return OK;
}
void QueueTraverse(SqQueue Q,void(*vi)(QElemType))
{ /* 从队头到队尾依次对队列Q中每个元素调用函数vi() */
int i;
i=Q.front;
while(i!=Q.rear)
{
vi(Q.base[i]);
i=(i+1)%MAX_QSIZE;
}
printf("\n");
}
vector
初始化
vector<T> v; //采用模板实现类实现,默认构造函数 vector为空, size为0
vector(v.begin(), v.end()); //将v[begin(), end())区间中的元素拷贝给本身。
vector(n, elem); //构造函数将n个elem拷贝给本身。
vector(const vector &vec); //拷贝构造函数。
vector<pair<string, int>> record;//存放pair
vector<int> temp(size);//指定初始大小
vector<vector<int>> visited(m,vector<int>(n,0));//创建一个二维vector 并初始化为0
visited.resize(m, vector<int>(n, 0));//将一个二维vector resize为m*n 并初始化为0
resize和reserve的区别
首先必须弄清楚两个概念:
capacity:指容器在分配新的存储空间之前能存储的元素总数。
size:指当前容器所存储的元素个数
在弄清这两个概念以后,很容易懂resize和reserve的区别:
reserve表示容器预留空间,但并不是真正的创建对象,需要通过insert()或push_back()等创建对象。resize既分配了空间,也创建了对象。
reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小。
两者的形参个数不一样。
resize带两个参数,一个表示容器大小,一个表示初始值(默认为0)
reserve只带一个参数,表示容器预留的大小。
遍历
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
常用方法
赋值
//**************************************************************
//assign赋值方法,释放了原来的数据空间,并分配了新的数据空间
//数组名作assign参数时,第1个表示数据起始位置(包括在内),第二个参数表示结束位置(不包括在内)实际上传入的指针进行了隐式转换成了迭代器。迭代器用白话来说就是包含指针的一种类类型。通过指针和类相关成员变量和方法可以遍历所指向对象数据。
v3.assign(v1.begin(), v1.end());//还可以传入数组名
v4.assign(10, 100);将[beg, end)区间中的数据拷贝赋值给本身
assign(n, elem); //将n个elem拷贝赋值给本身
Vec1.assign({3, 4, 5});
//容量与大小
//**************************************************************
empty(); //判断容器是否为空 为空返回true
capacity(); //容器的容量
size(); //返回容器中元素的个数
resize(int num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除。
resize(int num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除
//插入与删除
//**************************************************************
push_back(ele); //尾部插入元素ele
pop_back(); //删除最后一个元素
insert(const_iterator pos, ele); //迭代器指向位置前面插入元素ele
insert(const_iterator pos, int count,ele); //迭代器指向位置pos前面插入count个元素ele
erase(const_iterator pos); //删除迭代器指向的元素
erase(const_iterator start, const_iterator end); //删除迭代器从start到end之间的元素
clear(); //删除容器中所有元素
//对vector中的数据的存取操作
//**************************************************************
//at(int idx); //返回索引idx所指的数据
operator[]; //返回索引idx所指的数据
front(); //返回容器中第一个数据元素
back(); //返回容器中最后一个数据元素
v1.swap(v2); // 将vec与本身的元素互换 互换两个容器得内容
常见问题
v.begin() 返回指向第一个元素得迭代器
v.end() 返回指向最后一个元素后面的位置的迭代器
string
char与int互转(单个字符转换)
char a='5'
int i=10
//char转int
int temp=a-'0';
//int 转char
char temp=i+'0';
字符串转数字
#include<cstring>
string s1;
//stoi将字符串转int型时,可以自动去除前导零 int stoi(const string&)
stoi(s1)
//字符数组转数字
//int atoi(const char* str)
atoi(s1.c_str());
数字转字符串
string str=to_string(22);
字符串替换函数(要注意的是,str.repalce 就把str本身改变了)
string& replace(int pos, int n, const string& str); //替换从pos开始n个字符为字符串str
string& replace(int pos, int n,const char* s); //替换从pos开始的n个字符为字符串s
初始化
string(); //创建一个空的字符串 例如: string str;
string(const char* s); //使用字符串s初始化
string(const string& str); //使用一个string对象初始化另一个string对象
string(int n, char c); //使用n个字符c初始化
赋值与拼接
string& operator=(const char* s); //char*类型字符串 赋值给当前的字符串
string& operator=(const string &s); //把字符串s赋给当前的字符串
string& operator=(char c); //字符赋值给当前的字符串
string& assign(const char *s); //把字符串s赋给当前的字符串
string& assign(const char *s, int n); //把字符串s的前n个字符赋给当前的字符串
string& assign(const string &s); //把字符串s赋给当前字符串
string& assign(int n, char c); //用n个字符c赋给当前字符串
拼接
string& operator+=(const char* str); //重载+=操作符
string& operator+=(const char c); //重载+=操作符
string& operator+=(const string& str); //重载+=操作符c
string& append(const char *s); //把字符串s连接到当前字符串结尾
string& append(const char *s, int n); //把字符串s的前n个字符连接到当前字符串结尾
string& append(const string &s); //同operator+=(const string& str)
string& append(const string &s, int pos, int n); //字符串s中从pos开始的n个字符连接到字符串结尾
查找与替换c
int find(const string& str, int pos = 0) const; //查找str第一次出现位置,从pos开始查找
int find(const char* s, int pos = 0) const; //查找s第一次出现位置,从pos开始查找
int find(const char* s, int pos, int n) const; //从pos位置查找s的前n个字符第一次位置
int find(const char c, int pos = 0) const; //查找字符c第一次出现位置
int rfind(const string& str, int pos = npos) const; //查找str最后一次位置,从pos开始查找
int rfind(const char* s, int pos = npos) const; //查找s最后一次出现位置,从pos开始查找
int rfind(const char* s, int pos, int n) const; //从pos查找s的前n个字符最后一次位置
int rfind(const char c, int pos = 0) const; //查找字符c最后一次出现位置
string& replace(int pos, int n, const string& str); //替换从pos开始n个字符为字符串str
string& replace(int pos, int n,const char* s); //替换从pos开始的n个字符为字符串s
//find查找是从左往后,rfind从右往左
//find找到字符串后返回查找的第一个字符位置,找不到返回-1
//replace在替换时,要指定从哪个位置起,多少个字符,替换成什么样的字符串
插入删除与存储
char& operator[](int n); //通过[]方式取字符
char& at(int n); //通过at方法获取字符
string& insert(int pos, const char* s); //插入字符串
string& insert(int pos, const string& str); //插入字符串
string& insert(int pos, int n, char c); //在指定位置插入n个字符c
string& erase(int pos, int n = npos); //删除从Pos开始的n个字符
string substr(int pos = 0, int n = npos) const; //返回由pos开始的n个字符组成的字符串
stack
栈中只有顶端的元素才可以被外界使用,因此栈不允许有遍历行为
栈中进入数据称为 — 入栈 push
栈中弹出数据称为 — 出栈 pop
top()是取栈顶元素
一般取出元素要先top 再pop
#include <stack>
//栈容器常用接口
void test01()
{
//创建栈容器 栈容器必须符合先进后出
stack<int> s;
//向栈中添加元素,叫做 压栈 入栈
s.push(10);
s.push(20);
s.push(30);
while (!s.empty()) {
//输出栈顶元素
cout << "栈顶元素为: " << s.top() << endl;
//弹出栈顶元素
s.pop();
}
cout << "栈的大小为:" << s.size() << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
queue
Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口
队列容器允许从一端新增元素,从另一端移除元素
队列中只有队头和队尾才可以被外界使用,因此队列不允许有遍历行为
队列中进数据称为 — 入队 push
队列中出数据称为 — 出队 pop
拿队列头部的数据用 front 不是用top
c++ STL提供得队列实际上是一个双向队列
#include
Deque 的特点:
(1) 随机访问方便,即支持[ ] 操作符和vector.at() ,但性能没有vector 好;
(2) 可以在内部进行插入和删除操作,但性能不及list ;
(3) 可以在两端进行push 、pop ;
(4) 相对于verctor 占用更多的内存。
可以使用[]操作符访问双向队列中单个的元素
iterator erase( iterator pos ); //删除pos位置上的元素
iterator erase( iterator start, iterator end ); //删除start和end之间的所有元素
front() 返回第一个元素的引用
back() 返回最后一个元素
begin() 返回指向第一个元素的迭代器
end() 返回指向尾部的迭代器
pop_back() 删除尾部的元素
pop_front() 删除头部的元素
push_back() 在尾部加入一个元素
push_front() 在头部加入一个元素
#include <queue>
#include <string>
class Person
{
public:
Person(string name, int age)
{
this->m_Name = name;
this->m_Age = age;
}
string m_Name;
int m_Age;
};
void test01()
{
//创建队列
queue<Person> q;
//准备数据
Person p1("唐僧", 30);
Person p2("孙悟空", 1000);
Person p3("猪八戒", 900);
Person p4("沙僧", 800);
//向队列中添加元素 入队操作
q.push(p1);
q.push(p2);
q.push(p3);
q.push(p4);
//队列不提供迭代器,更不支持随机访问
while (!q.empty()) {
//输出队头元素
cout << "队头元素-- 姓名: " << q.front().m_Name
<< " 年龄: "<< q.front().m_Age << endl;
cout << "队尾元素-- 姓名: " << q.back().m_Name
<< " 年龄: " << q.back().m_Age << endl;
cout << endl;
//弹出队头元素
q.pop();
}
cout << "队列大小为:" << q.size() << endl;
}
int main()
{
test01();
system("pause");
return 0;
}
priority_queue
#include<queue>
//小顶堆 从小到大排列 只保证队列头部为最小的,并不保证内部完全有序
priority_queue<int,vector<int>,greater<int> > q
//大顶堆 从大到小排列
priority_queue<int,vector<int>,less<int> >
//默认是大顶堆
priority_queue<int> q;
//sort greater<int> 是从大到小排
top 访问队头元素
empty 队列是否为空
size 返回队列内元素个数
push 插入元素到队尾 (并排序)
emplace 原地构造一个元素并插入队列
pop 弹出队头元素
swap 交换内容
//自定义比较函数 返回ture得情况 表示优先级高 优先级高得放堆顶
stl给我们提供了两个仿函数 用于比较int类型
仿函数得本质是重载()
Stl仿函数 less<int>
template<class _Ty = void>
struct less
{ // functor for operator<
typedef _Ty first_argument_type;
typedef _Ty second_argument_type;
typedef bool result_type;
constexpr bool operator()(const _Ty& _Left, const _Ty& _Right) const
{ // apply operator< to operands
return (_Left < _Right);
}
};
//自定义仿函数
struct cmp
{
bool operator()( pair<int,int> a, pair<int,int> b)
{
if(a.first<b.first) return false;
else if(a.first==b.first)
{
if(a.second<b.second) return false;
else return true;
}
else
{
return true;
}
}
hash_map
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A8lCekXj-1629084912962)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210407150759304.png)]
由上图可知,每一个存入HashMap的key-velue,key都会经过哈希算法,计算出一个数字index,然后将value存放在数组arr[index]的元素上。由此可见,每次取值也只需要通过key算出index,去arr[index]处读取值即可。这样的存值和读取每一次的步骤几乎是一个差不多的常数,所以时间复杂度为0(1)。
观察上面的实现步骤我们可以发现,哈希算法才是整个取值和读值的重点,我们无法保证不同的key不会算出一样的index,如果算出一样的index,一个数组元素里如何存放另两个值?这种现像,我们叫做哈希碰撞,也叫哈希冲突。
哈希表 是使用O(1)时间复杂度 进行数据的插入删除和查找 但需要O(N)的额外空间
#include<unordered_map>
unordered_map<char,int> hashmap;
//插入
hashmap.insert(make_pair('a', 1));
hashmap.insert(make_pair('b', 2));
hashmap.insert(make_pair('c', 3));
//访问元素
hashmap['a']=1;
//删除一个键值
hashmap.erase(1);
//获取hash_map的大小
hash_map.size()
//清楚所有元素
hash_map.clear( )
//判空
hash_map.empty()
unordered_map<char,int>::iterator it=hashmap.find('a');
//it->second=1
//还可以像数组一样使用hash_map 但时间复杂度好像会提高
//访问一个hashmap中没有的元素好像默认会是0
if (it == hashmap.end())
{
cout << "not find" << endl;
}
else
{
cout << "find" << endl;
}
哈希
//hashset性质
//删除一个元素
hash_set.erase(s[i - 1])
//检查n是否在hash_set中 存在返回1 不存在返回0
hash_set.count(n)
//插入一个元素
hash_set.insert(n)
//获取hash_set的大小
hash_set.size()
//清楚所有元素
hash_set.clear( )
//判空
hash_set.empty()
// /*
// 1.它是无序的,即添加的顺序和遍历出来的顺序是不同的;
// 2.它里面不允许有重复元素,是因为它是基于HashMap实现的;
// 3. 保证集合中存储的值都是唯一不重复的,若插入重复的值,重复的值会替换掉原来的值
// 4.这个 pair 对象包含一个迭代器,以及一个附加的布尔值用来说明插入是否成功。
// 如果元素被插入,返回的迭代器会指向新元素;如果没有被插入,迭代器指向阻止插入的元素。
// 可以用一个迭代器作为 insert() 的第一个参数,它指定了元素被插入的位置,如果忽略插入位置,在这种情况下,只会返回一个迭代器
// */
set
map
内部实现是红黑树具有自动排序的功能 每个键只能出现一次,会键自动排序
C++ maps是一种关联式容器,包含“关键字/值”对
begin() 返回指向map头部的迭代器
clear() 删除所有元素
count() 返回指定元素出现的次数, (帮助评论区理解: 因为key值不会重复,所以只能是1 or 0)
empty() 如果map为空则返回true
end() 返回指向map末尾的迭代器
equal_range() 返回特殊条目的迭代器对
erase() 删除一个元素
find() 查找一个元素
get_allocator() 返回map的配置器
insert() 插入元素
key_comp() 返回比较元素key的函数
lower_bound() 返回键值>=给定元素的第一个位置
max_size() 返回可以容纳的最大元素个数
rbegin() 返回一个指向map尾部的逆向迭代器
rend() 返回一个指向map头部的逆向迭代器
size() 返回map中元素的个数
swap() 交换两个map
upper_bound() 返回键值>给定元素的第一个位置
value_comp() 返回比较元素value的函数
map<int, string> mapStudent;
// 定义一个map对象
map<int, string> mapStudent;
//当map中有这个关键字时,insert操作是不能在插入数据的
// 第一种 用insert函數插入pair
mapStudent.insert(pair<int, string>(000, "student_zero"));
// 第二种 用insert函数插入value_type数据
mapStudent.insert(map<int, string>::value_type(001, "student_one"));
// 第三种 用"array"方式插入
mapStudent[123] = "student_first";
mapStudent[456] = "student_second";
// find 返回迭代器指向当前查找元素的位置否则返回map::end()位置
iter = mapStudent.find("123");
if(iter != mapStudent.end())
cout<<"Find, the value is"<<iter->second<<endl;
else
cout<<"Do not Find"<<endl;
//迭代器刪除
iter = mapStudent.find("123");
mapStudent.erase(iter);
//用关键字刪除
int n = mapStudent.erase("123"); //如果刪除了會返回1,否則返回0
//用迭代器范围刪除 : 把整个map清空
mapStudent.erase(mapStudent.begin(), mapStudent.end());
//等同于mapStudent.clear()
//获取大小
int nSize = mapStudent.size();
list
assign() 给list赋值
back() 返回最后一个元素
begin() 返回指向第一个元素的迭代器
clear() 删除所有元素
empty() 如果list是空的则返回true
end() 返回末尾的迭代器
erase() 删除一个元素
front() 返回第一个元素
get_allocator() 返回list的配置器
insert() 插入一个元素到list中
max_size() 返回list能容纳的最大元素数量
merge() 合并两个list
pop_back() 删除最后一个元素
pop_front() 删除第一个元素
push_back() 在list的末尾添加一个元素
push_front() 在list的头部添加一个元素
rbegin() 返回指向第一个元素的逆向迭代器
remove() 从list删除元素
remove_if() 按指定条件删除元素
rend() 指向list末尾的逆向迭代器
resize() 改变list的大小
reverse() 把list的元素倒转
size() 返回list中的元素个数
sort() 给list排序
splice() 合并两个list
swap() 交换两个list
unique() 删除list中重复的元素
链表
面试链表手动输入输出
#include<iostream>
using namespace std;
struct ListNode {
int val;
ListNode* next;
ListNode(int x) :val(x), next(nullptr) {};
};
//链表测试输入输出
int main()
{
//ListNode* head = (ListNode*)malloc(sizeof(ListNode));
//head->val = 1;
int a;
ListNode* pre =new ListNode(1);
ListNode* head = pre;
for (int i = 0; i < 3; i++)
{
cin >> a;
ListNode* temp = new ListNode(a);
pre->next = temp;
pre = temp;
}
/*ListNode* head = new ListNode(1);
ListNode* node1 = new ListNode(2);
ListNode* node2= new ListNode(3);
head->next = node1;
node1->next = node2;*/
while (head)
{
cout << head->val<< endl;
head = head->next;
}
cout << "111" << endl;
return 0;
}
单链表
c语言版本
#include<string.h>
#include<ctype.h>
#include<malloc.h> /* malloc()等 */
#include<limits.h> /* INT_MAX等 */
#include<stdio.h> /* EOF(=^Z或F6),NULL */
#include<stdlib.h> /* atoi() */
#include<io.h> /* eof() */
#include<math.h> /* floor(),ceil(),abs() */
#include<process.h> /* exit() */
/* 函数结果状态代码 */
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Boolean; /* Boolean是布尔类型,其值是TRUE或FALSE */
typedef int ElemType;
typedef struct LNode
{
ElemType data;
struct LNode* next;
};
typedef struct LNode* LinkList;
void InitList(LinkList* L);
void DestroyList(LinkList* L);
void ClearList(LinkList L);
Status ListEmpty(LinkList L);
int ListLength(LinkList L);
Status GetElem(LinkList L, int i, ElemType* e);
int LocateElem(LinkList L, ElemType e, Status(*compare)(ElemType, ElemType));
Status PriorElem(LinkList L, ElemType cur_e, ElemType* pre_e);
Status NextElem(LinkList L, ElemType cur_e, ElemType* next_e);
Status ListInsert(LinkList L, int i, ElemType e);
Status ListDelete(LinkList L, int i, ElemType* e);
void ListTraverse(LinkList L, void (*vi)(ElemType));
Status equal(ElemType c1, ElemType c2);
int comp(ElemType a, ElemType b);
void print(ElemType c);
void print1(ElemType* c);
void print2(ElemType c);
int main()
{
LinkList L; /* 与main2-1.c不同 */
ElemType e, e0;
Status i;
int j, k;
//初始化
InitList(&L);
for (j = 1; j <= 5; j++)
i = ListInsert(L, 1, j);
printf("在L的表头依次插入1~5后:L=");
//打印
ListTraverse(L, print); /* 依次对元素调用print(),输出元素的值 */
//判空
i = ListEmpty(L);
printf("L是否空:i=%d(1:是 0:否)\n", i);
//清空
ClearList(L);
printf("清空L后:L=");
ListTraverse(L, print);
i = ListEmpty(L);
printf("L是否空:i=%d(1:是 0:否)\n", i);
//插入
for (j = 1; j <= 10; j++)
ListInsert(L, j, j);
printf("在L的表尾依次插入1~10后:L=");
ListTraverse(L, print);
//获取指定pos的元素
GetElem(L, 5, &e);
printf("第5个元素的值为:%d\n", e);
for (j = 0; j <= 1; j++)
{
//查找某个元素是否存在在链表中
k = LocateElem(L, j, equal);
if (k)
printf("第%d个元素的值为%d\n", k, j);
else
printf("没有值为%d的元素\n", j);
}
for (j = 1; j <= 2; j++) /* 测试头两个数据 */
{
GetElem(L, j, &e0); /* 把第j个数据赋给e0 */
i = PriorElem(L, e0, &e); /* 求e0的前驱 */
if (i == INFEASIBLE)
printf("元素%d无前驱\n", e0);
else
printf("元素%d的前驱为:%d\n", e0, e);
}
for (j = ListLength(L) - 1; j <= ListLength(L); j++) /* 最后两个数据 */
{
GetElem(L, j, &e0); /* 把第j个数据赋给e0 */
i = NextElem(L, e0, &e); /* 求e0的后继 */
if (i == INFEASIBLE)
printf("元素%d无后继\n", e0);
else
printf("元素%d的后继为:%d\n", e0, e);
}
k = ListLength(L); /* k为表长 */
for (j = k + 1; j >= k; j--)
{
i = ListDelete(L, j, &e); /* 删除第j个数据 */
if (i == ERROR)
printf("删除第%d个元素失败\n", j);
else
printf("删除第%d个元素成功,其值为:%d\n", j, e);
}
printf("依次输出L的元素:");
ListTraverse(L, print);
DestroyList(&L);
printf("销毁L后:L=%u\n", L);
return 0;
}
void InitList(LinkList* L)
{
/*操作结果:构造一个空的线性表*/
*L = (LinkList)malloc(sizeof(struct LNode));/*产生头结点,并使L指向此头结点*/
if (!*L)
{
exit(OVERFLOW);/*存储分配失败*/
}
(*L)->next = NULL;/*指针域为空*/
}
void DestroyList(LinkList* L)
{
/*初始条件:线性表L已存在。操作结果:销毁线性表L*/
LinkList q;
while (*L)
{
q = (*L)->next;
free(*L);
*L = q;
}
}
void ClearList(LinkList L)/*不改变L*/
{
LinkList p, q;
p = L->next;/*p指向第一个节点*/
while (p)/*没到尾表*/
{
q = p->next;
free(p);
p = q;
}
L->next = NULL;/*头结点指针域为空*/
}
Status ListEmpty(LinkList L)
{
/*初始条件:线性表L已存在。操作结果:若L为空表,则返回TRUE,否则返回FALSE*/
if (L->next)/*非空*/
{
return FALSE;
}
else
{
return TRUE;
}
}
int ListLength(LinkList L)
{
/*初始条件:线性表L已存在。操作结果:返回L中数据元素个数*/
int i = 0;
LinkList p = L->next;/*p指向第一个结点*/
while (p)/*没到表尾*/
{
i++;
p = p->next;
}
return i;
}
Status GetElem(LinkList L, int i, ElemType* e)
{
/*L为带头结点的单链表的头指针。当第i个元素存在时,其值赋给e并返回OK,否则返回ERROR*/
int j = 1;/*j为计数器*/
LinkList p = L->next;/*p指向第一个结点*/
while (p && j < i)/*顺指针向后查找,直到p指向第i个元素或p为空*/
{
p = p->next;
j++;
}
if (!p || j > i)/*第i个元素不存在*/
{
return ERROR;
}
*e = p->data;/*取第i个元素*/
return OK;
}
int LocateElem(LinkList L, ElemType e, Status(*compare)(ElemType, ElemType))
{
/*初始条件:线性表L已存在,compare()是数据元素判定函数(满足为1,否则为0)*/
/*操作结果:返回L中第1个与e满足关系compare()的数据元素的位序。*/
/*若这样的数据元素不存在,则返回值为0*/
int i = 0;
LinkList p = L->next;
while (p)
{
i++;
if (compare(p->data, e))/*找到这样的元素*/
{
return i;
}
p = p->next;
}
return 0;
}
Status PriorElem(LinkList L, ElemType cur_e, ElemType* pre_e)
{
/*初始条件:线性表L已存在*/
/*操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的前驱,*/
/* 返回OK;否则操作失败,pre_e无定义,返回INFEASIBLE*/
LinkList q, p = L->next;/*p指向第一个结点*/
while (p->next)/*p所指结点有后继*/
{
q = p->next;/*q为p的后继*/
if (q->data == cur_e)
{
*pre_e = p->data;
return OK;
}
p = q;/*p向后移*/
}
return INFEASIBLE;
}
Status NextElem(LinkList L, ElemType cur_e, ElemType* next_e)
{
/*初始条件:线性表L已存在*/
/*操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继,*/
/* 返回OK;否则操作失败,next_e无定义,返回INFEASIBLE*/
LinkList p = L->next;/*p指向第一个结点*/
while (p->next)/*p所指结点有后继*/
{
if (p->data == cur_e)
{
*next_e = p->next->data;
return OK;
}
p = p->next;
}
return INFEASIBLE;
}
Status ListInsert(LinkList L, int i, ElemType e)/*不改变L*/
{
/*在带头结点的单链线性表L中第i个位置之前插入元素e*/
int j = 0;
LinkList p = L, s;
while (p && j < (i - 1))/*寻找第i-1个结点*/
{
p = p->next;
j++;
}
if (!p || j > (i - 1))/*i小于1或者大于表长*/
{
return ERROR;
}
s = (LinkList)malloc(sizeof(struct LNode));/*生成新结点*/
s->data = e;/*插入L中*/
s->next = p->next;
p->next = s;
return OK;
}
Status ListDelete(LinkList L, int i, ElemType* e)/*不改变L*/
{
/*在带头结点的单链线性表L中,删除第i个元素,并由e返回其值*/
int j = 0;
LinkList p = L, q;
while (p->next && j < (i - 1))/*寻找第i个结点,并令p指向其前驱*/
{
p = p->next;
j++;
}
if ((!p->next) || j > (i - 1))/*删除位置不合理*/
{
return ERROR;
}
q = p->next;/*删除并释放结点*/
p->next = q->next;
*e = q->data;
free(q);
return OK;
}
void ListTraverse(LinkList L, void (*vi)(ElemType))
{
/*vi的形参类型为ElemType*/
/*初始条件:线性表L已存在。操作结果:依次对L的每个数据元素调用函数vi()*/
LinkList p = L->next;
while (p)
{
vi(p->data);
p = p->next;
}
printf("\n");
}
Status equal(ElemType c1, ElemType c2)
{
/*判断是否相等的函数*/
if (c1 == c2)
{
return TRUE;
}
else
{
return FALSE;
}
}
int comp(ElemType a, ElemType b)
{
/*根据a<,=或>b,分别返回-1,0或1*/
if (a == b)
{
return 0;
}
else
{
return (a - b) / abs(a - b);
}
}
void print(ElemType c)
{
printf("%d", c);
}
void print1(ElemType* c)
{
printf("%d", *c);
}
void print2(ElemType c)
{
printf("%c", c);
}
具有实用意义的线性链表
为了熟悉链表操作可以自己将所有函数实现一下
#include<string.h>
#include<ctype.h>
#include<malloc.h> /* malloc()等 */
#include<limits.h> /* INT_MAX等 */
#include<stdio.h> /* EOF(=^Z或F6),NULL */
#include<stdlib.h> /* atoi() */
#include<io.h> /* eof() */
#include<math.h> /* floor(),ceil(),abs() */
#include<process.h> /* exit() */
/* 函数结果状态代码 */
#define TRUE 1
#define FALSE 0
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
/* #define OVERFLOW -2 因为在math.h中已定义OVERFLOW的值为3,故去掉此行 */
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Boolean; /* Boolean是布尔类型,其值是TRUE或FALSE */
typedef int ElemType;
typedef struct LNode //结点类型
{
ElemType data;
struct LNode *next;
}LNode, *Link, *Position;
typedef struct LinkList//链表类型
{
Link head, tail;//分别指向线性链表中的头结点和最后一个结点
int len;//指示线性链表中数据元素的个数
}LinkList;
void MakeNode(Link *p, ElemType e);
void FreeNode(Link *p);
void InitList(LinkList *L);
void ClearList(LinkList *L);
void DestroyList(LinkList *L);
void InsFirst(LinkList *L, Link h, Link s);//形参增加L,因为需修改L
Status DelFirst(LinkList *L, Link h, Link *q);//形参增加L,因为需修改L
void Append(LinkList *L, Link s);
Position PriorPos(LinkList L, Link p);
Status Remove(LinkList *L, Link *q);
void InsBefore(LinkList *L, Link *p, Link s);
void InsAfter(LinkList *L, Link *p, Link s);
void SetCurElem(Link p, ElemType e);
ElemType GetCurElem(Link p);
Status ListEmpty(LinkList L);
int ListLength(LinkList L);
Position GetHead(LinkList L);
Position GetLast(LinkList L);
Position NextPos(Link p);
Status LocatePos(LinkList L, int i, Link *p);
Position LocateElem(LinkList L, ElemType e, Status (*compare)(ElemType, ElemType));
void ListTraverse(LinkList L, void(*visit)(ElemType));
void OrderInsert(LinkList *L, ElemType e, int (*comp)(ElemType, ElemType));
Status LocateElemP(LinkList L, ElemType e, Position *q, int(*compare)(ElemType, ElemType));
Status equal(ElemType c1, ElemType c2);
int comp(ElemType a, ElemType b);
void print(ElemType c);
void print2(ElemType c);
void print1(ElemType *c);
int main()
{
Link p,h;
LinkList L;
Status i;
int j,k;
InitList(&L); /* 初始化空的线性表L */
for(j=1;j<=2;j++)
{
MakeNode(&p,j); /* 生成由p指向、值为j的结点 */
InsFirst(&L,L.tail,p); /* 插在表尾 */
}
OrderInsert(&L,0,comp); /* 按升序插在有序表头 */
for(j=0;j<=3;j++)
{
i=LocateElemP(L,j,&p,comp);
if(i)
{
printf("链表中有值为%d的元素。\n",p->data);
}
else
{
printf("链表中没有值为%d的元素。\n",j);
}
}
printf("输出链表:");
ListTraverse(L,print); /* 输出L */
for(j=1;j<=4;j++)
{
printf("删除表头结点:");
DelFirst(&L,L.head,&p); /* 删除L的首结点,并以p返回 */
if(p)
{
printf("%d\n",GetCurElem(p));
}
else
{
printf("表空,无法删除 p=%u\n",p);
}
}
printf("L中结点个数=%d L是否空 %d(1:空 0:否)\n",ListLength(L),ListEmpty(L));
MakeNode(&p,10);
p->next=NULL; /* 尾结点 */
for(j=4;j>=1;j--)
{
MakeNode(&h,j*2);
h->next=p;
p=h;
} /* h指向一串5个结点,其值依次是2 4 6 8 10 */
Append(&L,h); /* 把结点h链接在线性链表L的最后一个结点之后 */
OrderInsert(&L,12,comp); /* 按升序插在有序表尾头 */
OrderInsert(&L,7,comp); /* 按升序插在有序表中间 */
printf("输出链表:");
ListTraverse(L,print); /* 输出L */
for(j=1;j<=2;j++)
{
p=LocateElem(L,j*5,equal);
if(p)
{
printf("L中存在值为%d的结点。\n",j*5);
}
else
{
printf("L中不存在值为%d的结点。\n",j*5);
}
}
for(j=1;j<=2;j++)
{
LocatePos(L,j,&p); /* p指向L的第j个结点 */
h=PriorPos(L,p); /* h指向p的前驱 */
if(h)
{
printf("%d的前驱是%d。\n",p->data,h->data);
}
else
{
printf("%d没前驱。\n",p->data);
}
}
k=ListLength(L);
for(j=k-1;j<=k;j++)
{
LocatePos(L,j,&p); /* p指向L的第j个结点 */
h=NextPos(p); /* h指向p的后继 */
if(h)
{
printf("%d的后继是%d。\n",p->data,h->data);
}
else
{
printf("%d没后继。\n",p->data);
}
}
printf("L中结点个数=%d L是否空 %d(1:空 0:否)\n",ListLength(L),ListEmpty(L));
p=GetLast(L); /* p指向最后一个结点 */
SetCurElem(p,15); /* 将最后一个结点的值变为15 */
printf("第1个元素为%d 最后1个元素为%d\n",GetCurElem(GetHead(L)->next),GetCurElem(p));
MakeNode(&h,10);
InsBefore(&L,&p,h); /* 将10插到尾结点之前,p指向新结点 */
p=p->next; /* p恢复为尾结点 */
MakeNode(&h,20);
InsAfter(&L,&p,h); /* 将20插到尾结点之后 */
k=ListLength(L);
printf("依次删除表尾结点并输出其值:");
for(j=0;j<=k;j++)
{
i=Remove(&L,&p);
if(!i) /* 删除不成功 */
{
printf("删除不成功 p=%u\n",p);
}
else
{
printf("%d ",p->data);
}
}
return 0;
}
void MakeNode(Link *p, ElemType e)
{
//分配有p指向的值为e的结点。若分配失败,则退出
*p = (Link)malloc(sizeof(LNode));
if (!*p)
{
exit(ERROR);
}
(*p)->data = e;
}
void FreeNode(Link *p)
{
//释放p所指结点
free(*p);
*p = NULL;
}
void InitList(LinkList *L)
{
//构造一个空的线性链表L
Link p;
p = (Link)malloc(sizeof(LNode));//生成头结点
if (p)
{
p->next = NULL;
(*L).head = (*L).tail = p;
(*L).len = 0;
}
else
{
exit(ERROR);
}
}
void ClearList(LinkList *L)
{
//将线性链表L重置为空表,并释放原链表的结点空间
Link p, q;
if ( (*L).head != (*L).tail )//不是空表
{
p = q = (*L).head->next;
(*L).head->next = NULL;
while ( p != (*L).tail )
{
p = q->next;
free(q);
q = p;
}
free(q);
(*L).tail = (*L).head;
(*L).len = 0;
}//if
}
void DestroyList(LinkList *L)
{
//销毁线性链表L,L不再存在
ClearList(L);//清空链表
FreeNode( &(*L).head );
(*L).tail = NULL;
(*L).len = 0;
}
void InsFirst(LinkList *L, Link h, Link s)//形参增加L,因为需修改L
{
//h指向L的一个结点,
//把h当做头结点,将s所指结点插入在第一个结点之前
s->next = h->next;
h->next = s;
if (h == (*L).tail)//h指向尾结点
{
(*L).tail = h->next;//修改尾指针
}
(*L).len++;
}
Status DelFirst(LinkList *L, Link h, Link *q)//形参增加L,因为需修改L
{
//h指向L的一个结点,把h当做头结点,删除链表中的第一个结点并以q返回。
//若链表为空(h指向尾结点),q=NULL,返回FALSE
*q = h->next;
if (*q)//链表非空
{
h->next = (*q)->next;
if (!h->next)//删除尾结点
{
(*L).tail = h;//修改尾指针
}
(*L).len--;
return OK;
}
else
{
return FALSE;//链表空
}
}
void Append(LinkList *L, Link s)
{
//将指针s(s->data为第一个数据元素)所指(彼此以指针相连以NULL结尾)的
//一串结点链接在线性链表L的最后一个结点之后,并改变链表L的尾指针指向新的尾结点
int i = 1;
(*L).tail->next = s;
while (s->next)
{
s = s->next;
i++;
}
(*L).tail = s;
(*L).len += i;
}
Position PriorPos(LinkList L, Link p)
{
//已知p指向线性链表L中的一个结点,返回p所指结点的直接前驱
//的位置。若无前驱,则返回NULL
Link q;
q = L.head->next;
if (q == p)//无前驱
{
return NULL;
}
else
{
while (q->next != p)//q不是p的直接前驱
{
q = q->next;
}
return q;
}
}
Status Remove(LinkList *L, Link *q)
{
//删除线性表L中的尾结点并以q返回,改变链表L的尾指针指向新的尾结点
Link p = (*L).head;
if ( (*L).len == 0 )//空表
{
*q = NULL;
return FALSE;
}
while (p->next != (*L).tail)
{
p = p->next;
}
*q = (*L).tail;
p->next = NULL;
(*L).tail = p;
(*L).len--;
return OK;
}
void InsBefore(LinkList *L, Link *p, Link s)
{
//已知p指向线性链表L中的一个结点,将s所指结点插入在p所指结点之前
//并修改指针p指向新插入的结点
Link q;
q = PriorPos(*L, *p);//q是p的前驱
if (!q)//p无前驱
{
q = (*L).head;
}
s->next = *p;
q->next = s;
*p = s;
(*L).len++;
}
void InsAfter(LinkList *L, Link *p, Link s)
{
//已知p指向线性链表L中的一个结点,将s所指结点插入在p所指结点之后
//并修改指针p指向新插入的结点
if (*p == (*L).tail)//修改尾指针
{
(*L).tail = s;
}
s->next = (*p)->next;
(*p)->next = s;
*p = s;
(*L).len++;
}
void SetCurElem(Link p, ElemType e)
{
//已经p指向线性链表中的一个结点,用e更新p所指结点中数据元素的值
p->data = e;
}
ElemType GetCurElem(Link p)
{
//已知p指向线性链表中的一个结点,返回p所指结点中数据元素的值
return p->data;
}
Status ListEmpty(LinkList L)
{
//若线性链表L为空表,则返回TRUE,否则返回FALSE
if (L.len)
{
return FALSE;
}
else
{
return TRUE;
}
}
int ListLength(LinkList L)
{
//返回线性链表L中元素个数
return L.len;
}
Position GetHead(LinkList L)
{
//返回线性链表L中头结点的位置
return L.head;
}
Position GetLast(LinkList L)
{
//返回线性链表L中最后一个结点的位置
return L.tail;
}
Position NextPos(Link p)
{
//已知p指向线性链表L中的一个结点,返回p所指结点的直接后继的位置。
//若无后继,则返回NULL
return p->next;
}
Status LocatePos(LinkList L, int i, Link *p)
{
//返回p指示线性链表L中第i个结点的位置,并返回OK,
//i值不合法是返回ERROR。i=0为头结点
int j;
if ((i<0) || (i>L.len))
{
return ERROR;
}
else
{
*p = L.head;
for(j=1; j<=i; j++)
{
*p = (*p)->next;
}
return OK;
}
}
Position LocateElem(LinkList L, ElemType e, Status (*compare)(ElemType, ElemType))
{
//返回线性链表L中第1个与e满足函数compare()判定关系的元素的位置
//若不存在这样的元素,则返回NULL
Link p = L.head;
do
{
p = p->next;
}
while( p && (compare(p->data, e)) );//没到表尾且没找到满足关系的元素
return p;
}
void ListTraverse(LinkList L, void(*visit)(ElemType))
{
//依次对L的每个数据元素调用函数visit()
Link p = L.head->next;
int j;
for (j=1; j<=L.len; j++)
{
visit(p->data);
p = p->next;
}
printf("\n");
}
void OrderInsert(LinkList *L, ElemType e, int (*comp)(ElemType, ElemType))
{
//已知L为有序线性链表,将元素e按非降序插入在L中。(用于一元多项式)
Link o, p, q;
q = (*L).head;
p = q->next;
while ( (p!=NULL) && (comp(p->data, e)<0) )//p不是尾表且元素值小于e
{
q = p;
p = p->next;
}
o = (Link)malloc(sizeof(LNode));//生成结点
o->data = e;//赋值
q->next = o;//插入
o->next = p;
(*L).len++;//表长加1
if(!p)//插在表尾
{
(*L).tail = o;//修改尾结点
}
}
Status LocateElemP(LinkList L, ElemType e, Position *q, int(*compare)(ElemType, ElemType))
{
//若升序链表L中存在与e满足判定函数compare()取值为0的元素,则q指示L中
//第一个值为e的结点的位置,并返回TRUE,否则q指示第一个与e满足判定函数
//compare()取值>0的元素的前驱的位置。并返回FALSE。(用于一元多项式)
Link p = L.head, pp;
do
{
pp = p;
p = p->next;
}while( p && (compare(p->data, e) < 0) );
//没到表尾且p->data.expn<e.expn
if ( (!p) || (compare(p->data, e) > 0) )//到表尾或compare(p->data, e)>0
{
*q = pp;
return FALSE;
}
else//找到
{
*q = p;
return TRUE;
}
}
Status equal(ElemType c1, ElemType c2)
{
//判断是否相等的函数
if (c1 == c2)
{
return TRUE;
}
else
{
return FALSE;
}
}
int comp(ElemType a, ElemType b)
{
//根据a<、=或>b,分别返回-1、0或1
if (a == b)
{
return 0;
}
else
{
return (a-b) / abs(a-b);
}
}
void print(ElemType c)
{
printf("%d ", c);
}
void print2(ElemType c)
{
printf("%c ", c);
}
void print1(ElemType *c)
{
printf("%d ", *c);
}
刷题链表测试API
struct ListNode {
int val;
ListNode* next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode* next) : val(x), next(next) {}
};
ListNode* InitList()
{
/*操作结果:构造一个空的线性表*/
ListNode* L = new ListNode;/*产生头结点,并使L指向此头结点*/
L->val = -1;
(L)->next = NULL;/*指针域为空*/
return L;
}
void ListInsert(ListNode* L, int e)/*不改变L*/
{
if (L->val == -1)
{
L->val = e;
return;
}
ListNode* temp = new ListNode;
temp->val = e;
temp->next = nullptr;
while (L->next != nullptr)
{
L = L->next;
}
L->next = temp;
}
void list_printf_all(ListNode* L)
{
while (L != nullptr)
{
cout << L->val << endl;
L = L->next;
}
}
int main()
{
ListNode* list = InitList();
for (int i = 1; i < 6; i++)
{
ListInsert(list, i);
}
list_printf_all(list);
return 0;
}
反转链表
迭代写法
核心思想:让后一个指针的next指向前一个指针
注意点: 需要一个temp来保存后一个指针的next 需要一个前驱节点和当前节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head)
{
//1.定义三个指针变量 prev代表前一个节点初始指向NULL curr代表当前节点指向head temp用于存放curr.next
//2.修改该节点的指向时,首先存放curr.next到temp中 将curr指向prev 完成反转 然后将curr指针指向下一个节点 prev指向当前节点
ListNode* prev = nullptr;
ListNode* curr = head;
ListNode* temp;
while(curr)
{
temp = curr->next;
curr->next = prev;
prev = curr;
curr = temp;
}
return prev;
}
};
递归写法
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head)
{
//递归结束条件
if (!head || !head->next)
{
return head;//返回的是链表最后一个节点,也就是反转后链表的头节点
}
//递归公式
ListNode*p= reverseList(head->next);//p used to return
//返回原链表的尾节点
//最小子问题
head->next->next = head;
head->next = nullptr;
return p;//返回原链表的尾节点
}
};
好理解的递归
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* reverseList(ListNode* head)
{
return reverse(nullptr,head);
}
ListNode* reverse(ListNode* pre,ListNode* cur)
{
if(cur==nullptr) return pre;
ListNode* temp=cur->next;
//完成反转动作
cur->next=pre;
//递归参数传递相当于迭代法的双指针更新
return reverse(cur,temp);
}
};
k个一组翻转链表
#include<iostream>
using namespace std;
struct ListNode
{
int val;
ListNode* next;
ListNode(int x) { val = x; }
};
//9:46
//1.断开待反转区域与后面的连接 并保存
//2.保存上一段的最后一个结点 并连接
//定义一个pre节点指向待反转链表的前面
class Solution {
public:
//输入一个头节点 翻转链表 返回翻转后的头节点
ListNode* reverse(ListNode* head)
{
ListNode* temp;
ListNode* pre = nullptr;
ListNode* cur = head;
while (cur)
{
temp = cur->next;
cur->next = pre;
pre = cur;
cur = temp;
}
return pre;
}
ListNode* reverseKGroup(ListNode* head, int k)
{
ListNode* dummy = new ListNode(0);
dummy->next = head;
ListNode* pre = dummy;//指向待翻转链表的前一个
ListNode* end = dummy;//指向待翻转链表的结尾
//head指向待翻转链表的开头
while (end->next)
{
//查找是否还有k个节点
for (int i = 0; i < k; i++)
{
end = end->next;
//不够k个
if (end == nullptr) return dummy->next;
}
//end 现在指向待翻转链表的结尾
ListNode* temp1 = end->next;//保存下一段的开头
//断开
end->next = nullptr;
ListNode* temp2 = pre->next;//保存这一段的开头
pre->next = reverse(temp2);//前一个节点连接新头节点
temp2->next = temp1;//这一段结尾与下一段开头连接
//更新pre和end
pre = temp2;
end = temp2;
}
return dummy->next;
}
};
合并两个有序链表
迭代
ListNode* mergeTwoLists1(ListNode* l1, ListNode* l2)
{
ListNode* prev = new ListNode(-1);//设定一个哑节点 便于返回
ListNode* l3 = prev;
while (l1 != nullptr && l2 != nullptr)
{
if (l1->val < l2->val)
{
l3->next = l1;
l1 = l1->next;
l3 = l3->next;
}
else
{
l3->next = l2;
l2 = l2->next;
l3 = l3->next;
}
}
if (l1 != nullptr)
{
l3->next = l1;
}
if (l2 != nullptr)
{
l3->next = l2;
}
// l3->next = (!l1) ? l1 : l2;
return prev->next;
}
递归
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
//递归函数功能 合并两个有序链表并返回剩下部分的头节点
//递归结束条件 一个链表到底
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
{
//相当于输入的两个小的有序链表 要返回这两个小的局部链表合并后的头节点
//递的时候并没有连起来 等待返回才连起来
if(!l1) return l2;
if(!l2) return l1;
if(l1->val<l2->val)
{
l1->next=mergeTwoLists(l1->next,l2);
return l1;
}
/*
else if(l1->val=l2->val) //如果考虑去重
{
l2->next=mergeTwoLists(l1->next,l2->next);
return l2;
}
*/
else
{
l2->next=mergeTwoLists(l1,l2->next);
return l2;
}
}
};
两两交换链表中的节点
迭代
ListNode* swapPairs(ListNode* head)
{
ListNode* prev_ya = new ListNode(-1);
ListNode* prev = prev_ya;
ListNode* curr = head;
ListNode* temp=nullptr;
if (curr == nullptr ) return nullptr;
if (curr->next == nullptr)
{
return curr;
}
while (curr != nullptr && curr->next!=nullptr)
{
prev->next = curr->next;
temp = curr->next->next;
curr->next->next = curr;
prev = curr;
curr->next = temp;
curr = temp;
}
return prev_ya->next;
}
求两个链表相交的位置
哈希
//1.遍历链表1 将每个节点的值保存到哈希集合中
//2.遍历链表2同时在哈希集合中判断中是否存在相同的地址节点
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB)
{
unordered_set<ListNode*> headA_node;//空间 O(m)
while (headA)//时间 o(m)
{
headA_node.insert(headA);
headA = headA->next;
}
while (headB)//时间 o(n)
{
unordered_set<ListNode*>::iterator it = headA_node.find(headB);
if (it != headA_node.end())
{
return headB;
}
headB = headB->next;
}
return NULL;
}
智力
ListNode* getIntersectionNode2(ListNode* headA, ListNode* headB)
{
ListNode* l_a = headA;
ListNode* l_b = headB;
while (l_a != l_b) //o(m+n)
{
l_a = (l_a) ? l_a->next : headB;
l_b = (l_b) ? l_b->next : headA;
}
return l_a;
}
LRU缓存机制
考察点主要是双向链表和哈希表的结合 细节比较容易出错
#include<iostream>
using namespace std;
#include<unordered_map>
//9:48
//哈希表存放键和双向链表的node
//双向链表头部存放最新的 尾部存放最旧的 设计一个伪头节点和伪尾节点
//get 在hash_map 中查找是否有对应得键 如果有 返回 并把该节点移动到双向链表得开头
//put
//如果超过容量 删除双向链表得尾节点 并从hash中删除
//如果在哈希中有则 更改值 并把该节点移动到双向链表得开头 ; 如果没有插入 并插入双链表得开头
class LRUCache
{
public:
//双向链表
struct Node
{
int val;
int key;
Node* pre;
Node* next;
Node(int _key, int _val) :key(_key), val(_val), pre(nullptr), next(nullptr) {}
};
unordered_map<int, Node*> hash_map;
int cap;//总容量
Node* dummy_head;
Node* dummy_tail;
LRUCache(int capacity)
{
cap = capacity;
//建立头尾哑节点 并构建初始双向链表
dummy_head = new Node(-1, -1);
dummy_tail = new Node(-1, -1);
dummy_head->next = dummy_tail;
dummy_tail->pre = dummy_head;
}
//在双向链表头部中插入一个元素
void insert(Node* p)
{
p->next = dummy_head->next;
dummy_head->next->pre = p;
dummy_head->next = p;
p->pre = dummy_head;
}
//在双向链表中删除一个指定元素
//传入这个节点的地址
void remove (Node* p)
{
p->pre->next = p->next;
p->next->pre = p->pre;
}
void put(int key, int value)
{
// cout<<"put key="<<key<<"value= "<<value<<endl;
//如果存在就更新 并在双向链表中把他移动到头部
if (hash_map.find(key) != hash_map.end())
{
hash_map[key]->val = value;
remove(hash_map[key]);
insert(hash_map[key]);
}
else//不存在
{
// cout<<"hash_map.size()= "<<hash_map.size()<<"cap "<<cap<<endl;
//满了 删除双向链表的最后一个节点
if (hash_map.size() == cap)
{
cout<<"full"<<endl;
auto p=dummy_tail->pre;
hash_map.erase(p->key);
remove(p);
delete p;
}
Node* temp = new Node(key, value);
hash_map.insert(make_pair(key, temp));
insert(temp);
}
}
int get(int key)
{
// cout<<"get key="<<key<<endl;
// cout<<"hash.size()"<<hash_map.size()<<endl;
// cout<<"dummy_head->next->key= "<<dummy_head->next->key<<endl;
// cout<<"dummy_head->next->next->key= "<<dummy_head->next->next->key<<endl;
//存在 就返回 并在双向链表中移动到头部
if (hash_map.find(key) != hash_map.end())
{
// cout<<"--"<<endl;
Node* p=hash_map[key];
remove(p);
// cout<<"--"<<endl;
insert(p);
// cout<<"--"<<endl;
return p->val;
}
else//不存在
{
return -1;
}
}
};
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache* obj = new LRUCache(capacity);
* int param_1 = obj->get(key);
* obj->put(key,value);
*/
重排链表
这里题两个解法都要掌握,一个是可以借助辅助空间 一个是原地修改
先遍历一遍把节点存进数组中,再进行修改
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
//10:25
//10:31
//10:40
//10:59
class Solution
{
public:
void reorderList(ListNode* head)
{
ListNode* p=head;
vector<ListNode*> path;
int index=0;
while(head)
{
path.push_back(head);
head=head->next;
index++;
}
index=index-1;
if(index<2) return ;
int i=0;
while(i+1<index)
{
ListNode* temp=path[index];
temp->next=p->next;
p->next=temp;
path[index-1]->next=nullptr;
p=temp->next;
index--;
i++;
}
return;
}
};
原地修改
快慢指针寻找链表的中点+翻转链表+合并两个链表
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
//11:38
class Solution
{
public:
//找到链表的中点 返回中点的头节点 快慢指针 <=2不考虑
ListNode* find_mid_list(ListNode* head)
{
ListNode* fast=head;
ListNode* slow=head;
while(fast->next&&fast->next->next)
{
slow=slow->next;
fast=fast->next->next;
}
return slow;
}
//反转链表
ListNode* reverse(ListNode* head)
{
ListNode* dummy=nullptr;
ListNode* pre=dummy;
while(head)
{
ListNode* tmp;
tmp=head->next;
head->next=pre;
pre=head;
head=tmp;
}
return pre;
}
//合并链表 间隔合并两个链表 以headA为头
void merge_list(ListNode* headA,ListNode* headB)
{
ListNode* p=headA;
while(headA&&headB)
{
headA=headA->next;
p->next=headB;
headB=headB->next;
p->next->next=headA;
p=p->next->next;
}
}
void reorderList(ListNode* head)
{
ListNode* headA=head;
ListNode* headB;
//<=2 直接返回
if(!head||!head->next||!head->next->next) return ;
headB=find_mid_list(headA);
//断开
ListNode* temp=headB;
headB=headB->next;
temp->next=nullptr;
ListNode* headB_rev=reverse(headB);//反转后的头节点
merge_list(headA,headB_rev);
}
};
树
树的概念及性质
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N7y4WA3A-1629084912963)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210423224934547.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-90F9h5Oa-1629084912963)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210423224951024.png)]
完全二叉树 除最后一层节点外,其他层节点都必须要有两个子节点,并且最后一层节点都要左排列
完全二叉树便于顺序存储
补充知识点:完全二叉树的应用
堆是一个完全二叉树。
堆根据元素的排列方式,可以分为最大堆(max-heap)和最小堆(min-heap),其中:
最大堆:是最大的完全二叉树,其每个节点的值都大于或等于其子节点。所以在最大堆中,根结点是该完全二叉树的最大结点。
最小堆:是最小的完全二叉树,其每个节点的值都小于或等于其子节点。所以在最小堆中,根结点是该完全二叉树的最小结点。
堆并不归属于STL库容器组件,而是作为优先队列(priority queue)的底层实现。堆是一种完全二叉树,整棵二叉树除了最底层的叶子节点之外是填满的,而最底层的叶子节点由左至右是没有空隙的,所以我们可以利用array 或者vector来存储所有的节点。
树的表示方法
#include "stdio.h"
#include "string.h"
#include "stdlib.h"
/*
struct BiNode
{
int data;
struct BiNode *lchild, *rchild;
};
//重新重命名类型
typedef struct BiNode BiNode;
//也是重命名类型 是一个指针的数据类型
typedef struct BiNode *BiTree;
*/
//二叉链表 重点
typedef struct BiNode
{
int data;
struct BiNode *lchild, *rchild;
}BiNode, *BiTree;
//三叉链表
typedef struct TriTNode
{
int data;
//左右孩子指针
struct TriTNode *lchild, *rchild;
struct TriTNode *parent;
}TriTNode, *TriTree;
//双亲链表
#define MAX_TREE_SIZE 100
typedef struct BPTNode
{
int data;
int parentPosition; //指向双亲的指针 //数组下标
char LRTag; //左右孩子标志域
}BPTNode;
typedef struct BPTree
{
BPTNode nodes[100]; //因为节点之间是分散的,需要把节点存储到数组中
int num_node; //节点数目
int root; //根结点的位置 //注意此域存储的是父亲节点在数组的下标
}BPTree;
//用这个数据结构能表达出一颗树。。。能,怎么表达?不能why
void main11()
{
BPTree tree;
BiNode t1, t2, t3, t4, t5;
memset(&t1, 0, sizeof(BiNode));//初始化时应指向空
memset(&t2, 0, sizeof(BiNode));
memset(&t3, 0, sizeof(BiNode));
memset(&t4, 0, sizeof(BiNode));
memset(&t5, 0, sizeof(BiNode));
t1.data = 1;
t2.data = 2;
t3.data = 3;
t4.data = 4;
t5.data = 5;
//建立树关系
//t1的左孩子为t2
t1.lchild = &t2;
//t1的右孩子为t3
t1.rchild = &t3;
//t2的左孩子为t4
t2.lchild = &t4;
//t3的左孩子为t5
t3.lchild = &t5;
system("pause");
}
void main()
{
BiTree p1, p2, p3, p4, p5;
p1 = (BiTree)malloc(sizeof(BiNode));
p2 = (BiTree)malloc(sizeof(BiNode));
p3 = (BiTree)malloc(sizeof(BiNode));
p4 = (BiTree)malloc(sizeof(BiNode));
p5 = (BiTree)malloc(sizeof(BiNode));
memset(p1, 0, sizeof(BiNode));
memset(p2, 0, sizeof(BiNode));
memset(p3, 0, sizeof(BiNode));
memset(p4, 0, sizeof(BiNode));
memset(p5, 0, sizeof(BiNode));
p1->data = 1;
p2->data = 2;
p3->data = 3;
p4->data = 4;
p5->data = 5;
//建立树关系
//t1的左孩子为t2
p1->lchild = p2;
//t1的右孩子为t3
p1->rchild = p3;
//t2的左孩子为t4
p2->lchild = p4;
//t3的左孩子为t5
p3->lchild = p5;
system("pause");
}
树的遍历
//二叉链表
typedef struct BiNode
{
int data;
struct BiNode *lchild, *rchild;
}BiNode, *BiTree;
//中序遍历
void inOrder(BiNode *root)
{
if (root != NULL)
{
inOrder(root->lchild);
printf("%d ", root->data);
inOrder(root->rchild);
}
return ;
}
//前序遍历
void preOrder(BiNode *root)
{
if (root != NULL)
{
printf("%d ", root->data);
preOrder(root->lchild);
preOrder(root->rchild);
}
}
//后序遍历
void postOrder(BiNode *root)
{
if (root != NULL)
{
postOrder(root->lchild);
postOrder(root->rchild);
printf("%d ", root->data);
}
}
树遍历的迭代写法
前序遍历的迭代写法
//前序遍历迭代写法
class Solution
{
public:
//先讲根节点入栈 然后取出根节点 将根节点的右孩子和左孩子入栈(由于要先弹出左孩子)
vector<int> preorderTraversal(TreeNode* root)
{
vector<int> res;
stack<TreeNode*> temp_stack;
temp_stack.push(root);
if (root == nullptr) return res;
while (!temp_stack.empty())
{
TreeNode* cur = temp_stack.top();
temp_stack.pop();
res.push_back(cur->val);
if (cur->right) temp_stack.push(cur->right);
if (cur->left) temp_stack.push(cur->left);
}
return res;
}
};
中序遍历的迭代写法
如果一个节点不为空就将他插入栈,并且往左走;如果为空,就把栈顶元素取出来打印(每次在这里访问),再访问该元素的右孩子。
#include<iostream>
#include<vector>
using namespace std;
#include<stack>
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
};
class Solution {
public:
//
vector<int> inorderTraversal(TreeNode* root)
{
vector<int> res;
//栈中存放遍历的顺序
stack<TreeNode*> stack_temp;
TreeNode* cur = root;
// stack_temp.push(root->val);
while (cur != nullptr || !stack_temp.empty())
{
//如果不为空则一直递归访问左子节点 并用栈记录访问过的元素
if (cur != nullptr)
{
stack_temp.push(cur);
cur = cur->left;
}
//访问到尽头后,从栈中取出一个元素插入到结果数组中并访问该元素的右节点
else
{
cur = stack_temp.top();
res.push_back(cur->val);
stack_temp.pop();
cur = cur->right;
}
}
return res;
}
};
后序遍历的迭代写法
//后序遍历迭代写法
class Solution
{
public:
//这里有点取巧 先求出 根右左 再翻转数组 得到后序遍历的左右根
vector<int> postorderTraversal(TreeNode* root)
{
vector<int> res;
stack<TreeNode*> temp_stack;
temp_stack.push(root);
if (root == nullptr) return res;
while (!temp_stack.empty())
{
TreeNode* cur = temp_stack.top();
temp_stack.pop();
res.push_back(cur->val);
if (cur->left) temp_stack.push(cur->left);
if (cur->right) temp_stack.push(cur->right);
}
reverse(res.begin(), res.end());
return res;
}
};
二叉树的层序遍历
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root)
{
queue<TreeNode*> que;
vector<vector<int>> res;
if (root != nullptr) que.push(root);
while (!que.empty())
{
//控制每一层的大小
int size = que.size();
//每一层的值
vector<int> vec;
for (int i = 0; i < size; i++)
{
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
res.push_back(vec);
}
return res;
}
};
二叉树的层序遍历2
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
#include<algorithm>
struct TreeNode
{
int val;
TreeNode* left;
TreeNode* right;
};
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root)
{
//先自顶向下 再翻转数组
vector<vector<int>> res;
queue<TreeNode*> que;
que.push(root);
while (!que.empty())
{
int size = que.size();
vector<int> vec;
for (int i = 0; i < size; i++)
{
TreeNode* node = que.front();
que.pop();
vec.push_back(node->val);
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
res.push_back(vec);
}
reverse(res.begin(), res.end());
return res;
}
};
填充每一个节点到右侧节点的指针
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;
Node() : val(0), left(NULL), right(NULL), next(NULL) {}
Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
Node(int _val, Node* _left, Node* _right, Node* _next)
: val(_val), left(_left), right(_right), next(_next) {}
};
class Solution {
public:
Node* connect(Node* root)
{
queue<Node*> que;
if (root != nullptr) que.push(root);
while (!que.empty())
{
//控制每一层的大小
int size = que.size();
//每一层的值
vector<int> vec;
for (int i = 0; i < size; i++)
{
Node* node = que.front();
que.pop();
//前一个出队的节点指向后一个出队的节点
if(que.front()) node->next = que.front();
else node->next = nullptr;
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
}
return root;
}
};
二叉树的最近公共祖先
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
//从上往下查找如果当前节点位于p,q值区间之间就是最近公共子节点
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
while(root)
{
if(root->val>p->val&&root->val>q->val) root=root->left;
else if(root->val<p->val&&root->val<q->val) root=root->right;
else//由于不存在等于得情况,所以只剩下root位于两者之间得情况
{
return root;
}
}
return nullptr;
}
};
二叉搜索树中的插入操作
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
//直接在空结点上添加元素好像就要可以
//也可以返回空 找到应该插入的位置后 插入
TreeNode* insertIntoBST(TreeNode* root, int val)
{
if(root==nullptr)
{
TreeNode* node=new TreeNode(val);
return node;
}
if(val<root->val) root->left=insertIntoBST(root->left,val);
if(val>root->val) root->right=insertIntoBST(root->right,val);
return root;
}
};
删除二叉搜索树中的节点
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
//1.叶子节点 直接删除
//2.有一个子节点 直接把子节点续上
//3.有两个子节点 把右边的续上 把左边的接到后边的最左端点
TreeNode* deleteNode(TreeNode* root, int key)
{
if(root==nullptr) return root;
if(root->val==key)
{
//返回跟上级连接的点
//1.叶子节点 直接删除
if(root->left==nullptr&&root->right==nullptr) return nullptr;
//2.有一个子节点 直接把子节点续上
else if(root->left==nullptr&&root->right) return root->right;
else if(root->left&&root->right==nullptr) return root->left;
//3.有两个子节点 把右边的续上 把左边的接到后边的最左端点
else if(root->left&&root->right)
{
//找到右侧最左边
TreeNode* tmp=root->right;
while(tmp->left)
{
tmp=tmp->left;
}
tmp->left=root->left;
return root->right;
}
}
//做好连接准备
if(key<root->val) root->left=deleteNode(root->left,key);
if(key>root->val) root->right=deleteNode(root->right,key);
return root;
}
};
完全二叉树节点的个数
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
//返回节点个数
int countNodes(TreeNode* root)
{
if(root==nullptr) return 0;
TreeNode* left=root->left;
TreeNode* right=root->right;
int left_h=0;
int right_h=0;
//求当前节点的左右节点深度并判断是否相等
while(left)
{
left=left->left;
left_h++;
}
while(right)
{
right=right->right;
right_h++;
}
//满二叉树的性质 2^n-1
if(left_h==right_h)
{
return (2<<left_h)-1;
}
return 1+countNodes(root->left)+countNodes(root->right);
}
};
求叶子节点个数
void countLeafNum(BiNode *root)
{
if (root != NULL)
{
countLeafNum(root->lchild); //递归遍历左子树,求叶子节点数
c
if (root->lchild == NULL && root->rchild==NULL)
{
g_num ++;
}
countLeafNum(root->rchild);//递归遍历右子树,求叶子节点数
}
}
求树的深度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-baqbA9J8-1629084912964)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210425104604861.png)]
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode* root)//
{
if(root==nullptr)
{
return 0;
}
//先假设这个函数能计算最大深度,写出递归公式 再考虑边界条件
int nums=1+max(maxDepth(root->left),maxDepth(root->right));
//遍历到叶子节点时,返回1+0+0
return nums;
}
//简便写法
/*
int maxDepth(TreeNode* root)//
{
return root ? 1+max(maxDepth(root->left),maxDepth(root->right)):0
}
*/
};
判断树是否平衡
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VMwJhHpn-1629084912964)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210425172117024.png)]
//自顶而下 暴力解法 利用求树深度的函数
//递归思想 先判断该结点是否平衡 再判断
class Solution {
public:
int maxDepth(TreeNode* root)//
{
return root ? 1+max(maxDepth(root->left),maxDepth(root->right)):0;
}
bool isBalanced(TreeNode* root)
{
if(root==nullptr)
{
return true;
}
if(abs(maxDepth(root->left)-maxDepth(root->right))>1)//判断该节点是否满足要求
{
return false;
}
else
{
return isBalanced(root->left)&&isBalanced(root->right);//判断左右是否满足要求
}c
}
};
//从底至顶 类似先序遍历
/*
递归返回值:
当节点root 左 / 右子树的高度差 < 2<2 :则返回以节点root为根节点的子树的最大高度,即节点 root 的左右子树中最大高度加 11 ( max(left, right) + 1 );
当节点root 左 / 右子树的高度差 \geq 2≥2 :则返回 -1−1 ,代表 此子树不是平衡树 。
递归终止条件:
当越过叶子节点时,返回高度 00 ;
当左(右)子树高度 left== -1 时,代表此子树的 左(右)子树 不是平衡树,因此直接返回 -1−1 ;
*/
//平衡返回树的最大深度 不平衡返回-1
int helper(TreeNode* root)
{
//递归终止条件
if (root == nullptr)
{
return 0;
}
//递归公式
int left=helper(root->left);
if (left == -1) return -1;
int right = helper(root->right);
if (right == -1) return -1;
return abs(left - right) <= 1 ? (max(left, right) + 1) : -1;
//递归返回值
}
bool isBalanced(TreeNode* root)
{
return helper(root) != -1;
}
求二叉树的直径
//暴力的解法
/*
子问题 计算一个节点为起点的路径经过节点数最大值d_node 设置全局变量ans 记录该值 递归遍历每个节点
子问题解法 计算左子树和右子树的最大深度之和加1 (L+R)+1
路径数比结点数减一
*/
int ans = 0;
int maxdepth(TreeNode* root)
{
return root ? max(maxdepth(root->left), maxdepth(root->right)) + 1 : 0;
}
int helper(TreeNode* root)
{
return root ? maxdepth(root->left) + maxdepth(root->right) : 0;
}
//返回以该节点为起点路径的最大值
//更新全局变量ans
int diameterOfBinaryTree(TreeNode* root)
{
if (root == nullptr) return 0;
int temp = helper(root);
if (temp > ans)
{
ans = temp;
}
diameterOfBinaryTree(root->left);
diameterOfBinaryTree(root->right);
return ans;
}
//优化
//返回最大深度
//同时更新全局变量ans
int ans=0;
int depth(TreeNode* root)
{
if (!root) return 0;
int L = depth(root->left);
int R = depth(root->right);
int temp = L + R + 1;
if (temp > ans) ans = temp;
return max(L, R) + 1;
}
int diameterOfBinaryTree(TreeNode* root)
{
depth(root);
return ans-1;
}
copy树
BiNode *copyTree(BiNode *T)
{
BiNode *newLptr = NULL, *newRptr = NULL;
BiNode *newNode = NULL;
if (T->lchild != NULL)
{
newLptr =copyTree(T->lchild);
}
else
{
newLptr = NULL;
}
if (T->rchild != NULL)
{
newRptr = copyTree(T->rchild);
}
else
{
newRptr = NULL;
}
newNode = (BiNode *)malloc(sizeof(BiNode));
if (newNode == NULL)
{
return NULL;
}
newNode->lchild = newLptr;
newNode->rchild = newRptr;
newNode->data = T->data;
return newNode;
}
中序遍历的非递归写法
步骤1:结点的所有路径情况
如果结点有左子树,该结点入栈;
如果结点没有左子树,访问该结点;
分析3:路径所有情况
如果结点有右子树,重复步骤1;
如果结点没有右子树(结点访问完毕),回退,让栈顶元素出栈,访问栈顶元素,并访问右子树,重复步骤1
如果栈为空,表示遍历结束。
注意:入栈的结点表示,本身没有被访问过,同时右子树也没有被访问过。
#include "iostream"
#include "stack"
using namespace std;
//二叉链表
typedef struct BiNode
{
int data;
struct BiNode *lchild, *rchild;
}BiNode, *BiTree;
//一直往左走,找到中序遍历的起点
BiNode *GoFarLeft(BiNode *T, stack<BiNode *> &s)
{
if (T == NULL)
{
return NULL;
}
while (T->lchild) //如果有左子树,就入栈。并指向该左子树
{
s.push(T);
T = T->lchild;
}
return T;
}
void InOrder2(BiNode *T)
{
stack<BiNode *> s;
//步骤1:一直往左走,找到中序遍历的起点
BiTree t = GoFarLeft(T, s);
while (t)
{
printf("%d ", t->data); //中序遍历打印
//如果t节点有右子树,那么重复步骤1
if (t->rchild != NULL)
{
t = GoFarLeft(t->rchild, s);
}
//如果t没有右子树,根据栈顶指示,访问栈顶元素
else if (!s.empty())
{
t = s.top();
s.pop();
}
//如果t没有右子树,并且栈为空
else
{
t = NULL;
}
}
}
void main()
{
BiNode t1, t2, t3, t4, t5;
memset(&t1, 0, sizeof(BiNode));
memset(&t2, 0, sizeof(BiNode));
memset(&t3, 0, sizeof(BiNode));
memset(&t4, 0, sizeof(BiNode));
memset(&t5, 0, sizeof(BiNode));
t1.data = 1;
t2.data = 2;
t3.data = 3;
t4.data = 4;
t5.data = 5;
t1.lchild = &t2;
t1.rchild = &t3;
t2.lchild = &t4;
t3.lchild = &t5;
InOrder2(&t1);
system("pause");
}
二叉搜索树
binary search tree,中文翻译为二叉搜索树、二叉查找树或者二叉排序树。简称为BST
性质:
1.左子树上所有结点的值均小于或等于它的根结点的值。
2.右子树上所有结点的值均大于或等于它的根结点的值。
3.左、右子树也分别为二叉排序树。
4.没有键值相等的节点
查找方法:从根节点开始,若当前节点的值大于查找值则向左下走,若当前节点的值小于查找值则向右下走。同时因为二叉查找树是有序的,对其中序遍历的结果即为排好序的数组 。
验证一颗树是否时二叉搜索树就是要看中序遍历的值是否是从小到大的
template <class T>
class BST
{
struct Node
{
T data;
Node* left;
Node* right;
};
Node* root;
Node* makeEmpty(Node* t)
{
if (t == NULL) return NULL;
makeEmpty(t->left);
makeEmpty(t->right);
delete t;
return NULL;
}
Node* insert(Node* t, T x)
{
if (t == NULL)
{
t = new Node;
t->data = x;
t->left = t->right = NULL;
}
else if (x < t->data)
{
t->left = insert(t->left, x);
}
else if (x > t->data)
{
t->right = insert(t->right, x);
}
return t;
}
Node* find(Node* t, T x)
{
if (t == NULL) return NULL;
if (x < t->data) return find(t->left, x);
if (x > t->data) return find(t->right, x);
return t;
}
Node* findMin(Node* t)
{
if (t == NULL || t->left == NULL) return t;
return findMin(t->left);
}
Node* findMax(Node* t)
{
if (t == NULL || t->right == NULL) return t;
return findMax(t->right);
}
Node* remove(Node* t, T x)
{
Node* temp;
if (t == NULL) return NULL;
else if (x < t->data) t->left = remove(t->left, x);
else if (x > t->data) t->right = remove(t->right, x);
else if (t->left && t->right) //X= t->data 如果待删除的节点有两个叶子节点的情况
{
temp = findMin(t->right);
t->data = temp->data;
t->right = remove(t->right, t->data);
}
else
{
temp = t;
if (t->left == NULL) t = t->right;
else if (t->right == NULL) t = t->left;
delete temp;
}
return t;
}
public:
BST(): root(NULL) {}
~BST()
{
root = makeEmpty(root);
}
void insert(T x)
{
insert(root, x);
}
void remove(T x)
{
remove(root, x);
}
};
AVL树(平衡二叉搜索树)
(1)非叶子节点最多拥有两个子节点;
(2)非叶子节值大于左边子节点、小于右边子节点;
(3)树的左右两边的层级数相差不会大于1;
(4)没有值相等重复的节点;
优点:平衡二叉树是基于二分法的策略提高数据的查找速度的二叉树的数据结构;使用平衡二叉树能保证数据的左右两边的节点层级相差不会大于1.,通过这样避免树形结构由于删除增加退化为线性链表影响查询效率,保证数据平衡的情况下查找数据的速度近于二分法查找;
局限性
由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多,更多的地方是用追求局部而不是非常严格整体平衡的红黑树.当然,如果应用场景中对插入删除不频繁,只是对查找要求较高,那么AVL还是较优于红黑树.
应用
C++中map、 set、 multimap, multiset的底层实现都是平衡⼆叉搜索树,所以map、 set的增删操作时间时间复杂度是logn,注意我这⾥没有说unordered_map、 unordered_set, unordered_map、
unordered_map底层实现是哈希表。
红黑树
红黑树和AVL树的区别在于它使用颜色来标识结点的高度,它所追求的是局部平衡而不是AVL树中的非常严格的平衡
它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
红黑树从根到叶子节点的最长路径不会超过最短路径的2倍
自平衡:红黑树在插入元素时会导致上述规则不满足,需要通过变色和旋转来解决
STL的map和set的内部实现
B树(平衡多路查找树)
一个节点中可以有多个关键字(值)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iQ9z3myR-1629084912965)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427141341516.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OMz0sNk9-1629084912966)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427141527643.png)]
性质:
1.B树中所有节点的孩子节点数中的最大值称为B树的阶,记为M(重点)
2.树中的每个节点至多有M棵子树 —即:如果定了M,则这个B树中任何节点的子节点数量都不能超过M
3.若根节点不是终端节点,则至少有两棵子树
3.除根节点和叶节点外,所有点至少有m/2棵子树(上溢)
4.所有的叶子结点都位于同一层。(比如上面的图片中我没有了11 13 15,那么我12就没有存在的意义了,就需要调整整个树的布局)。
B树的意义
B树的出现是由于硬盘IO的操作效率很低 ,当在大量数据存储中,查询时我们不能一下子将所有数据加载到内存中,只能逐一加载磁盘页,每个磁盘页对应树的节点。造成大量磁盘IO操作(最坏情况下为树的高度)。平衡二叉树由于树深度过大而造成磁盘IO读写过于频繁,进而导致效率低下。
所以,我们为了减少磁盘IO的次数,就你必须降低树的深度,将“瘦高”的树变得“矮胖”
B树大量应用在数据库和文件系统当中。
B+树
与B树的区别 ,分支是从关键字处引出的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IxJYwSDq-1629084912966)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427142559556.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GW41OwkI-1629084912967)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427145535356.png)]
只有叶子节点才会有data,其他都是索引。
mysql使用B+树作为索引:
b树和b+树的区别
B树每个节点都存储数据,所有节点组成这棵树。B+树只有叶子节点存储数据(B+数中有两个头指针:一个指向根节点,另一个指向关键字最小的叶节点),叶子节点包含了这棵树的所有数据,所有的叶子结点使用链表相连,便于区间查找和遍历,所有非叶节点起到索引作用。
B树中叶节点包含的关键字和其他节点包含的关键字是不重复的,B+树的索引项只包含对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
B树中每个节点(非根节点)关键字个数的范围为m/2(向上取整)-1,m-1,并且具有n个关键字的节点包含(n+1)棵子树。B+树中每个节点(非根节点)关键字个数的范围为m/2(向上取整),m,具有n个关键字的节点包含(n)棵子树。
B+树中查找,无论查找是否成功,每次都是一条从根节点到叶节点的路径。
b树的优点
1.B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。
b+树的优点
- 所有的叶子结点使用链表相连,便于区间查找和遍历。B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
- b+树的中间节点不保存数据,能容纳更多节点元素。
哈夫曼树(最优二叉树)
定义:
设二叉树具有n个带权值的叶节点,那么从根节点到各个叶子节点的路径长度与相应节点权值的乘积的和,叫做二叉树的带权路径长度。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1TWXPDjs-1629084912967)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427165544568.png)]
具有最小带权路径长度的二叉树称为哈夫曼树(也称最优树)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8agErvny-1629084912968)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427171616412.png)]
思想:
- 权值越大的叶节点越靠近根节点。
- 权值越小的叶节点越远离根节点。
- 目的是为了减少查找过程中比较的次数
哈夫曼树不一定唯一
构造哈夫曼树的方法成为哈夫曼算法
构造思想:贪心先选尽量权值小的作为叶子节点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b7PTWFY3-1629084912969)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427194244786.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CiU6Sedl-1629084912969)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427194737921.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-68XF9JZq-1629084912970)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427195144837.png)]
哈夫曼编码
出现次数较多的字母短一些
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U7zKpupT-1629084912970)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427201501989.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q07di5gj-1629084912971)(D:\find_job\数据结构与算法\数据结构与算法.assets\image-20210427202008818.png)]
二叉线索树
dfs
平分物品(网易2021校招笔试提前批)
题目:
现在有n个物品,每一个物品都有一个价值,现在想将这些物品分给两个人,要求这两个人每一个人分到的物品的价值总和相同(个数可以不同,总价值相同即可),剩下的物品就需要扔掉,现在想知道最少需要扔多少价值的物品才能满足要求分给两个人。
这道题我认为是选择问题 通过dfs每一种可能的选择,找到所有可能的解法
题意转换
将题意转换很重要,题目是求最少丢掉多少物品能够平分给两个人,转换为两个人从0开始拿,计算出所有满足平分条件的最值(最少丢弃)
具体步骤
- 首先将题目转换为两个人从0开始拿物品,对于每一件物品开始进行选择,对于每个物品有三种选择,给第一个人、给第二个人、丢掉。
2.参数说明:nums记录n个物品的价值 ;result1、result2记录两个人目前分别拿了多少;sum记录所有元素总和,index记录选择进行到哪个元素了,n记录总共有多少个需要选择的物品
3.选择结束条件:搜索到最后一个物品 判断两者是否相等 相等则记录此时的最值
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<bits/stdc++.h>
//定义两组数据的累加和 从0开始dfs每种可能,遇到两者相同的情况,就记录此时需要扔掉
//选择问题 两个人从0开始拿物品,遇到一个物品有三种选择,给第一个人,给第二个人,扔掉。
//走到结尾就找到舍弃价值最小的那一个节点
int res = INT_MAX;//最小扔掉的价值
void dfs(vector<int>& nums,int result1,int result2,int sum,int index,int n)
{
//一直选择到最后一个数字才返回
if (index == n)
{
if (result1 == result2)
{
res = min(res, sum - result1 - result2);
}
return;
}
//选择环节 每次进入选择环节都有三种选择
dfs(nums,result1 + nums[index], result2, sum, index + 1,n);
dfs(nums,result1,result2 + nums[index], sum, index + 1,n);
dfs(nums,result1,result2,sum,index+1,n);
}
int main()
{
int t;
cin >> t;
while (t--)//一个while输出一个答案
{
int n;
cin >> n;
int temp;
vector<int> nums;//输入数组
for (int i = 0; i < n; i++)
{
cin >> temp;
nums.push_back(temp);
}
int sum = 0;
for (auto i : nums)
{
sum += i;
}
dfs(nums, 0, 0, sum, 0, n);
cout << res << endl;
res = INT_MAX;
}
}
动态规划
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MFXxl5Nc-1629084912971)(数据结构与算法.assets/image-20210806165140990.png)]
整数拆分
#include<algorithm>
class Solution {
public:
//21:24
//dp[i]=max(i*j-i),i*dp[j-i]
int integerBreak(int n)
{
vector<int> dp(n+1);
dp[2]=1;
for(int i=3;i<=n;i++)
{
for(int j=1;j<i-1;j++)
{
dp[i]=max(dp[i],max((i-j)*j,dp[i-j]*j));
}
}
return dp[n];
}
};
01背包问题
01背包问题就是 :有N件物品和⼀个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。 每件物品只能⽤⼀次,求解将哪些物品装⼊背包⾥物品价值总和最⼤。
解决这个问题的核心思路就是 定义dp(i,j)为在0-i中选任意个,背包所能装下的最大重量
1.考虑最后一个i 是装还是不装,dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
2.考虑如何初始化
3.写出二维dp后砍掉第一维,注意内层循环要从后往前遍历
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NYHkQ86z-1629084912972)(数据结构与算法.assets/image-20210809150914705.png)]
分割等和子集
dp 的定义为 :从0-i中任选,是否能恰好等于j 如果能就为true 不能就为false
二维dp
//10:12
//dp[i][j]为从0-i中任选,是否能恰好等于j 如果能就为true 不能就为false
//dp[i][j]有两个来源,一个是不选i dp[i-1][j] ,一个是选i dp[i-1][j-nums[i]](且j-nums[i]>0)
class Solution {
public:
bool canPartition(vector<int>& nums)
{
int len=nums.size();
int sum=0;
for(int i=0;i<len;i++)
{
sum+=nums[i];
}
if(sum%2!=0) return false;//不能被2整除
int target=sum/2;
vector<vector<bool>> dp;
dp.resize(len,vector<bool>(target+1,false));
for(int k=0;k<target+1;k++)
{
if(nums[0]==k)
{
dp[0][k]=true;
break;
}
}
// cout<<"a"<<endl;
// cout<<"len="<<len<<" target= "<<target<<endl;
for(int i=1;i<len;i++)
{
for(int j=1;j<target+1;j++)
{
// cout<<"i= "<<i<<" j= "<<j<<endl;
if(j==nums[i])//直接选最后一个数就满足
{
dp[i][j]=true;
continue;
}
else if(j<nums[i])
{
dp[i][j]=dp[i-1][j];//最后一个数大于目标 不能取最后一个数
continue;
}
dp[i][j]=dp[i-1][j]||dp[i-1][j-nums[i]];//选i或不选i只要有一个为真就可以
}
}
return dp[len-1][target];
}
};
一维dp
//10:12
//dp[i][j]为从0-i中任选,是否能恰好等于j
//dp[i][j]有两个来源,一个是不选i dp[i-1][j] ,一个是选i dp[i-1][j-nums[i]](且j-nums[i]>0)
//优化 由于dp[i][j]只来源dp[i-1][j]和dp[i-1][j-nums[i] 故可以用滚动数组优化
class Solution {
public:
bool canPartition(vector<int>& nums)
{
int len=nums.size();
int sum=0;
for(int i=0;i<len;i++)
{
sum+=nums[i];
}
if(sum%2!=0) return false;//不能被2整除
int target=sum/2;
vector<bool> dp;
dp.resize(target+1,false);
for(int k=0;k<target+1;k++)
{
if(nums[0]==k)
{
dp[k]=true;
break;
}
}
// cout<<"a"<<endl;
// cout<<"len="<<len<<" target= "<<target<<endl;
for(int i=1;i<len;i++)
{
for(int j=target;j>nums[i];j--)
{
// // cout<<"i= "<<i<<" j= "<<j<<endl;
// if(j==nums[i])//直接选最后一个数就满足
// {
// dp[j]=true;
// continue;
// }
// else if(j<nums[i])
// {
// // dp[j]=dp[j];//最后一个数大于目标 不能取最后一个数
// continue;
// }
dp[j]=dp[j]||dp[j-nums[i]];//选i或不选i只要有一个为真就可以
}
}
return dp[target];
}
};
最后一块石头的重量2
二维dp
//`13:01
//转换为01背包问题
//找到两堆重量最近的两堆,相撞之后剩下的石头最小
//在0-i中选择任意个 使总和最接近 sum/2
//dp[i][j]=dp[i-1][j]+dp[i-1][j-stone[i]] 所能放的最大重量
#include<algorithm>
class Solution
{
public:
int lastStoneWeightII(vector<int>& stones)
{
int len=stones.size();
int sum=0;
for(int i=0;i<len;i++)
{
sum+=stones[i];
}
int target=sum/2;//取整
vector<vector<int>> dp(len,vector<int>(target+1,0));
for(int k=0;k<target+1;k++)
{
if(stones[0]<=k)
{
dp[0][k]=stones[0];
}
}
for(int i=1;i<len;i++)
{
for(int j=1;j<target+1;j++)
{
if(j<stones[i]) dp[i][j]=dp[i-1][j];
else
{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-stones[i]]+stones[i]);
}
}
}
return abs((sum-dp[len-1][target])-dp[len-1][target]);
}
};
一维dp
//`13:01
//转换为01背包问题
//找到两堆重量最近的两堆,相撞之后剩下的石头最小
//dp[i][j]定义 在0-i中选择任意个 使总和最接近 j 的最大值
//dp[i][j]=dp[i-1][j]+dp[i-1][j-stone[i]]
#include<algorithm>
class Solution
{
public:
int lastStoneWeightII(vector<int>& stones)
{
int len=stones.size();
int sum=0;
for(int i=0;i<len;i++)
{
sum+=stones[i];
}
int target=sum/2;//取整
vector<int> dp(target+1,0);
for(int k=0;k<target+1;k++)
{
if(stones[0]<=k)
{
dp[k]=stones[0];
}
}
for(int i=1;i<len;i++)
{
for(int j=target;j>=stones[i];j--)
{
dp[j]=max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return abs((sum-dp[target])-dp[target]);
}
};
不同的二叉搜索树
这道题主要是利用二叉搜索树的性质得到以i为头节点的二叉搜索树,左边有多少个节点,右边有多少个节点。
class Solution {
public:
//1.求n个节点组成的二叉搜索树=由1为头节点的个数+2为头节点+...n为头节点
//2.以i为头节点的n个节点的二叉搜索树个数,由于左子树有0-i-1,右子树有i+1-n
int numTrees(int n)
{
//dp[i]为i个节点的二叉搜索树个数
vector<int>dp(n+1,0);
dp[0]=1;
dp[1]=1;
//计算i个节点构成的二叉搜索树个数
for(int i=2;i<=n;i++)
{
//计算头节点为j的累加和
for(int j=1;j<=i;j++)
{
dp[i]+=dp[j-1]*dp[i-j];
}
}
return dp[n];
}
};
凑零钱
这个题的核心是理解这样一件事情
假设f(s)为组成金额s最少的硬币数,最后一枚硬币的值为c,那么f(s)=f(s-c)+1;
由于不知道最后一枚硬币是哪一个 所以要枚举每一种情况 并选择最小的
那么也是一个多叉树的模型,两种办法解决
自顶而下的记忆化递归(递归函数返回的是什么、递归结束条件、如何记忆化、如何遍历每一种情况找到最小的)
自下而上的迭代(迭代好像简单一些耶,但核心思想一样)
两种复杂度差不多的
记忆化递归
哈希表占用内存较大 直接用数组记录会更快
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<unordered_map>
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
//备忘录 键为需要凑的钱数 值为最少需要的个数
// unordered_map<int, int> hash_map;
//数组下标为需要凑的钱数 值为最少需要的个数
vector<int> memo(amount+1, -2);
return fun(coins, amount, memo);
}
//返回凑钱数为amount时 最少需要的个数
int fun(vector<int>& coins, int amount, vector<int>& memo)
{
if (amount == 0) return 0;
if (amount < 0) return -1;
//以前计算过
if (memo[amount] != -2) return memo[amount];
//个数
int res = INT_MAX;
for (int i = 0; i < coins.size(); i++)
{
int a = fun(coins, amount - coins[i], memo);
if (a == -1) continue;
//在for里找到最小的dp[amount]
res = min(res, a + 1);
}
//没找到
if (res == INT_MAX) res = -1;
//记录计算过的值
memo[amount] = res;
return res;
}
};
迭代写法
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
#include<unordered_map>
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
//自下往上
//定义dp[i]为 总金额为i 时 dp[i]为最少硬币的个数
vector<int>dp(amount + 1, INT_MAX-2);
dp[0] = 0;
//遍历计算每个dp[i]
for (int i = 0; i <= amount; i++)
{
//计算每一个dp[i]的最小值
for (int j = 0; j < coins.size(); j++)
{
if (i - coins[j] < 0) continue;
dp[i] = min(dp[i], dp[i - coins[j]]+1);
}
}
if (dp[amount] == INT_MAX-2) return -1;
return dp[amount];
}
};
最大子序合
注意扩展要求出子区间的下标
class Solution {
public:
//动态规划 dp[i]为以下标i结尾的最大和
//递推公式:dp[i]=max(dp[i-1]+nums[i],nums[i])
//kuozh
int maxSubArray(vector<int>& nums)
{
// vector<int> dp;
int dp;
// dp.resize(nums.size());
// dp[0]=nums[0];
dp=nums[0];
int res=nums[0];
int start=0;
int end=0;
//最大连续和的区间
int start_max=0;
int end_max=0;
for(int i=1;i<nums.size();i++)
{
// dp=max(dp+nums[i],nums[i]);
//以前的是正增益
if(dp>0)
{
dp=dp+nums[i];
end++;
}
//以前的是负增益
else
{
start=i;
end=i;
dp=nums[i];
}
if(dp>res)
{
res=dp;
start_max=start;
end_max=end;
}
}
cout<<"start_max= "<<start_max<<" end_max= "<<end_max<<endl;
return res;
}
};
回溯算法
实质是递归 纯暴力搜索
组合问题: N个数⾥⾯按⼀定规则找出k个数的集合
切割问题:⼀个字符串按⼀定规则有⼏种切割⽅式
⼦集问题:⼀个N个数的集合⾥有多少符合条件的⼦集
排列问题: N个数按⼀定规则全排列,有⼏种排列⽅式
棋盘问题: N皇后,解数独等等
回溯中常用得技巧:
startindex
回溯模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
组合
题目:给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
这个题也是套常规的回溯模板
但要加一个剪枝 就是如果剩下的元素已经不足还需要的元素就要没必要继续下去了
#include<iostream>
using namespace std;
#include<vector>
class Solution
{
public:
vector<vector<int>> res;
vector<vector<int>> combine(int n, int k)
{
vector<int> temp;
backstacking(temp,n,k,1);
return res;
}
void backstacking(vector<int>& temp,int n,int k,int StartIndex)
{
if (temp.size() == k)
{
res.push_back(temp);
return;
}
//剪枝优化 如果剩下的元素已经不足还需要的元素就要没必要继续下去了
//已经选择的元素个数 temp.size()
//还需要 k-temp.size()
//集合至少要从n-(k-temp.size())+1
for (int i = StartIndex; i <= n - (k - temp.size()) + 1; i++)
{
temp.push_back(i);
backstacking(temp, n, k, i + 1);
temp.pop_back();
}
}
};c++
组合问题3
题目:找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
组合问题在在做单层选择时要使用start_index,避免重复组合
剪枝:元素数量总和剪枝 for循环的范围剪枝
#include<iostream>
using namespace std;
#include<vector>
#include <unordered_set>
class Solution {
public:
vector<vector<int>> res;
vector<vector<int>> combinationSum3(int k, int n)
{
//记录所有的路径
vector<int> temp;
// unordered_set<int> hash_set;
dfs( temp, k, n,0,1);
return res;
}
//i 已经用掉了几个数
//sum 当前的总和
//temp 存放当前的路径
void dfs(vector<int>& temp,int k,int n,int sum,int start_index)
{
if (temp.size() == k && sum == n)
{
res.push_back(temp);
return;
}
if (temp.size() > k || sum > n) return;
for (int j = start_index; j <= 9; j++)
{
temp.push_back(j);
sum += j;
// cout<<j<<" "<<pos<<endl;
dfs(temp, k, n, sum ,j+1);
sum -= j;
temp.pop_back();
}
}
};
电话号码得字母组合
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0IOb0WRt-1629084912973)(数据结构与算法.assets/image-20210715142503921.png)]
前面两个组合都同一集合之间的组合,下面来看不同集合之间的组合
首先要搞清楚如何建立一个映射,将数字映射到字母组合
注意控制决策层数和树的单层遍历
#include<iostream>
using namespace std;
#include<vector>
#include<string>
class Solution {
public:
//建立一个映射 将数字映射到字母组合
string convert_map[10]
{
"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
};
vector<string> res;
string path;
vector<string> letterCombinations(string digits)
{
if (digits.size() == 0) return {};
backstacking(digits, 0);
return res;
}
//index控制决策树的高度
void backstacking(string digits,int index)
{
if (index == digits.size())
{
res.push_back(path);
return;
}
int nums = digits[index] - '0';//将index对应得字母转换为数字
//单层树 遍历该数字对应得字母
for (int i = 0; i < convert_map[nums].size(); i++)
{
path.push_back(convert_map[nums][i]);
backstacking(digits, index + 1);
path.pop_back();
}
}
};
组合总和
给定一个无重复元素的数组 candidates
和一个目标数 target
,找出 candidates
中所有可以使数字和为 target
的组合。
candidates
中的数字可以无限制重复被选取。
说明:
- 所有数字(包括
target
)都是正整数。 - 解集不能包含重复的组合。
这个题要注意的点是:
1.不能重复 要用startindex来控制
2.for循环遍历的范围存在一个剪枝的优化点
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum(vector<int>& candidates, int target)
{
sort(candidates.begin(), candidates.end());
backstacking(candidates, target, 0,0);
return res;
}
void backstacking(vector<int>& candidates, int target, int sum,int startindex)
{
if (sum == target)
{
res.push_back(path);
return;
}
if (sum > target)
{
return;
}
//如果sum+接下里循环的第一个数就超过target 就没有必要进入下层遍历
for (int i = startindex; i < candidates.size()&&sum+candidates[i]<=target; i++)
{
path.push_back(candidates[i]);
sum += candidates[i];
backstacking(candidates, target, sum, i);
sum -= candidates[i];
path.pop_back();
}
}
};
组合总和2
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
注意:解集不能包含重复的组合。
要注意是同一个树层上去重还是同一个树枝上去重
主要for的循环范围可以优化 只要有排序这里就可以优化
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
class Solution
{
public:
//遇到相同元素在树层上剪枝
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
{
sort(candidates.begin(), candidates.end());
vector<bool> used(candidates.size(),false);
backstacking(candidates, target, 0, 0,used);
return res;
}
void backstacking(vector<int>& candidates, int target,int sum,int startindex,vector<bool>& used)
{
if (sum == target)
{
res.push_back(path);
return;
}
if (sum > target)
{
return;
}
//
for (int i = startindex; i < candidates.size()&&sum+candidates[i]<=target; i++)
{
if (i > 0 && candidates[i] == candidates[i - 1]&&used[i-1]==false) continue;
used[i] = true;
path.push_back(candidates[i]);
sum += candidates[i];
if (sum > target) return;
backstacking(candidates, target, sum, i + 1, used);
used[i] = false;
path.pop_back();
sum-= candidates[i];
}
}
};
分割回文串
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQ7Ws31O-1629084912974)(数据结构与算法.assets/image-20210717150710244.png)]
这个字符串处理挺麻烦的
主要是这句话
string temp = s.substr(startindex,i-startindex+1);
#include<iostream>
using namespace std;
#include<vector>
#include<string>
class Solution
{
public:
vector<vector<string>> res;
//每一层选择的节点都必须是回文串
vector<vector<string>> partition(string s)
{
vector<string> path;
backstacking(s, path, 0);
return res;
}
//在每一个树层遍历所有的可能情况 把不是回文的删除
//终止条件是切割完
//检查一个字符是否是回文串
//是回文串返回ture 不是返回false
bool check(string s)
{
if (s.size() == 1)
{
return true;
}
int flag = true;
//定义两个指针一个指向头一个指向尾
int head = 0;
int end = s.size() - 1;
while (head < end)
{
if (s[head] != s[end])
{
flag = false;
}
head++;
end--;
}
return flag;
}
//num 从哪个字母开始截取
void backstacking(string s, vector<string> path, int startindex)
{
if (startindex == s.size())
{
res.push_back(path);
return;
}
//从num开始 i个字符
for (int i = startindex; i < s.size(); i++)
{
//难点在于如何切分
//同一树层应该是从同一个点开始逐渐增加切割的大小
//同一树枝应该是startindex要往前进一位,切割大小逻辑不变
//i-startindex+1保证了切割数量同一树层是逐个增加的
string temp = s.substr(startindex,i-startindex+1);
if (!check(temp)) continue;
path.push_back(temp);
backstacking(s, path, i + 1);
path.pop_back();
}
}
};
复原ip地址
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KnNoHjIU-1629084912974)(数据结构与算法.assets/image-20210717150642807.png)]
#include<iostream>
using namespace std;
#include<vector>
#include<string>
#include<cstring>
class Solution
{
public:
vector<string> res;
string path;
//一个字符串分割成四个整数 最多三位最少一位
//剪枝条件 整数大于255 有前导0
vector<string> restoreIpAddresses(string s)
{
backstacking(s, 0, 0);
return res;
}
//传入一个字符串 有效返回true 无效返回false
bool is_vaild(string temp)
{
if (temp.size() > 3 || temp.size() == 0) return false;
int num = stoi(temp);
if (num > 255) return false;
if (temp[0] == '0' && temp.size() != 1) return false;
return true;
}
void backstacking(string s,int startindex,int pointnums)
{
//当有三个逗点时 只需要判断剩下字符串是否满足条件
if (pointnums == 3)
{
string temp = s.substr(startindex, s.size() - startindex);
// cout<<temp<<endl;
if (is_vaild(temp))
{
res.push_back(path+temp);
}
return;
}
for (int i = startindex; i < s.size(); i++)
{
string temp = s.substr(startindex, i - startindex + 1);
if (!is_vaild(temp)) continue;
//有效
path += temp;
pointnums++;
path.push_back('.');
backstacking(s, i + 1, pointnums);
pointnums--;
path.erase(path.size()-(i - startindex + 2), i - startindex + 2);
// path.erase(startindex)
}
}
};
子集
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KuOatDC2-1629084912975)(数据结构与算法.assets/image-20210718202547652.png)]
这个题实际上是收集所有的叶子节点就可以了,自己琢磨的时候想复杂了
收集所有的叶子节点
class Solution
{
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex)
{
result.push_back(path); // 收集⼦集,要放在终⽌添加的上⾯,否则会漏掉⾃⼰
if (startIndex >= nums.size())
{ // 终⽌条件可以不加
return;
}
for (int i = startIndex; i < nums.size(); i++)
{
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums)
{
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};
在结尾加了一个最大值,自己h瞎写的,也可以过
#include<iostream>
using namespace std;
#include<vector>
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<vector<int>> subsets(vector<int>& nums)
{
nums.push_back(INT_MIN);
backstacking(nums, 0, nums.size());
return res;
}
//-1
void backstacking(vector<int>& nums, int startindex, int size)
{
if (startindex == size)
{
// cout<<"--"<<endl;
res.push_back(path);
return;
}
for (int i = startindex; i < size; i++)
{
if (nums[i] != INT_MIN)
{
// cout<<nums[i]<<endl;
path.push_back(nums[i]);
}
backstacking(nums, i + 1, size);
if (nums[i] != INT_MIN)
{
path.pop_back();
}
}
}
};
递增子序列
感觉很难得一道
要注意树层上去掉重复元素得方法
#include<iostream>
using namespace std;
#include<vector>
class Solution
{
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& nums, int startIndex)
{
if (path.size() > 1)
{
result.push_back(path);
}
int used[201] = { 0 }; // 这⾥使⽤数组来进⾏去重操作,题⽬说数值范围[-100, 100]
for (int i = startIndex; i < nums.size(); i++)
{
if ((!path.empty() && nums[i] < path.back())
|| used[nums[i] + 100] == 1)
{
continue;
}
used[nums[i] + 100] = 1; // 记录这个元素在本层⽤过了,本层后⾯不能再⽤了
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
public:
vector<vector<int>> findSubsequences(vector<int>& nums)
{
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
};
全排列
题目c++
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
排列问题就不用start_index,要使用used数组记录使用过的元素
#include<iostream>
using namespace std;
#include<vector>
class Solution {
public:
//排列问题是有顺序的
//相当于一颗多叉树,每个节点都可以选择没用过的元素
vector<vector<int>> res;
vector<vector<int>> permute(vector<int>& nums)
{
vector<bool> used(nums.size(), false);
vector<int> temp;
BackTracking(nums, temp, used);
return res;
}
void BackTracking(vector<int>& nums, vector<int>& temp,vector<bool>& used)
{
//终止条件
if (temp.size() == nums.size())
{
res.push_back(temp);
return;
}
for (int i = 0; i < nums.size(); i++)
{
//第i个数已经被用过了
if (used[i] == true) return;
used[i] = true;
temp.push_back(nums[i]);
BackTracking(nums, temp, used);
used[i] = false;
temp.pop_back();
}
}
};
全排列2
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
这道题的关键在于去重
画决策树可以知道 如果同一树层两个节点相同,那么他这两个节点对应剩下的树枝都是相同的,所以应该在树层剪枝
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
class Solution {
public:
//排列问题是有顺序的
//相当于一颗多叉树,每个节点都可以选择没用过的元素
vector<vector<int>> res;
vector<vector<int>> permuteUnique(vector<int>& nums)
{
vector<bool> used(nums.size(), false);
vector<int> temp;
sort(nums.begin(), nums.end());
BackTracking(nums, temp, used);
return res;
}
void BackTracking(vector<int>& nums, vector<int>& temp,vector<bool>& used)
{
//终止条件
if (temp.size() == nums.size())
{
res.push_back(temp);
return;
}
for (int i = 0; i < nums.size(); i++)
{
//由于排过序了 如果当前这个数跟前一个数一样 就在同一树层把这个分支减掉
if (i > 0 && nums[i - 1] == nums[i]&& used[i-1]==false ) continue;
//第i个数已经被用过了
if (used[i] == true) continue;
used[i] = true;
temp.push_back(nums[i]);
BackTracking(nums, temp, used);
used[i] = false;
temp.pop_back();
}
}
};
排序算法
算法检验函数
void Random(int* a, int n, int l, int r)//生成范围在l~r的随机数
{
srand(time(0)); //设置时间种子 取系统时间
for (int i = 0; i < n; i++) {
a[i] = rand() % (r - l + 1) + l;//生成区间r~l的随机数
}
}
void check()
{
int n = 1000;
int* arr1= new int[n];
Random(arr1, n, 0, 1000);
vector<int> check_num(arr1, arr1 + n);
sort(check_num.begin(), check_num.end());
;
quick_sort(arr1, 0, n - 1);//检验快排算法
bool same = true;
for (int i = 0; i < n; i++)
{
if (arr1[i] != check_num[i]) same = false;
}
same == true ? cout << "right" << endl : cout << "false" << endl;
}
冒泡排序
冒泡排序:比较相邻的元素,如果前一个比后一个大就交换,遍历每一对,最后一个元素会是最大的。重复上述步骤直到没有元素需要比较。(两个for遍历,中间是交换代码)
//****************************************
//冒泡排序
/*
1.原理:比较两个相邻的元素,将值大的元素交换到右边
2.思路:依次比较相邻的两个数,将比较小的数放在前面,比较大的数放在后面。
(1)第一次比较:首先比较第一和第二个数,将小数放在前面,将大数放在后面。
(2)比较第2和第3个数,将小数 放在前面,大数放在后面。
......
(3)如此继续,知道比较到最后的两个数,将小数放在前面,大数放在后面,重复步骤,直至全部排序完成
(4)在上面一趟比较完成后,最后一个数一定是数组中最大的一个数,所以在比较第二趟的时候,最后一个数是不参加比较的。
(5)在第二趟比较完成后,倒数第二个数也一定是数组中倒数第二大数,所以在第三趟的比较中,最后两个数是不参与比较的。
(6)依次类推,每一趟比较次数减少依次
内层遍历完一趟后,最后一个数就是最大的了,所以下一次只需要遍历到n-i+1,外层循环相当于就是记个数
*/
//****************************************
void bubble_sort(vector<int> &nums,int n)
{
bool swaped;//如果发现没有需要交换的 提前结束
for (int i = 0; i <n ; i++)
{
swaped = false;
for (int j = 1; j < n - i ; j++)
{
if (nums[j] < nums[j - 1])
{
swap(nums[j], nums[j - 1]);
swaped = true;
}
}
}
}
int main()
{
vector<int> nums{ 6,1,2,7,9,3,4,5,10,8,2 };
//insertion_sort(nums, nums.size());
bubble_sort(nums, nums.size());
printf_array(nums);
return 0;
}
选择排序
先找到最小的,放到数组的开头(与第一个元素交换),然后在剩余的元素中选择出最小的放到第二个元素的位置,以此类推。
//选择排序
//*****************************************
/*
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。
*/
//*****************************************
void selection_sort(vector<int>& nums)
{
int n = nums.size();
for (int i = 0; i < n; i++)
{
int j = i;
int min= nums[j];
int min_index = 0;
int flag=false;
for (; j < n; j++)
{
if (nums[j] < min)
{
min = nums[j];
min_index = j;
flag = true;
}
}
if(flag)
{
swap(nums[min_index], nums[i]);
}
}
}
int main()
{
vector<int> nums{ 6,1,2,7,9,3,4,5,10,8,2 };
selection_sort(nums);
//insertion_sort(nums, nums.size());
//bubble_sort(nums, nums.size());
printf_array(nums);
return 0;
}
插入排序
在数组头部维护一个排序好的序列,将后面的元素逐个跟已排序序列比较并插入进去,第一个元素认为是有序的,然后从第二个元素开始通过比较插入有序序列中。
改进:在查找插入的位置的时候,不要按顺序循环查找改用二分查找
/插入排序
//***********************************
//一个指针遍历未排序的序列,待比较数字过来后,从后往前遍历,若比较数字小于该数字,则交换这两个数字,一直到大于或到数组头
//外层循环遍历未排序的序列并插入
int insertion_sort(vector<int>& nums, int n)
{
for (int i = 0; i < n; i++)
{
for (int j = i ; j > 0 && nums[j-1]>nums[j]; j--)
{
swap(nums[j], nums[j - 1]);
}
}
return 0;
}
//****************************************
int main()
{
vector<int> nums{ 6,1,2,7,9,3,4,5,10,8,2 };
insertion_sort(nums, nums.size());
printf_array(nums);
return 0;
}
希尔排序
先分组,然后每个组内自己排序,然后再改变分组方式,再组内排序。
4.希尔排序
//希尔排序
//插排的升级 数组后面比较小的元素会移动更少的次数到前面
//将数组按照gap间隔分成若干组,对每组分别进行插入排序,然后缩小间隔再进行排序,直到间隔为1,最后进行一次插入排序
void shell_sort(vector<int> & nums,int n)
{
for (int gap = nums.size(); gap > 0; gap /= 2)
{
for (int i = gap; i < n; i++)//是几组同步进行
{
for (int j = i; j >gap-1 && nums[j] < nums[j - gap]; j -= gap) //新元素与有序序列的元素比较
{
swap(nums[j], nums[j - gap]);
}
}
}
}
int main()
{
vector<int> nums{ 6,1,2,7,9,3,4,5,10,8,2 };
//selection_sort(nums);
//insertion_sort(nums, nums.size());
shell_sort(nums, nums.size());
//bubble_sort(nums, nums.size());
printf_array(nums);
return 0;
}
归并排序
通过递归的方式将大的数组一直分割,直到数组的大小为 1,此时只有一个元素,那么该数组就是有序的了,之后再把两个数组大小为1的合并成一个大小为2的,再把两个大小为2的合并成4的 … 直到全部小的数组合并起来。
整体的思想是分治,即先使每个子序列有序,再使子序列段间有序
将数组元素拆分成个体,然后两两合并成一组,以此类推
递归注意结束条件、递归公式
先递归,再合并
1.完成数组拆分,直到拆成单个元素
2.完成两个分别有序子序列合并得功能函数
int printf_array(int nums[],int n)
{
for (int i = 0; i < n; i++)
{
cout << nums[i] << endl;
}
return 0;
}
//合并函数
//1.先解决子问题 两个有序数组如何合并成一个新的有序数组
//2.再修改边界条件 这里将left mid right_bound 均为数组下标 左闭右闭写法
//3.再写拆得过程 也就是递归得过程
void merge(int nums[], int left,int mid,int right_bound)
{
int i = left;
int j = mid+1;
int* temp = new int[right_bound-left+1];
int k = 0;
while (i <= mid && j <= right_bound)
{
temp[k++] = nums[i] >= nums[j] ? nums[j++] : nums[i++];
}
while (i <= mid) temp[k++] = nums[i++];
while (j <= right_bound) temp[k++] = nums[j++];
for (int i = 0; i < right_bound - left + 1; i++)
{
nums[left + i] = temp[i];
}
//printf_array(temp, right_bound - left + 1);
}
//拆分
//这里要注意边界条件一定要统一,这里全部采用左闭右闭得区间
void merge_sort(int nums[], int left, int right)
{
//递归出口
if (left < right)//边界条件
{
//进行二分 缩小问题规模
int mid = (left + right) / 2;
merge_sort(nums,left,mid);
merge_sort(nums,mid+1,right);
merge(nums, left, mid, right);
}
/*int num[] = { 1,3,4,2,5,6 };
merge(num, 1,2,4);*/
}
int main()
{
vector<int> nums{ 6,1,2,7,9,3,4,5,10,8,2 };
int nums2[] = { 6,1,2,7,9,3,4,5,10,8,2 };
int n = sizeof(nums2) / sizeof(nums2[0]);
//selection_sort(nums);
//insertion_sort(nums, nums.size());
//shell_sort(nums, nums.size());
//shell_sort2(nums, nums.size());
//bubble_sort(nums, nums.size());
merge_sort(nums2, 0,n-1);
printf_array(nums2,n);
//cout << nums.size() << endl;
//system("pause");
return 0;
}
快速排序
1)从序列中挑出一个元素,作为”基准”(pivot).
2)把所有比基准值小的元素放在基准前面,所有比基准值大的元素放在基准的后面(相同的数可以到任一边),这个称为分区(partition)操作。
3)对每个分区递归地进行步骤1~2,递归的结束条件是序列的大小是0或1,这时整体已经被排好序了。
核心思想:
参考链接
https://developer.51cto.com/art/201403/430986.htm
/*1.先从数列中取出一个数作为基准数。
1.1 直接取第一个元素
2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
2.1 两个指针i,j分别指向头尾,相向而行。先移动j,直到j找到一个小于等于基准数的,i找到一个大于基准数的,交换他们。
2.2 继续2.2 直到i=j ,交换这个位置和首元素
3.再对左右区间重复第二步,直到各区间只有一个数
3.1 递归
//手写快排
int quick_sort_partition(int nums[],int left,int right)
{
int axis = left;
int i = left+1;
int j = right;
int n = right-left + 1;
//printf_array(nums, n);
while (i <=j)//比轴小得都放到轴得左边,比轴大的都放在右边
{
while (i<=j && nums[i] <= nums[axis]) i++;
while (i<=j && nums[j] > nums[axis]) j--;
if (i < j)
{
swap(nums[i], nums[j]);
}
//printf_array(nums, n);
//cout << "----" << endl;
}
swap(nums[i-1], nums[left]);
//printf_array(nums,n);
return i - 1;
}
void quick_sort(int nums[], int left, int right)
{
if (left >= right) return;
int mid = quick_sort_partition(nums, left, right);
quick_sort(nums, left, mid - 1);
quick_sort(nums, mid + 1, right);
}
int main()
{
vector<int> nums{ 6,1,2,7,9,3,4,5,10,8,2 };
int n = sizeof(nums2) / sizeof(nums2[0]);
quick_sort(nums2, 0, n-1);
return 0;
}
快速排序模板题 记忆 删了重写
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s2mIsAtO-1629084912976)(数据结构与算法.assets/image-20210623112653317.png)]
还是这个模板好记忆一点
分析:
1.选一个哨兵 这里选x = nums[left + right >> 1];这个哨兵最好不要随意改变,要么容易出现边界问题要么容易超时.
2.快排的核心是分治 即将一个序列左部分小于等于x 右部分大于等于x
3.设置两个指针left right 分别从两端开始 left遇到大于等于x的停下里 right遇到小于等于x的停下来 然后如果i<j 就交换nums[i] nums[j]
void quick_sort(int nums[], int left, int right)
{
if (left >= right)
return;
//基准数一定要取出来
int i = left - 1, j = right + 1,x = nums[left + right >> 1];
while (i < j)
{
do i++; while (nums[i] < x);//找到一个>=x的数
do j--; while (nums[j] > x);//找到一个<=x的数
if (i < j)swap(nums[i], nums[j]);
}
quick_sort(nums, left, j), quick_sort(nums, j + 1, right);
}
左神快速排序
1.荷兰国旗问题
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9qRk0Oei-1629084912976)(数据结构与算法.assets/image-20210622211041896.png)]
问题一:设置一个小于等于num的区域,遍历数组
1.arr[i]<=num, arr[i]和<=区域的下一个数交换,<=区域右扩,i++;
2.arr[i]>num,i++
/*问题一:设置一个小于等于num的区域,遍历数组
1.arr[i] <= num, arr[i]和 <= 区域的下一个数交换, <= 区域右扩,i++;
2.arr[i] > num, i++
定义一个数组索引变量left <=left的所有数都是<=num的
相当于小于等于区域一直往后压 直到只剩下>的数字
结束条件 i=num.size()
*/
/*
问题二:设置一个小于num的区域 一个大于num的区域 一个等于num的区域
1.arr[i] < num, arr[i]和 < 区域的下一个数交换, < 区域右扩,i++;
2.arr[i]==num,i++
3.arr[i] > num arr[i]和 >区域的前一个数交换 >区域左扩
(这里i不++,因为交换过来的数我还没有判断)
定义一个<区域索引变量left 一个>区域索引变量right
< left的所有数都是< num的 >right区域的所有数都是> num的 中间都是等于num的
两边往中间压 中间剩余的就是=的
结束条件 i=right
*/
//问题二 代码
void sortColors(vector<int>& nums)
{
int left = -1;//<区域索引变量left
int right = nums.size();//一个>区域索引变量right
int i = 0;//遍历用
while (i < right )
{
//给定的数是1
//1.arr[i] < num, arr[i]和 < 区域的下一个数交换, < 区域右扩,i++;
if (nums[i] < 1)
{
swap(nums[i], nums[left + 1]);
left++;
i++;
}
//2.arr[i] == num, i++
else if (nums[i] == 1)
{
i++;
}
//3.arr[i] > num arr[i]和 > 区域的前一个数交换 > 区域左扩
else
{
swap(nums[i], nums[ right- 1]);
right--;
}
}
}
2.快速排序 1.0 2.0 3.0
//left_p right_p 为数组下标
int* partitions(int* nums, int left_p, int right_p)
{
int left = left_p-1;//<区域索引变量left
int right = right_p;//一个>区域索引变量right
int i = left_p;//遍历用
while (i < right)
{
//给定的数是右边界
//1.arr[i] < num, arr[i]和 < 区域的下一个数交换, < 区域右扩,i++;
if (nums[i] < nums[right_p])
{
//left先加后换
swap(nums[i], nums[left+1]);
left++;
i++;
}
//2.arr[i] == num, i++
else if (nums[i] == nums[right_p])
{
i++;
}
//3.arr[i] > num arr[i]和 > 区域的前一个数交换 > 区域左扩
else
{
//i跟右边界的前一个做交换
swap(nums[i], nums[right-1]);
right--;
}
}
swap(nums[right_p], nums[right]);
int* p = new int[2];
p[0] = left+1;
p[1] = right;
return p;
}
//经典快速排序
//在荷兰国旗问题上改进 把上述代码改成分区代码
//1.0 用最后一个数作为num,把小于等于的放左面,大于放右面,递归排序左面和右面。
//2.0 用最后一个数作为num,把小于放左面,等于放中间,大于放右面,递归排序左面和右面。
//left right为数组下标
void quick_sort(int* nums, int left, int right)
{
if (left >= right) return;
srand(time(0)); //设置时间种子 取系统时间
//rand()产生一个0-32767的随机数 所以randoms为left-right之间的随机数
int randoms = rand() % (right - left + 1) + left;//在left和right中取一个随机数
swap(nums[randoms], nums[right]);//把这个随机数交换到数组末尾
int* p;
p = partitions(nums, left, right);
quick_sort(nums, left, p[0] - 1);
quick_sort(nums, p[1] + 1, right);
}
堆排序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ZQwcAMz-1629084912977)(数据结构与算法.assets/image-20210726211323661.png)]
堆的特点: 完全二叉树 父节点值大于子节点
完全二叉树的重要性质: 将完全二叉树的节点按照层序遍历的顺序依次输入到一个数组中,那么任意一个子节点和父节点在数组中的索引有如下数量关系
parent=(i-1)/2
c1=2i+1//左孩子
c2=2i+2//右孩子
heapfiy操作
//对于某一个节点 使得根节点、左孩子、右孩子构成一个大顶堆 根大于左右
//同时对该节点底下的节点继续做heapfiy 直到叶子节点
//只有被交换的那个分支被破坏了,所以只需要递归那个分支
/数组的第i个节点执行heapify操作 取闭区间
void heapify(vector<int>& tree, int n, int i)
{
//左右孩子索引
int left = 2*i + 1;
int right = 2*i + 2;
//寻找根左右的最大值 相等不用换
//如果left right 超出数组范围也就是空了就排除
int max = i;
if (left<=n&&tree[left] > tree[max]) max = left;
if (right<=n&&tree[right] > tree[max]) max = right;
//如果根不是最大的,需要交换
if (max != i)
{
swap(tree[i], tree[max]);
//递归到交换过的节点继续进行heapify
heapify(tree, n, max);
}
}
heapify_build操作
根据输入的数组建立一个大根堆 从倒数第二层的第一个节点开始一直往前走
//根据输入的数组建立一个大根堆 从倒数第二层的第一个节点开始一直往前走 闭区间
void heapify_build(vector<int>& tree, int n)
{
//最后一个节点的父节点就是倒数第二层的第一个节点
int start = (n - 1) / 2;
for (int i = start; i >= 0; i--)
{
heapify(tree, n,i);
}
}
heapify_sort操作
建立大根堆后,每次交换最后一个节点和根节点(最大值) 也就是把当前最大值放到数组末尾
//对交换后的二叉树再进行heapify 使他再次成为一个大顶堆
//闭区间
void heapify_sort(vector<int>& tree, int n)
{
//先根据输入的数组建立一个大根堆
heapify_build(tree, n);
for (int i = 0; i < tree.size(); i++)
{
cout << tree[i] << endl;
}
//把根节点和最后一个节点交换 传入数组长度-1
//每次循环在数组末尾确定一个最大值
for (int i = 0; i <= n; i++)
{
swap(tree[0], tree[n-i]);
//交换后破坏了大根堆的规则
heapify(tree, n - i-1, 0);
}
}
TOP -k问题
堆的解决方法虽然速度不一定有快排快,但对于海量数据,本地不需要存储那么多数据
手写堆
class Solution {
public:
vector<int> smallestK(vector<int>& arr, int k)
{
if(k==0) return{};
//用数组的前k个数建立大根堆
heapify_build(arr,k-1);
//大根堆中维护当前最大的k个数
vector<int> res(arr.begin(),arr.begin()+k);
for(int i=0;i<res.size();i++)
{
cout<<res[i]<<endl;
}
for(int i=k;i<arr.size();i++)
{
// cout<<"--"<<endl;
// cout<<arr[i]<<" "<<res[0]<<endl;
//找一个比堆头小的数
if(arr[i]<res[0])
{
// cout<<"-"<<endl;
res[0]=arr[i];
heapify(res,k-1,0);
}
}
return res;
}
void heapify(vector<int>& nums,int n,int i)
{
int left=2*i+1;
int right=2*i+2;
int max=i;
if(left<=n&&nums[left]>nums[max]) max=left;
if(right<=n&&nums[right]>nums[max]) max=right;
if(max!=i)
{
swap(nums[max],nums[i]);
heapify(nums,n,max);
}
}
//建立一个大根堆
void heapify_build(vector<int>& nums,int n)
{
int start=(n-1)/2;
for(int i=start;i>=0;i--)
{
heapify(nums,n,i);
}
}
};
利用优先队列实现
#include<iostream>
using namespace std;
#include<vector>
#include<queue>
class Solution
{
public:
vector<int> smallestK(vector<int>& arr, int k)
{
if (k == 0) return{};
vector<int> res;
priority_queue<int, vector<int>, less<int>> q;
//用数组的前k个数建立大根堆
for (int i = 0; i < k; i++)
{
q.push(arr[i]);
}
//大根堆中维护当前最大的k个数
for (int i = k; i < arr.size(); i++)
{
//找一个比堆头小的数
if (arr[i] < q.top())
{
q.pop();
q.push(arr[i]);
}
}
for (int i = 0; i < k; i++)
{
res.push_back(q.top());
q.pop();
}
return res;
}
};
快排的变形
快速选择时间复杂度
第一次需要遍历n个元素 第二次n/2(假设) 所以总共n+n/2+n/4+…+1=o(n)
class Solution {
public:
vector<int> res;
vector<int> smallestK(vector<int>& arr, int k)
{
quick_sort(arr, 0, arr.size() - 1, k);
return res;
}
vector<int> quick_sort(vector<int> nums, int left, int right,int k)
{
if (left >= right)
return {};
//基准数一定要取出来
int i = left - 1, j = right + 1, x = nums[left + right >> 1];
while (i < j)
{
do i++; while (nums[i] < x);//找到一个>=x的数
do j--; while (nums[j] > x);//找到一个<=x的数
if (i < j)swap(nums[i], nums[j]);
}
//这时<=j 的都是<=x的 >=i的都是>=x的
//由于是要找最小的k个数 如果k是<=j的那么就只需要继续递归j的左边 反之同理
//这里要注意的是 <=j 实际上有j+1个数 所有k要跟j+1比较
if (k == j+1)
{
res.assign(nums.begin(), nums.begin() + k);
}
else if (k < j+1)
{
quick_sort(nums, left, j,k);
}
else if(k>j+1)
{
quick_sort(nums, j+1, right, k);
}
return res;
}
};
返回top-k
//k为数组下标 求top-k 传入k-1
int quick_sort(vector<int>& nums,int k,int left,int right)
{
if(left>=right) return nums[left] ;
int mid=(left+right)>>1;
int i=left-1,j=right+1;
int x=nums[mid];
while(i<j)
{
do i++;while(nums[i]<x);
do j--;while(nums[j]>x);
if(i<j) swap(nums[i],nums[j]);
}
cout<<"i= "<<i<<"j= "<<j<<endl;
cout<<"left= "<<left<<" right= "<<right<<endl;
if(k<=j) return quick_sort(nums,k,left,j);
else return quick_sort(nums,k,j+1,right);
}
二分查找
核心思想:
一般查找有序序列中的某个元素可以采用二分查找
基本公式:mid=1+(R-L)/2
二分模板记忆
https://www.acwing.com/blog/content/346/
二分模板
1.循环必须是l < r
2.if判断条件看是不是不满足条件, 然后修改上下界
3.若if else后是r = mid - 1,则前面mid 语句要加1
4.出循环一定是l == r,所以l和r用哪个都可以
二分只有下面两种情况
1:找大于等于给定数的第一个位置 (满足某个条件的第一个数)
2:找小于等于给定数的最后一个数 (满足某个条件的最后一个数)
// 判断条件很复杂时用check函数,否则if后直接写条件即可
bool check(int mid) {
...
return ...;
}
// 能二分的题一定是满足某种性质,分成左右两部分
// if的判断条件是让mid落在满足你想要结果的区间内
// 找满足某个条件的第一个数 即右半段
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1//是否加1取决于mid有没有可能是答案
}
return l;
}
// 找满足某个条件的最后一个数 即左半段
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
求开方k
- Sqrt(x) (Easy)**
题目描述
给定一个非负整数,求它的开方,向下取整。
输入输出样例
输入一个整数,输出一个整数。
Input: 8
Output: 2
8 的开方结果是 2:82842:::,向下取整即是 2。
核心思想:while结束的条件为L>R,初始L为0,R为a,mid为1+(R-L)/2 ,sqrt为a/mid;若sqrt大于mid,则将L赋值为mid+1,若sqrt小于mid,则将R赋值为mid+1,直到mid等于sqrt
int mySqrt(int a) {
if (a == 0) return a;
int l = 1, r = a, mid, sqrt;
while (l <= r) {
mid = l + (r - l) / 2;
sqrt = a / mid;
if (sqrt == mid) {
return mid;
} else if (mid > sqrt) {
r = mid - 1;
} else {
l = mid + 1;
}
}
return r;
}
查找值增序区间中的位置(二分查找模板题)
- Find First and Last Position of Element in Sorted Array (Medium)
题目描述
给定一个增序的整数数组和一个值,查找该值第一次和最后一次出现的位置。
输入输出样例
输入是一个数组和一个值,输出为该值第一次出现的位置和最后一次出现的位置(从 0 开
始);如果不存在该值,则两个返回值都设为-1。
Input: nums = [5,7,7,8,8,10], target = 8
Output: [3,4]
数字 8 在第 3 位第一次出现,在第 4 位最后一次出现。
核心思路 :还是每次减半,只不过需要分别找左边界和右边界,当nums[mid] = target时,right = mid,这样while结束时,left和right就会重合在左边界
#include <iostream>
#include <vector>
#include<algorithm>
#include <numeric>
#include <unordered_set>
#include<unordered_map>
#include <map>
#include <string>
using namespace std;
//
//
//
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target)
{
if (nums.empty())
{
return { -1,-1 };
}
//查找左区间和右区间模板基本相同 只要所以取边界得时候右区间要减1
int left = 0, right = nums.size();//这里right要取开区间
int mid,f_left=-1,f_right=-1;
//找左边界
while (left<right)
{
mid = (right + left) / 2;
if (nums[mid] >= target)//这里不同
{
right = mid;
}
if (nums[mid] < target)
{
left = mid+1 ;
}
}
f_left = left;
left = 0;
right = nums.size();
//找右边界
while (left < right)
{
mid = (right + left) / 2;
if (nums[mid] > target)
{
right = mid;
}
if (nums[mid] <= target)//这里不同
{
left = mid+1 ;
}
}
f_right = right-1;//这里right要减1
if (f_left==nums.size() || nums[f_left] != target)
{
return { -1,-1 };
}
return { f_left,f_right };
}
};
查找值是否存在于旋转数组中
- Search in Rotated Sorted Array II (Medium)
题目描述
一个原本增序的数组被首尾相连后按某个位置断开(如 [1,2,2,3,4,5] ! [2,3,4,5,1,2],在第一
位和第二位断开),我们称其为旋转数组。给定一个值,判断这个值是否存在于这个为旋转数组
中。
输入输出样例
输入是一个数组和一个值,输出是一个布尔值,表示数组中是否存在该值。
Input: nums = [2,5,6,0,0,1,2], target = 0
Output: true
核心思想:还是减半的思想,但是要先找到有序区间
#include <iostream>
#include <vector>
#include<algorithm>
#include <numeric>
#include <unordered_set>
#include<unordered_map>
#include <map>
#include <string>
using namespace std;
//start>mid 后半部分有序
//start<mid 前半部分有序
//start=mid 重复数字 start++ 跳过所有重复数字
//if 在有序区间 直接二分查找
//else if 重复上述 直到收敛到有序区间
class Solution {
public:
bool search(vector<int>& nums, int target)
{
int start = 0, end = nums.size() - 1;
while (start <= end) {
int mid = (start + end) / 2;
if (nums[mid] == target) {
return true;
}
if (nums[start] == nums[mid]) {
// 无法判断哪个区间是增序的
++start;
}
else if (nums[mid] <= nums[end]) {
// 右区间是增序的
if (target > nums[mid] && target <= nums[end]) {
start = mid + 1;
}
else {
end = mid - 1;
}
}
else {
// 左区间是增序的
if (target >= nums[start] && target < nums[mid]) {
end = mid - 1;
}
else {
start = mid + 1;
}
}
}
return false;
}
};
int main()
{
vector<int> nums{ 2,5,6,0,0,1,2 };
Solution s;
bool flag=s.search(nums,0);
if (flag)
{
cout << "true" << endl;
}
else
{
cout << "flase" << endl;
}
return 0;
}
矩阵中战斗力最弱的k行
class Solution {
public:
//稳定排序
//计算每一行1的个数存放到一个vector<pair>中 个数为键 行号为值
//排序 键不相等按键排序 键相等按值大小排序
//用二分来找1
int get_1_nums(vector<int> &row)
{
int left=0;
int right=row.size()-1;
int pos=-1;//用来保存最后出现1的下标
while(left<=right)
{
int mid=(left+right)>>1;
if(row[mid]==1)
{
left=mid+1;
pos=mid;
}
else right=mid-1;
}
return pos+1;
}
//自定义排序函数
struct cmp
{
bool operator()( pair<int,int> a, pair<int,int> b)
{
if(a.first<b.first) return false;
else if(a.first==b.first)
{
if(a.second<b.second) return false;
else return true;
}
else
{
return true;
}
}
};
vector<int> kWeakestRows(vector<vector<int>>& mat, int k)
{
//建立堆 并自定义排序规则
priority_queue<pair<int,int>,vector<pair<int,int>>,cmp> q;
// vector<pair<int,int>> temp;
vector<int> res;
for(int i=0;i<mat.size();i++)
{
int nums=get_1_nums(mat[i]);
// cout<<nums<<endl;
q.push(make_pair(nums,i));
}
// sort(temp.begin(),temp.end(),cmp);
for(int i=0;i<k;i++)
{
res.push_back(q.top().second);
q.pop();
}
return res;
}
};
双指针
算法核心思想
双指针多用于遍历数组,可以同时遍历一个数组或分别遍历不同的数组。常见的设计思想如下:
对于排序后的数组,双指针分别指向数组的头尾,遍历方向相反,用于搜索。
双指针指向两个数组,分别遍历
快慢指针
滑动窗口
两数之和
- Two Sum II - Input array is sorted (Easy)
题目描述
在一个增序的整数数组里找到两个数,使它们的和为给定值。已知有且只有一对解。
输入输出样例
输入是一个数组(numbers)和一个给定值(target)。输出是两个数的位置,从 1 开始计数。
Input: numbers = [2,7,11,15], target = 9
Output: [1,2]
在这个样例中,第一个数字(2)和第二个数字(7)的和等于给定值(9)。
核心思路:两个指针分别指向数组的头尾,相向而行遍历,若判断结果大于则增加左指针,若小于则减小右指针
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target)
{
vector<int>::iterator p_front = numbers.begin();
vector<int>::reverse_iterator p_behind = numbers.rbegin();
int first = 1;
int end = numbers.size();
while (1<end)
{
if (((*p_front )+( *p_behind)) == target)
{
return vector<int>{ first,end };
}
if ((*p_front + *p_behind) < target)
{
p_front++;
first++;
}
if ((*p_front + *p_behind) > target)
{
p_behind++;
end--;
}
}
}
};
int main()
{
vector<int> numbers{ 2,3,5,8,10,11 };
Solution s;
vector<int> ret;
ret= s.twoSum(numbers, 8);
cout << ret[0] << " " << ret[1] << endl;
}
归并两个有序数组
- Merge Sorted Array (Easy)
题目描述
给定两个有序数组,把两个数组合并为一个。
输入输出样例
输入是两个数组和它们分别的长度 m 和 n。其中第一个数组的长度被延长至 m + n,多出的
n 位被 0 填补。题目要求把第二个数组归并到第一个数组上,不需要开辟额外空间。
Input: nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
Output: nums1 = [1,2,2,3,5,6]
核心思想:两个指针分别遍历两个数组进行比较,每次将较大的放到第一个数组的末尾,并自减,第三个指针用来管理复制,如果第二个数组先复制完了,则剩下的数组不需要改变,若第一个数组先复制完,则将第二个数组剩余的元素放到第一个数组的头部
#include <iostream>
#include <vector>
#include<algorithm>
#include <numeric>
using namespace std;
class Solution {
public:
void merge(vector<int>& nums1, int m, vector<int>& nums2, int n)
{
int pos = m-- + n-- - 1;
while (m >= 0 && n >= 0)
{
// nums1[pos--] = nums1[m] > nums2[n] ? nums1[m--] : nums2[n--];
nums1[pos--] = (nums1[m] > nums2[n]) ? nums1[m--] : nums2[n--];
}
while (n >= 0)
{
nums1[pos--] = nums2[n--];
}
}
};
int main()
{
vector<int> nums1{ 1,2,3,0,0,0 };
vector<int> nums2{ 2,5,6 };
Solution s;
s.merge(nums1, 3, nums2,nums2.size());
for (int i = 0; i < nums1.size(); i++)
{
cout << nums1[i] << " ";
}
}
快慢指针
- Linked List Cycle II (Medium)
题目描述
给定一个链表,如果有环路,找出环路的开始点。
输入输出样例
输入是一个链表,输出是链表的一个节点。如果没有环路,返回一个空指针。
在这个样例中,值为 2 的节点即为环路的开始点。
如果没有特殊说明, LeetCode 采用如下的数据结构表示链表。
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
核心思想:如果两个人同时出发,如果赛道有环,那么快的一方总能追上慢的一方。进一步想,追上时快的一方肯定比慢的一方多跑了几圈,即多跑的路的长度是圈的长度的倍数。
原理:设起点到环的起点距离为m,已经确定有环,环的周长为n,(第一次)相遇点距离环的起点的距离是k。那么当两者相遇时,慢指针移动的总距离为i,i = m + a * n + k,因为快指针移动速度为慢指针的两倍,那么快指针的移动距离为2i,2i = m + b * n + k。其中,a和b分别为慢指针和快指针在第一次相遇时转过的圈数。我们让两者相减(快减慢),那么有i = (b - a) * n。即i是圈长度的倍数。利用这个结论我们就可以理解Floyd解法为什么能确定环的起点。将一个指针移到链表起点,另一个指针不变,即距离链表起点为i处,两者同时移动,每次移动一步。当第一个指针前进了m,即到达环起点时,另一个指针距离链表起点为i + m。考虑到i为圈长度的倍数,可以理解为指针从链表起点出发,走到环起点,然后绕环转了几圈,所以第二个指针也必然在环的起点。即两者相遇点就是环的起点。
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(x), next(NULL) {}
* };
*/
class Solution
{
public:
ListNode *detectCycle(ListNode *head)
{
ListNode *fast=head;
ListNode *slow=head;
do {
if (!fast || !fast->next) return nullptr;
fast = fast->next->next;
slow = slow->next;
} while (fast != slow);
fast=head;
while(fast!=slow)
{
fast=fast->next;
slow=slow->next;
}
return fast;
}
};
滑动窗口
- Minimum Window Substring (Hard)
题目描述
给定两个字符串 S 和 T,求 S 中包含 T 所有字符的最短连续子字符串的长度,同时要求时间
复杂度不得超过 O„n”。
输入输出样例
输入是两个字符串 S 和 T,输出是一个 S 字符串的子串。
Input: S = “ADOBECODEBANC”, T = “ABC”
Ou