剑指offer专题

本文介绍了多种算法问题的解决方案,包括字符串转整数、查找数组中的重复数字、不修改数组找重复、二维数组查找、替换空格、链表逆序打印、重建二叉树、用栈实现队列、旋转数组最小数字、矩阵路径、机器人运动范围、链表操作等。这些题目涵盖了数据结构和算法的基础知识,如动态规划、二分查找、回溯和递归等方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

p76 把字符串转换成整数
strToInt()
主要思路:
1. 先判断正负号的存在sign
2. 从正负号之后开始遍历字符串
if(str[i]>='0' && str[i]<='9'){
	num+=num*10+str[i]-'0';
}
p3 找出数组中重复的数字

主要思路:
就是num[i]与nums[nums[i]]之间的关系
在这里插入图片描述

p3不修改数组找重复数组
方法一

暴力

方法二

二分法

for(int i=0;i<nums.size();i++){
	// 先统计左区间值在[l,mid]区间有多少个数字
	记录在count
}
// 根据定理,[count单位长度有count+1个数,那么必定有一个数重复]
if(count>mid-l+1) r=mid; // 重复数字在左区间,否则在右区间
else l=mid+1;
p3 二维数组中的查找

主要思路:

  1. 二维数组是一个行递增,列递增的数组
  2. 因此从数组的右上角开始查找
i=0,j=size()-1
if(array[i][j]>target) j--; // 此列不考虑
else i++; // 此行不考虑
p4替换空格

主要思路:

  1. 先统计空格的数量
  2. 其次再从字符串的末尾覆盖原字符串,遇到空格则用%20代替
    需要注意的是,新的字符串的长度变为str.size()+2*count;
p5 从尾到头打印链表2
主要思路:
1. 先用stack去保存链表顺序输出的value
2. 在把stack出栈给vector,vector里面即是逆序
另一种思路,直接用vector去承接链表的值,然后再返回vector(rbegin(),rend());
p6 重建二叉树

主要思路:
二叉树的问题一般都是分治思想,递归去做,因为二叉树本身就是递归定义的
整体的思路是

  1. 前序序列获取根节点的信息,映射到中序序列,在中序序列中可以得到左右子树的长度
  2. 将左右子序的长度输入前序序列,就可以得到左右子树在前序序列中的索引
  3. 前序序列中左右子树的第一个节点为根节点,把这个根节点传入中序序列中
  4. 递归
unorded_map<int,int> map; // map['中序序列的元素']=index; // index为在中序序列中的索引号

TreeNode* buildTree(vector<int>& preorder, vector<int>& inordered){
	int n = preorder.size(); // 序列的长度
	
	// 将中序序列的位置信息用哈希表存储,便于查找根节点
	for(int i =0; i<inorder.size();++i){
		map[inorder[i]]=i; // map[key]=value
	}
	
	return dfs(preordered, inordered,0,n-1,0,n-1); // 前序序列,中序序列,前序的左右边界相对于前序序列),中序的左右边界(相对于中序序列))
}

TreeNode* dfs(vector<int> pre, vector<int> vin, int pl, int pr, int vl, int vr){
	// 前序 根左右 pre[pl]:根节点
	// 中序 左根右
 	// base case
 	 if(pl>pr) return NULL; // 到叶子节点了:当pl==pr时就到叶子节点了,在这里返回叶子节点的左右节点
 	 						// 为什么是base case
 	 						// 需要看递归论证
 	 						// 因为你在递归中传入的是:pl+1,pl+k
 	 						// 当到叶子节点时,k==0,此时pl+1>pl
	
	// 先序遍历重建二叉树
    // 找根节点
    TreeNode* root = new TreeNode(per[pl]);
    // 左子树的长度(在中序序列中计算)
    int k = map[pre[pl]]-vl; // 根节点在中序中的index/右边界-左(右)子树在中序序列的左边界
    
    // 左子树递归:传参结合画图
    root->left=dfs(pre,vin,pl+1/*左子树左边界在pre中*/,pl+k/*左子树右边界pre中*/,vl/*左子树左边界在vin中*/,vl+k-1/*左子树右边界在vin中*/);
    // 右子树递归
    root->right=dfs(pre,vin,pl+k+1,pr,vl)

}

在这里插入图片描述

p9 用两个栈实现队列

主要思路:

  1. 队列:管道, 栈:杯子
  2. 实现队列的push(),pop()功能,主要是通过两个stack来实现pop()
    具体操作如下:
push(){
	stack1<int>.push()
}
pop(){
	copy_stack(stack1,stack2);
	// stack1->stack2
	auto top_ele = stack2.top();
	copy_stack(stack2,stack1);
	
	return top_ele;
}
p10 旋转数组的最小数字

主要思路:
二分法用在具有两面性质的数组上,比如左区间都大于等于某个数,右区间都小于某个数,这样便就具有了某种性质
因此为了让数组具有二分的性质,先要对右区间末尾的数进行去重

while(nums[n=size()-1]==nums[0]) n--;

// base case
// 如果去重后是单调区间,直接返回(因为如果具有二段性,那么此时右区间都<nums[0])
if(nums[n]>nums[0]) return nums[0] 

// 开始二分
int l=0,r=n;
while(l<r)}{
	int mid = (l+r)>>2;
	if(nums[mid]>nums[0]/*说明mid在左区间*/){
		// 缩小到右区间的第一个数即旋转数组最小数
		l=mid+1;
	}
	else if(nums[mid]<nums[0]){
		r=mid; // 缩小区间到右区间的第一个数即旋转数组最小数
	}
	return mid;
}
p11 矩阵中的路径(dfs+回溯)

主要思路:
dfs+回溯

还有就是char *matrix是一维,怎么取元素,其实就相当于将二维matrix按行首尾相接铺开
int a = x+dx[i];
int b = y+dy[j];
matrix[a*cols+b]; // 取到按vector<vector<int>> matrix[a][b]定义的元素

// 其实关于dfs()其实需要明确的是dfs中的递归如何写
// 至于回溯,那就是为了减小内存消耗,递归树在某一支路没有搜到结果,往回搜索时接着上一个父节点往另一个分支取搜索
P12 机器人的运动范围(BFS+条件判断)

主要思路:
BFS的一些模板往上套

// 初始化队列
queue<pair<int,int>> q;
q.push({0,0});
vector<vector<bool>> visited(m,vector<int> n); // 是否遍历

// 开始BFS
while(q.size()){
	
	// dx={0,1,0-1};
	// dy={1,0,-1,0};
	auto tmp = q.front();
	q.pop();
	x=tmp.first;
	y=tmp.second;
	for(i=4){
		x=x+dx;
		y=y+dy;
		if(!inArea() && digit_sum>k) continue;	
	}	
}
p13 剪绳子

主要思路:
数学公式方法
动态规划(比较难)

p15 二进制中1的个数

主要思路:

  1. 将int n转为unsigned int n;
  2. 对于n, 统计n中1的个数
  int n1 =9;
  unsigned int n = n1;
  int count = 0;
  while (n)
  {
    if (n & 1)
      count++;
    n = n >> 1;
  }
  cout << count << endl;
  return 0;
p15 数值的整数次方

主要思路:
如果是负数,连乘之后要取倒数

链表

p17 删除链表中重复的节点

主要思路:
做链表的题一定要画图

  1. 建立虚拟头结点
  2. 双指针法
    整个链表分为三段
    p指针指向前一段链表(没有重复节点)的最后一位
    中间段为跳过的重复链表
    q指针指向新的链表的第一位
    定好位后将p->next=q,就相当于删除了重复链表
ListNode* deleteDuplication(ListNode* head){
	auto dummy = new ListNode(-1);
	dummy->next=head;
	
	auto p=dummy;
	while(p->next){
		auto q=p->next;

		while(q && p->next->val==q->val) q=q->next; // p->next与q之间为重复节点,q不断向后跳
		
		if(p->next->next==q) p=p->next; // 这两个节点之间的距离是否是1,是1说明经历了上面while一次,中间没有重复数字移动p
		else p->next=q; // p,q之间距离不止1,说明中间有重复数字,直接指向下一个元素
		
	}
	return dummy->next;
}

参考牛客网的题解,感觉更易懂
在这里插入图片描述

if(pHead==null || pHead.next==null){return pHead;}
ListNode dummy = new ListNode(0);
dummy.next = pHead;
ListNode pre = dummy;
ListNode last = dummy.next;

while(last!=null){
	if(last.next!=null && last.val==last.next.val){
		// 找到最后的一个相同节点
		while(last.next!=null && last.val == last.next.val){
			last = last.next;
		}
	}
	else{
		pre=pre.next;
		last=last.next;
	}
}
return dummy.next;
p21 链表中的倒数第k个节点
ListNode* kthNode(ListNode* pHead, int k){
	int length;
	ListNode* tmp=pHead;
	while(tmp){
		length++;
		tmp=tmp->next;
	}
	k=k%length;
	
	ListNode* first, second=pHead;
	while(k>0){
		first=first->next;
		k--;
	}
	// 快慢指针同时出发
	while(first->next){
		first=first->next;
		second=second->next;
	}
	
	return second;
}
p22 链表中环的入口结点
ListNode* circleNode(ListNode* pHead){
	ListNode* fast,*slow=pHead;
	while(fast){
		//fast=fast->next->next; 如果fast->next为null,则fast->next->next不能访问
		fast=fast->next;
		slow=slow->next;
		if(fast) fast=fast->next; // 正确的快指针的走法
		if(fast==slow){
			// 在环中相遇,回到出发点
			slow = pHead;
			while(fast!=slow){
				slow=slow->next;
				fast=fast->next;
			}
			return fast;
		}
	}
	return nullptr;
}
p24 合并两个排序的链表

主要思路:
归并排序的思想
然后链表的题一定要画图才能有思路

ListNode *sortGuibing(ListNode *phead1, ListNode *phead2)
  {
    ListNode *newHead = new ListNode(-1);
    auto cur = newHead;
    while (phead1 && phead2)
    {
      if (phead1->val <= phead2->val)
      {
        cur->next = phead1;
        cur = phead1;
        phead1 = phead1->next;
      }
      else
      {
        cur->next = phead2;
        cur = phead2;
        phead2 = phead2->next;
      }
    }
    if (phead1 /*说明pHead2先走到终点*/)
    {
      cur->next = phead1;
    }
    else
    {
      cur->next = phead2;
    }
    return newHead->next;
  }
p55 两个链表的第一个公共节点
ListNode *FindFirstCommonNode(ListNode *pHead1, ListNode *pHead2)
  {
    auto p1 = pHead1;
    auto p2 = pHead2;
    while (p1 != p2)
    {
      p1 = p1->next;
      p2 = p2->next;
      if (!p1)
        p1 = pHead2;
      if (!p2)
        p2 = pHead1;
    }

    return p1;
  }

数组

p21 调整数组顺序使奇数位于偶数前面

主要的思路:
两种方法:
方法一:
分别用队列保存奇数和偶数
然后再分别将队列push_back到数组中,
这个方法效率低,也重新开辟了空间,但是比较容易想到
方法二:
冒泡排序的思路,冒泡排序具有稳定性,所以采用

// 排序前后的稳定性
for(int i=0;i<array.size();++i){
	for(int j=array.size()-1;j>i;--j){
		if(array[j]%2==1 && array[j-1]%2==0){
			swap(array[j],array[j-1]);
		}
	}
}

动态规划

p43 连续子数组的最大和(面试高频)

主要思路:
这个用动态规划来解释可能会更明了
dp[i]:以 nums[i] 为结尾的最大子数组和为 dp[i]定义非常关键

int FindGreatestSumOfSubArray(vector<int> array){
	int n = num.size();
	if(0==n) return 0;

	vector<int> dp(n);
	// base case
	dp[0]=nums[0];
	// 状态转移方程
	for(int i=1;i<n;++i){
		dp[i]=max(nums[i],dp[i-1]+nums[i]);
	}
	// 结果,dp[i]最大值
	int res =INI_MIN;
	for(int i=0;i<n;++i){
		res=max(res,dp[i]);
	}
	return res;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值