├── .gitignore ├── Algorithm_Of_InterView.md ├── Company_Of_Interview.md ├── LICENSE ├── Maths_Of_Interview.md ├── README.md └── 剑指offer总结 ├── assets ├── basis_12_1.jpg ├── basis_19_1.jpg ├── basis_23_1.jpg ├── basis_25_1.jpg ├── basis_25_2.jpg ├── basis_25_3.jpg ├── basis_26_1.jpg ├── basis_26_3.jpg ├── basis_27_1.jpg ├── basis_30_1.jpg ├── basis_35_1.png ├── basis_35_2.png ├── basis_51_1.png ├── basis_51_2.png ├── basis_52_1.png ├── basis_57_1.png ├── basis_58_1.png ├── basis_64_1.png ├── basis_64_2.png ├── basis_6_1.jpg └── basis_6_2.jpg └── 剑指offer_summary.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | -------------------------------------------------------------------------------- /Algorithm_Of_InterView.md: -------------------------------------------------------------------------------- 1 | # 计算机视觉算法工程师面试中手撕代码的算法题总结 2 | 3 | ## 希望大家把自己的面试题目加进来,一起维护!本人本科非科班,可能一些划分和解答不合理,欢迎指出! 4 | 5 | ## (目前是微软、商汤、旷视、头条、阿里、腾讯、百度、海康威视、第四范式算法岗,其他公司也可以添加。) 6 | 7 | ## 1.按照公司划分 8 | 9 | ### 微软 10 | 1.n个文件(海量文件),查找和排序,二分查找时间复杂度(**微软**) 11 | 12 | 归并排序,二分查找 13 | 14 | 2.算法:一个数组里面是股票值,求什么时候购买和卖出,收益最大。 15 | 16 | 其实就是给定一个数组A,求max(Ai - Aj)。其中 i >j 。 17 | 18 | 一个数记录最大差,一个记录最小元素,遍历一次即可 19 | 20 | [leetcode 121](https://leetcode.com/problems/best-time-to-buy-and-sell-stock/) 21 | 22 | [leetcode 122] 23 | 24 | [leetcode 123] 25 | 26 | [leetcode 188] 27 | 28 | [leetcode 309] 29 | 30 | 3.最长连续公共子串(**微软**) 31 | 32 | ### 商汤 33 | 1.二叉树路径和为某一值的路径(**商汤**) 34 | 35 | [剑指offer 24](https://www.nowcoder.com/practice/b736e784e3e34731af99065031301bca?tpId=13&tqId=11177&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 36 | 37 | 2.数组中只出现一次的数字(**商汤**) 38 | 39 | [剑指offer 40](https://www.nowcoder.com/practice/e02fdb54d7524710a7d664d082bb7811?tpId=13&tqId=11193&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 40 | 41 | 3.链表中倒数第k个结点(**商汤**) 42 | 43 | [剑指offer 14](https://www.nowcoder.com/practice/529d3ae5a407492994ad2a246518148a?tpId=13&tqId=11167&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 44 | 45 | ### 旷视 46 | 1.数组中的逆序对(**旷视**) 47 | 48 | [剑指offer 35](https://www.nowcoder.com/practice/96bd6684e04a44eb80e6a68efc0ec6c5?tpId=13&tqId=11188&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 49 | 50 | ### 今日头条 51 | 52 | 1.x的n次方 53 | 54 | [leetcode 50](https://leetcode.com/problems/powx-n/) 55 | 56 | 2.链表排序(**头条**) 57 | 58 | [leetcode 148](https://leetcode.com/problems/sort-list/) 59 | 60 | 3.螺旋打印二维数组(**头条**) 61 | 62 | [leetcode 54](https://leetcode.com/problems/spiral-matrix/) 63 | 64 | 4.扎气球(**头条**) 65 | 66 | [leetcode 452](https://blog.csdn.net/yysave/article/details/84403875) 67 | 68 | 5.链表反转(**头条**) 69 | 70 | [剑指offer 15](https://www.nowcoder.com/practice/75e878df47f24fdc9dc3e400ec6058ca?tpId=13&tqId=11168&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 71 | 72 | 73 | ### 阿里巴巴 74 | 75 | ### 腾讯 76 | 77 | ### 百度 78 | 79 | ### 海康威视 80 | 1.二叉树的深度(**海康**) 81 | 82 | [剑指offer 38](https://www.nowcoder.com/practice/435fb86331474282a3499955f0a41e8b?tpId=13&tqId=11191&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 83 | 84 | 2.排序(**海康**) 85 | 86 | 快排,归并,堆排序 87 | 88 | 3.跳台阶(**海康**) 89 | 90 | [剑指offer 8](https://www.nowcoder.com/practice/8c82a5b80378478f9484d87d1c5f12a4?tpId=13&tqId=11161&tPage=1&rp=1&ru=%2Fta%2Fcoding-interviews&qru=%2Fta%2Fcoding-interviews%2Fquestion-ranking) 91 | 92 | 4.连续子数组的最大和(**海康**) 93 | 94 | [剑指offer 30](https://www.nowcoder.com/practice/459bd355da1549fa8a49e350bf3df484?tpId=13&tqId=11183&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) 95 | 96 | 5.最长不重复子串(**海康**) 97 | 98 | [leetcode 3/剑指offer第二版48](https://leetcode.com/problems/longest-substring-without-repeating-characters/) 99 | 100 | ### 第四范式 101 | 102 | 1.删除字符(**第四范式**) 103 | ``` 104 | abcdabcd 4 105 | bcdabcd 106 | bcdbcd 107 | cdbcd 108 | cdcd 109 | ``` 110 | 2.螺旋三角形输出(**第四范式**) 111 | ``` 112 | 5 113 | 114 | 1 115 | 2 12 116 | 3 13 11 117 | 4 14 15 10 118 | 5 6 7 8 9 119 | ``` 120 | 121 | ### 同学or面经题目(other) 122 | 123 | 1.判断链表对称/链表回文 124 | 125 | 2.求一个数组中只包含0,1使得其中0,1个数相等的最大子数组(百度) 126 | 127 | 3.给定一个二叉树pNode* root,找到二叉树里的pNode* target节点,并打印出路径(**阿里**) 128 | 129 | 先序遍历,一个vector存路径递归即可 130 | 131 | [参考解答](https://blog.csdn.net/shixiaoguo90/article/details/23759887) 132 | 133 | 4.最长公共子序列(阿里) 134 | 135 | ## 数据结构类 136 | ### 1.链表 137 | 商汤-3 138 | 头条-2,5 139 | ### 2.数组 140 | ### 3.字符串 141 | ### 4.栈与队列 142 | ### 5.二叉树 143 | 商汤-1 144 | 海康-1 145 | other-3 146 | ### 6.二叉搜索树 147 | ### 7.数组计数 148 | 第四范式-1 149 | ### 7.哈希表 150 | ## 算法类 151 | ### 1.递归 152 | 海康-3 153 | ### 2.排序 154 | 微软-1 155 | 旷视-1 156 | 海康-2 157 | ### 3.搜索 158 | ### 4.回溯 159 | ### 5.位运算 160 | 商汤-2 161 | ### 6.动态规划 162 | 微软-3 163 | 海康-4 164 | 海康-5 165 | ### 7.双指针 166 | ### 8.贪心 167 | 头条-4 168 | ### 9.限制运算 169 | ### 10.找规律 170 | 头条-3 171 | 第四范式-2 172 | 173 | -------------------------------------------------------------------------------- /Company_Of_Interview.md: -------------------------------------------------------------------------------- 1 | 岗位:计算机视觉算法岗 2 | 3 | 关于题目:凭记忆整理; 4 | 5 | 关于侵权:如果侵犯公司隐私,请告知删除; 6 | 7 | 关于个人:跨专业菜鸟一枚,只是给大家提供一些参考。 8 | 9 | ### 1.搜狗杭州cv岗 10 | 11 | 结果:需要六个月实习,去不了 12 | 13 | 全程50分钟,无传统算法题。 14 | 15 | 根据记忆,大概有如下一些问题。 16 | 17 | ##### 1.介绍一下前一段实习 18 | 19 | 简单介绍了一下目标检测的实习经历 20 | 21 | ##### 2.目标检测two-stage模型 22 | 23 | RCNN-->SPPNet-->Fast RCNN-->Faster RCNN-->RFCN-->DCN-->DCNv2 24 | 25 | 其中重点问了selective search和RPN,另外每个的创新点需要讲一下。 26 | 27 | ##### 3.目标检测one-stage模型 28 | 29 | YOLO系列、SSD、RefineDet 30 | yolo和ssd区别 31 | 32 | ##### 4.resnet和densenet及其区别 33 | 34 | ##### 5.Inception系列的演化 35 | 36 | ##### 6.BN的原理和实现细节,其中均值和标准差的计算,以及训练和测试时分别怎么用 37 | 38 | ##### 7. [Focal loss](https://blog.csdn.net/u014380165/article/details/77019084) 39 | 40 | ##### 8.小目标检测用什么方法 41 | ##### 9.mobilenet 42 | 43 | ##### 10.项目和比赛的某些细节 44 | ##### 11.coco冠军方案 45 | --------------------- 46 | 47 | ### 2.头条AI lab 48 | 开始以为头条就一个nlp类型的公司,拿来练手了,也是心大,第一天投,第二天就面试了,就刷了个位数的题,手撕代码虽然很简单,结果依然很惨。自然是一周内凉凉,后来又被另一个组捞,但是二面后告知对方不想做非检测方向,就结束了。 49 | 50 | #### 一面(1小时) 51 | 52 | ##### 1.算法:x的n次方(x任意,n自然数) 53 | 54 | ##### 2.算法:链表排序 55 | 56 | ##### 3.目标检测sota模型 57 | 58 | ##### 4.多标签不平衡怎么处理 59 | ##### 5.改善nms 60 | iou-guided-nms:iounet 61 | soft-nms 62 | 63 | ##### 6.改善rpn 64 | 65 | ##### 7.rfbnet 66 | Receptive Field Block 67 | 模拟人类视觉的感受野加强网络的特征提取能力,在结构上RFB借鉴了Inception的思想,主要是在Inception的基础上加入了dilated卷积层(dilated convolution),从而有效增大了感受野(receptive field) 68 | 69 | ##### 8.coco冠军方案 70 | #### 二面(1小时) 71 | ##### 1.介绍实习 72 | ##### 2.目标检测模型发展 73 | ##### 3.深度可分离卷积 74 | ##### 4.focal loss 75 | ##### 5.Densenet,有没有改进模型 76 | ##### 6.多标签/多任务不平衡怎么处理 77 | ##### 7.two-stage为什么效果更好 78 | ##### 8.算法:螺旋打印二维数组 79 | ### 3.平安科技 80 | 81 | 结果:offer 82 | 83 | ##### 1.相同层数,densenet和resnet哪个好,为什么densenet更好 84 | 85 | ##### 2.激活函数 86 | 87 | ##### 3.损失函数 88 | 89 | ##### 4.数字图像处理,各种滤波 90 | 91 | ##### 5.focal loss 92 | 93 | ##### 6.深度可分离卷积 94 | 95 | ##### 7.k折交叉验证 96 | 97 | ##### 8.模型融合,adaboost 98 | 99 | ### 4.第四范式 100 | 101 | 结果:一面写两道算法题,面试官说写的不错,但是我问面试官怎么看待第四范式毁约应届生,可能因为这个没给二面机会吧,不过我当时心情不怎么好,这样问确实有点不太妥,毕竟跟面试官也没什么关系。 102 | 103 | ``` 104 | 第一题:按照字母顺序删除指定数目的字母 105 | case: 106 | input: 107 | abcdabcd 4 108 | output: 109 | cdcd 110 | 111 | 第二题:给定数字n,输出循环三角形 112 | case: 113 | input 114 | 5 115 | output: 116 | 1 117 | 2 12 118 | 3 13 11 119 | 4 14 15 10 120 | 5 6 7 8 9 121 | ``` 122 | ### 5.海康威视 123 | 124 | offer 125 | 126 | 一面电面,二面现场上机。 127 | 128 | #### 一面 129 | ##### 1.自我介绍 130 | ##### 2.人脸属性SOTA模型 131 | ##### 3.实验室科研项目 132 | ##### 4.天池比赛 133 | ##### 5.kaggle比赛 134 | ##### 6.为什么选择densenet 135 | ##### 7.kaggle比赛阈值 136 | ##### 8.实习内容 137 | ##### 9.refinedet和rfcn-dcn 138 | ##### 10.二叉树的深度 139 | ##### 11.排序,快排、堆排序 140 | ##### 12.深拷贝和浅拷贝 141 | #### 二面 142 | 143 | ##### 1.算法:爬楼梯 144 | 145 | ##### 2.算法:连续子数组的最大和 146 | 147 | ##### 3.算法:最长不重复子串 148 | 149 | ##### 4.C++:继承与多态 150 | 151 | ##### 5.C++:指针与引用的区别 152 | 153 | ##### 6.数组和链表 154 | 155 | ### 6.商汤科技 156 | 157 | research offer。 158 | 159 | 强推一下商汤,面试官和hr都超级nice。 160 | 161 | #### 一面/二面 162 | ##### 1.自我介绍 163 | ##### 2.实验室项目 164 | ##### 3.mask rcnn 165 | roialign 166 | ##### 4.refinedet 167 | ##### 5.rfcn-dcn 168 | ##### 6.分类loss函数 169 | ##### 7.传统机器学习 170 | logistic回归 171 | svm 172 | boosting 173 | bagging 174 | 175 | ##### 8.数据预处理 176 | ##### 9.处理不平衡的方法 177 | ##### 10.ssd和yolo对比 178 | ##### 11.retinanet 179 | ##### 12.算法:二叉树路径和为给定值 180 | ##### 13.算法:一个数组,其他数出现两次,另一个出现一次,找出; 181 | 改进:另两个出现一次 182 | ##### 14.算法:链表中倒数第k个结点 183 | ##### 15.概率:圆上任意三个点组成的三角形,包含圆心的概率 184 | ##### 16.gan 185 | ##### 17.分布式,多卡使用 186 | ##### 18.什么框架 187 | dataloader,dataset,sampler关系 188 | ##### 19.创新的想法 189 | ##### 20.天池比赛的思路 190 | ##### 21.实习内容 191 | ### 7.旷视一面/二面/三面 192 | 193 | hr加了微信,三面后第二天还微信问了一堆,然后说综合评估,然后就没有然后了,感觉略坑,都这样了,挂了发个邮件也好,这方面头条就做的很好。 194 | 195 | ##### 1.说一下nms 196 | 197 | ##### 2.人脸和身体一起检测,怎么处理 198 | 199 | ##### 3.目前目标检测存在的问题,以及你的解决思路 200 | 201 | ##### 4.人脸检测 202 | 203 | ##### 5.算法:逆序对 204 | 205 | ##### 6.概率:x,y,z都是(0,1)均匀分布,x+y+z<1的概率 206 | 207 | ### 8.滴滴一面 208 | 主要看下是不是对分类、检测、分割是不是都比较熟悉,可能是需要全栈做项目吧。 209 | 210 | 感觉我也没兴趣,对方也没兴趣,不知道什么状态。 211 | 212 | ### 9.微软小冰 213 | 214 | 二面后两周左右问了说没挂,在综合评估,本来以为挂了。 215 | 216 | 然后收到电话,offer 217 | 218 | 微软的面试官真的很nice。 219 | 220 | #### 一面 221 | ##### 1.人脸属性的任务,方法 222 | 223 | ##### 2.天池 224 | 225 | ##### 3.kaggle 226 | 227 | ##### 4.目标检测 228 | 229 | ##### 5.n个文件(海量文件),查找和排序,二分查找时间复杂度 230 | 231 | 归并排序,二分查找 232 | #### 二面 233 | ##### 1.算法:一个数组里面是股票值,求什么时候购买和卖出,收益最大。 234 | 235 | 一个数记录最大差,一个记录最小元素,遍历一次即可 236 | ##### 2.算法:最长连续公共子串 237 | 238 | dp 239 | ##### 3.知道哪些cv任务 240 | 241 | 分类/检测/分割 242 | ##### 4.卷积/池化/全连接层/BN等组件 243 | 244 | IN/GN等 245 | ##### 5.激活函数 246 | 247 | ##### 6.优化器 248 | 249 | ##### 7.mAP的概念 250 | 251 | ### 10.阿里云 252 | 253 | offer 254 | 255 | #### 一面/二面 256 | ##### 1.多任务和多标签 257 | 258 | ##### 2.传统机器学习,svm,boosting,bagging,随机森林 259 | 260 | bagging和随机森林的区别 261 | ##### 3.属性任务不平衡 262 | 263 | ##### 4.目标检测存在的问题 264 | 265 | 小目标,怎么解决 266 | 遮挡,怎么解决 267 | 268 | #### 三面 269 | ##### 1.属性任务实际应用 270 | 271 | ##### 2.目标检测实际应用 272 | 273 | ##### 3.属性任务创新之处 274 | 275 | ##### 4.天池创新 276 | 277 | ##### 5.目标检测精度 278 | 279 | ##### 6.属性任务提升思路 280 | 281 | #### 四面 282 | ##### 1.深度神经网络和深度学习区别 283 | 284 | ##### 2.深度学习为什么比机器学习好 285 | 286 | ##### 3.各种排序算法,快排时间复杂度,时间复杂度推导, 287 | 288 | 时间复杂度O(n)的排序算法 289 | ##### 4.detection两阶段阈值,有什么好方法 290 | 291 | ##### 5.目标检测有什么改进 292 | 293 | ##### 6.目标检测实习调参 294 | 295 | #### HR面 296 | ##### 1.自我介绍 297 | 298 | ##### 2.实习 299 | 300 | ##### 3.个人规划 301 | 302 | ##### 4.希望做研究还是工程 303 | 304 | ##### 5.有没有和人发生争执 305 | 306 | ##### 6.遇到的困难 307 | 308 | ##### 7.给你项目怎么规划 309 | 310 | ##### 8.有没有论文 311 | 312 | 313 | ### 11.腾讯一面 314 | 被cdg广告部门捞简历,拒面了。 315 | 316 | ### 12.Intel北京研究院 317 | 这个是日常实习,告诉面试官只能暑期,他坚持面一下。 318 | 319 | 也是超级nice的面试官,感觉好像外企面试官都很好,当然国内一些公司比如商汤阿里的部分面试官也很好。 320 | 321 | 讨论很有深度,也很舒服,感觉双方都比较满意。 322 | 323 | 说如果想去实习可以提前一个月联系。 324 | 325 | ##### 1.介绍自己的科研和实习经历 326 | 327 | ##### 2.实习用的模型 328 | 329 | 具体达到的recall和precision 330 | ##### 3.weighted sample和focal loss 331 | 332 | ##### 4.人脸属性sota模型 333 | 334 | ##### 5.天池模型改进的解释 335 | 336 | ##### 6.解决不平衡的思路 337 | 338 | ##### 7.train,val,test 339 | 340 | 过拟合,怎么调参 341 | ##### 8.如果训练集不平衡,测试集平衡,直接训练和过采样欠采样处理,哪个更好 342 | 343 | ##### 9.softmax 344 | 345 | ##### 10.F1score是 $\alpha$ =1,那么$\alpha$什么时候取其他值 346 | 347 | ### 13.华为终端 348 | 349 | [华为终端算法实习](https://zhuanlan.zhihu.com/p/63954419) 350 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 spectre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Maths_Of_Interview.md: -------------------------------------------------------------------------------- 1 | # 算法工程师面试中相关数学题总结 2 | 3 | ## 大家一起贡献自己面过的数学题呀! 4 | 5 | #### 1.概率:圆上任意三个点组成的三角形,包含圆心的概率 6 | 7 | 答案:1/4 8 | 9 | #### 2.概率:x,y,z都是(0,1)均匀分布,x+y+z<1的概率 10 | 11 | 答案:1/6 12 | 13 | #### 3.期望:抛硬币直到连续若干次正面的概率 14 | 15 | 这题比较麻烦,现场基本推不出来。。 16 | 17 | [抛硬币直到连续若干次正面的概率](https://www.cnblogs.com/avril/archive/2013/06/28/3161669.html) 18 | 19 | [用数学解赌博问题不稀奇,用赌博解数学问题才牛B](http://www.matrix67.com/blog/archives/3638) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Algorithms_Engineer_Interview 2 | 计算机视觉算法工程师面试总结 3 | 4 | ## 1.[手撕代码](https://github.com/espectre/Algorithms_Engineer_Interview/blob/master/Algorithm_Of_InterView.md) 5 | ## 2.[数学题](https://github.com/espectre/Algorithms_Engineer_Interview/blob/master/Maths_Of_Interview.md) 6 | ## 3.[实习面试](https://github.com/espectre/Algorithms_Engineer_Interview/blob/master/Company_Of_Interview.md) 7 | ## 4.待续 8 | -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_12_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_12_1.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_19_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_19_1.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_23_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_23_1.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_25_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_25_1.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_25_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_25_2.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_25_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_25_3.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_26_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_26_1.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_26_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_26_3.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_27_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_27_1.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_30_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_30_1.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_35_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_35_1.png -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_35_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_35_2.png -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_51_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_51_1.png -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_51_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_51_2.png -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_52_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_52_1.png -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_57_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_57_1.png -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_58_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_58_1.png -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_64_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_64_1.png -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_64_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_64_2.png -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_6_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_6_1.jpg -------------------------------------------------------------------------------- /剑指offer总结/assets/basis_6_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/espectre/Algorithms_Engineer_Interview/8c6826a0f8aa548dc1024eaac2971841b98ba0b5/剑指offer总结/assets/basis_6_2.jpg -------------------------------------------------------------------------------- /剑指offer总结/剑指offer_summary.md: -------------------------------------------------------------------------------- 1 | [TOC] 2 | 3 | toc好像在Github没什么用,下载了可以有目录 4 | --- 5 | 6 | 7 | ### [牛客网刷题页面](https://www.nowcoder.com/ta/coding-interviews) 8 | 9 | --- 10 | 11 | ### 一、数据结构类 12 | 13 | 14 | 15 | #### 1. 链表(8) 16 | 17 | ##### 链表结构体定义 18 | ```c 19 | struct ListNode { 20 | int val; 21 | struct ListNode* next; 22 | ListNode(int x) : 23 | val(x), next(NULL) { 24 | } 25 | }; 26 | ``` 27 | ##### 复杂链表结构体 28 | ```c 29 | struct RandomListNode { 30 | int label; 31 | struct RandomListNode *next, *random; 32 | RandomListNode(int x) : 33 | label(x), next(NULL), random(NULL) { 34 | } 35 | }; 36 | ``` 37 | ##### 链表原则 38 | ``` 39 | 1.使用p->next之前先判断p是否为空 40 | ``` 41 | 42 | ##### 1. 003-从尾到头打印链表 43 | **题目描述** 44 | 45 | 输入一个链表,返回一个反序的链表。 46 | 47 | **解题思路** 48 | 49 | 不希望修改原链表的结构,后进先出,使用栈实现 50 | 51 | **参考代码** 52 | ```c 53 | class Solution { 54 | public: 55 | vector printListFromTailToHead(ListNode* head) { 56 | stack nodes; 57 | vector result; 58 | ListNode* pNode = head; 59 | while(pNode!= NULL){ 60 | nodes.push(pNode->val); 61 | pNode = pNode->next; 62 | } 63 | 64 | while(!nodes.empty()){ 65 | result.push_back(nodes.top()); 66 | nodes.pop(); 67 | } 68 | return result; 69 | } 70 | }; 71 | ``` 72 | ##### 2. 014-链表中倒数第k个结点 73 | **题目描述** 74 | 75 | 输入一个链表,输出该链表中倒数第k个结点。 76 | 77 | **解题思路** 78 | 79 | 定义两个指针,第一个先移动k-1步,然后两个指针同时移动;第一个指针到达末尾时,第二个指针刚好到达倒数第k个结点 80 | 81 | **参考代码** 82 | ```c 83 | class Solution { 84 | public: 85 | ListNode* FindKthToTail(ListNode* pListHead, unsigned int k) { 86 | if(pListHead==NULL||k<=0){ 87 | return NULL; 88 | } 89 | ListNode* pAhead=pListHead; 90 | ListNode* pBehind=pListHead; 91 | for(unsigned int i=0;inext!=NULL){ 93 | pAhead=pAhead->next; 94 | } 95 | else{ 96 | return NULL; 97 | } 98 | } 99 | while(pAhead->next!=NULL){ 100 | pAhead=pAhead->next; 101 | pBehind=pBehind->next; 102 | } 103 | return pBehind; 104 | } 105 | }; 106 | ``` 107 | ##### 3. 015-反转链表 108 | **题目描述** 109 | 110 | 输入一个链表,反转链表后,输出链表的所有元素。 111 | 112 | **解题思路** 113 | 114 | 使用三个指针,分别指向当前遍历到的结点、它的前一个结点以及后一个结点。 115 | 116 | 遍历时,当前结点的指针指向前一个结点。 117 | 118 | **参考代码** 119 | ```c 120 | class Solution { 121 | public: 122 | ListNode* ReverseList(ListNode* pHead) { 123 | ListNode* pReversedHead = NULL; 124 | ListNode* pNode = pHead; 125 | ListNode* pPrev = NULL; 126 | while(pNode != NULL){ 127 | ListNode* pNext = pNode->next; 128 | if(pNext == NULL){ 129 | pReversedHead = pNode; 130 | } 131 | pNode->next = pPrev; 132 | pPrev = pNode; 133 | pNode = pNext; 134 | } 135 | return pReversedHead; 136 | } 137 | }; 138 | ``` 139 | ##### 4. 016-合并两个排序的链表 140 | **题目描述** 141 | 142 | 输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。 143 | 144 | **解题思路** 145 | 146 | 先判断输入的链表是否为空。如果第一个链表为空,则直接返回第二个链表;如果第二个链表为空,则直接返回第一个链表。如果两个链表都是空链表,合并的结果是得到一个空链表。 147 | 148 | 两个链表都是排序好的,我们只需要从头遍历链表,判断当前指针,哪个链表中的值小,即赋给合并链表指针即可。使用递归就可以轻松实现。 149 | 150 | **参考代码** 151 | ```c 152 | class Solution { 153 | public: 154 | ListNode* Merge(ListNode* pHead1, ListNode* pHead2){ 155 | //判断指针是否为空 156 | if(pHead1 == NULL){ 157 | return pHead2; 158 | } 159 | else if(pHead2 == NULL){ 160 | return pHead1; 161 | } 162 | ListNode* pMergedHead = NULL; 163 | if(pHead1->val < pHead2->val){ 164 | pMergedHead = pHead1; 165 | pMergedHead->next = Merge(pHead1->next, pHead2); 166 | } 167 | else{ 168 | pMergedHead = pHead2; 169 | pMergedHead->next = Merge(pHead1, pHead2->next); 170 | } 171 | return pMergedHead; 172 | } 173 | }; 174 | ``` 175 | ##### 5. 025-复杂链表的复制 176 | **题目描述** 177 | 178 | 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空) 179 | 180 | **解题思路** 181 | 182 | 将复杂链表的复制过程分解为三个步骤。 183 | 184 | 在写代码的时候我们每一步定义一个函数,这样每个函数完成一个功能,整个过程的逻辑也就非常清晰明了了。 185 | 186 | 我们这里采用三步: 187 | 188 | 第一步:复制复杂链表的label和next。但是这次我们把复制的结点跟在原结点后面,而不是直接创建新的链表; 189 | 190 | 第二步:设置复制出来的结点的random。因为新旧结点是前后对应关系,所以也是一步就能找到random; 191 | 192 | 第三步:拆分链表。奇数是原链表,偶数是复制的链表。 193 | 194 | ![basis_25_1](assets/basis_25_1.jpg) 195 | 196 | ![basis_25_2](assets/basis_25_2.jpg) 197 | 198 | ![basis_25_3](assets/basis_25_3.jpg) 199 | 200 | **参考代码** 201 | 202 | ```c 203 | class Solution { 204 | public: 205 | 206 | //第一步,复制复杂链表的label和next 207 | void CloneNodes(RandomListNode* pHead){ 208 | RandomListNode* pNode = pHead; 209 | while(pNode != NULL){ 210 | RandomListNode* pCloned = new RandomListNode(0); 211 | pCloned->label = pNode->label; 212 | pCloned->next = pNode->next; 213 | pCloned->random = NULL; 214 | 215 | pNode->next = pCloned; 216 | pNode = pCloned->next; 217 | } 218 | } 219 | 220 | //第二步,处理复杂链表的random指针 221 | void ConnectSiblingNodes(RandomListNode* pHead){ 222 | RandomListNode* pNode = pHead; 223 | while(pNode != NULL){ 224 | RandomListNode* pCloned = pNode->next; 225 | if(pNode->random != NULL){ 226 | pCloned->random = pNode->random->next; 227 | } 228 | pNode = pCloned->next; 229 | } 230 | } 231 | 232 | //第三步,拆分复杂指针 233 | RandomListNode* ReconnectNodes(RandomListNode* pHead){ 234 | RandomListNode* pNode = pHead; 235 | RandomListNode* pClonedHead = NULL; 236 | RandomListNode* pClonedNode = NULL; 237 | 238 | if(pNode != NULL){ 239 | pClonedHead = pClonedNode = pNode->next; 240 | pNode->next = pClonedNode->next; 241 | pNode = pNode->next; 242 | } 243 | 244 | while(pNode != NULL){ 245 | pClonedNode->next = pNode->next; 246 | pClonedNode = pClonedNode->next; 247 | pNode->next = pClonedNode->next; 248 | pNode = pNode->next; 249 | } 250 | return pClonedHead; 251 | } 252 | 253 | RandomListNode* Clone(RandomListNode* pHead) 254 | { 255 | CloneNodes(pHead); 256 | ConnectSiblingNodes(pHead); 257 | return ReconnectNodes(pHead); 258 | } 259 | }; 260 | ``` 261 | ##### 6. 036-两个链表的第一个公共结点 262 | **题目描述** 263 | 264 | 输入两个链表,找出它们的第一个公共结点。 265 | 266 | **解题思路** 267 | 268 | 先让把长的链表的头砍掉,让两个链表长度相同,这样,同时遍历也能找到公共结点。此时,时间复杂度O(m+n),空间复杂度为O(MAX(m,n))。 269 | 270 | **参考代码** 271 | ```c 272 | class Solution { 273 | public: 274 | ListNode* FindFirstCommonNode( ListNode* pHead1, ListNode* pHead2) { 275 | // 如果有一个链表为空,则返回结果为空 276 | if(pHead1 == NULL || pHead2 == NULL){ 277 | return NULL; 278 | } 279 | // 获得两个链表的长度 280 | unsigned int len1 = GetListLength(pHead1); 281 | unsigned int len2 = GetListLength(pHead2); 282 | // 默认 pHead1 长, pHead2短,如果不是,再更改 283 | ListNode* pHeadLong = pHead1; 284 | ListNode* pHeadShort = pHead2; 285 | int LengthDif = len1 - len2; 286 | // 如果 pHead1 比 pHead2 小 287 | if(len1 < len2){ 288 | ListNode* pHeadLong = pHead2; 289 | ListNode* pHeadShort = pHead1; 290 | int LengthDif = len2 - len1; 291 | } 292 | // 将长链表的前面部分去掉,使两个链表等长 293 | for(int i = 0; i < LengthDif; i++){ 294 | pHeadLong = pHeadLong->next; 295 | } 296 | 297 | while(pHeadLong != NULL && pHeadShort != NULL && pHeadLong != pHeadShort){ 298 | pHeadLong = pHeadLong->next; 299 | pHeadShort = pHeadShort->next; 300 | } 301 | return pHeadLong; 302 | } 303 | private: 304 | // 获得链表长度 305 | unsigned int GetListLength(ListNode* pHead){ 306 | if(pHead == NULL){ 307 | return 0; 308 | } 309 | unsigned int length = 1; 310 | while(pHead->next != NULL){ 311 | pHead = pHead->next; 312 | length++; 313 | } 314 | return length; 315 | } 316 | }; 317 | ``` 318 | ##### 7. 055-链表中环的入口结点 319 | **题目描述** 320 | 321 | 一个链表中包含环,请找出该链表的环的入口结点。 322 | 323 | **解题思路** 324 | 325 | 判断是否有环:使用快慢指针,一个每次走一步,一个每次走两步。如果两个指针相遇,表明链表中存在环,并且两个指针相遇的结点一定在环中。 326 | 327 | 计算环的长度:从相遇的这个环中结点出发,一边继续向前移动一边计数,当再次回到这个结点时,就可以得到环中结点数目了。 328 | 329 | 寻找环的入口:定义两个指针P1和P2指向链表的头结点。如果链表中的环有n个结点,指针P1先在链表上向前移动n步,然后两个指针以相同的速度向前移动。当第二个指针指向入口结点时,第一个指针已经绕环一圈又回到了入口结点,它们相遇的结点正好是环的入口结点。 330 | 331 | **参考代码** 332 | ```c 333 | class Solution { 334 | public: 335 | ListNode* EntryNodeOfLoop(ListNode* pHead) 336 | { 337 | if(pHead == NULL){ 338 | return NULL; 339 | } 340 | ListNode* meetingnode = MeetingNode(pHead); 341 | if(meetingnode == NULL){ 342 | return NULL; 343 | } 344 | // 回环链表结点个数 345 | int nodesloop = 1; 346 | // 找到环中结点个数 347 | ListNode* pNode1 = meetingnode; 348 | while(pNode1->next != meetingnode){ 349 | pNode1 = pNode1->next; 350 | nodesloop++; 351 | } 352 | pNode1 = pHead; 353 | // 第一个指针向前移动nodesloop步 354 | for(int i = 0; i < nodesloop; i++){ 355 | pNode1 = pNode1->next; 356 | } 357 | // 两个指针同时移动,找到环入口 358 | ListNode* pNode2 = pHead; 359 | while(pNode1 != pNode2){ 360 | pNode1 = pNode1->next; 361 | pNode2 = pNode2->next; 362 | } 363 | return pNode1; 364 | } 365 | private: 366 | // 使用快慢指针,找到任意的一个环中结点 367 | ListNode* MeetingNode(ListNode* pHead){ 368 | ListNode* pSlow = pHead->next; 369 | if(pSlow == NULL){ 370 | return NULL; 371 | } 372 | ListNode* pFast = pSlow->next; 373 | while(pFast != NULL && pSlow != NULL){ 374 | if(pFast == pSlow){ 375 | return pFast; 376 | } 377 | pSlow = pSlow->next; 378 | pFast = pFast->next; 379 | if(pFast != NULL){ 380 | pFast = pFast->next; 381 | } 382 | } 383 | return NULL; 384 | } 385 | }; 386 | ``` 387 | ##### 8. 056-删除链表中重复的结点 388 | **题目描述** 389 | 390 | 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。 391 | 392 | **解题思路** 393 | 394 | 删除重复结点,只需要记录当前结点前的最晚访问过的不重复结点pPre、当前结点pCur、指向当前结点后面的结点pNext的三个指针即可。如果当前节点和它后面的几个结点数值相同,那么这些结点都要被删除,然后更新pPre和pCur;如果不相同,则直接更新pPre和pCur。 395 | 396 | 需要考虑的是,如果第一个结点是重复结点我们该怎么办?这里我们分别处理一下就好,如果第一个结点是重复结点,那么就把头指针pHead也更新一下。 397 | 398 | **参考代码** 399 | ```c 400 | class Solution { 401 | public: 402 | ListNode* deleteDuplication(ListNode* pHead) 403 | { 404 | if(pHead == NULL){ 405 | return NULL; 406 | } 407 | // 指向当前结点前最晚访问过的不重复结点 408 | ListNode* pPre = NULL; 409 | // 指向当前处理的结点 410 | ListNode* pCur = pHead; 411 | // 指向当前结点后面的结点 412 | ListNode* pNext = NULL; 413 | 414 | while(pCur != NULL){ 415 | // 如果当前结点与下一个结点相同 416 | if(pCur->next != NULL && pCur->val == pCur->next->val){ 417 | pNext = pCur->next; 418 | // 找到不重复的最后一个结点位置 419 | while(pNext->next != NULL && pNext->next->val == pCur->val){ 420 | pNext = pNext->next; 421 | } 422 | // 如果pCur指向链表中第一个元素,pCur -> ... -> pNext ->... 423 | // 要删除pCur到pNext, 将指向链表第一个元素的指针pHead指向pNext->next。 424 | if(pCur == pHead){ 425 | pHead = pNext->next; 426 | } 427 | // 如果pCur不指向链表中第一个元素,pPre -> pCur ->...->pNext ->... 428 | // 要删除pCur到pNext,即pPre->next = pNext->next 429 | else{ 430 | pPre->next = pNext->next; 431 | } 432 | // 向前移动 433 | pCur = pNext->next; 434 | } 435 | // 如果当前结点与下一个结点不相同 436 | else{ 437 | pPre = pCur; 438 | pCur = pCur->next; 439 | } 440 | } 441 | return pHead; 442 | } 443 | }; 444 | ``` 445 | 446 | 447 | #### 2. 数组(5) 448 | 449 | 450 | ##### 1. 013-调整数组顺序使奇数位于偶数前面 451 | **题目描述** 452 | 453 | 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。 454 | 455 | **解题思路** 456 | 457 | 创建双向队列,遍历数组,奇数前插入,偶数后插入。最后使用assign方法实现不同容器但相容的类型赋值。 458 | 459 | **参考代码** 460 | ```c 461 | class Solution { 462 | public: 463 | void reOrderArray(vector &array) { 464 | deque result; 465 | int num = array.size(); 466 | for(int i = 0; i < num; i++){ 467 | if(array[num - i - 1] % 2 == 1){ 468 | result.push_front(array[num - i - 1]); 469 | } 470 | if(array[i] % 2 == 0){ 471 | result.push_back(array[i]); 472 | } 473 | } 474 | array.assign(result.begin(),result.end()); 475 | } 476 | }; 477 | ``` 478 | 479 | ##### 2. 028-数组中出现次数超过一半的数字 480 | **题目描述** 481 | 482 | 数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。 483 | 484 | **解题思路** 485 | 486 | 数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现次数的和还要多。因此我们可以考虑在遍历数组的时候保存两个值:一个是数组中的一个数字,一个是这个数字出现的次数。当我们遍历到下一个数字的时候,如果下一个数字和我们之前保存的数字相同,则次数加1;如果下一个数字和我们之前保存的数字不同,则次数减1。如果次数为零,我们需要保存下一个数字,并把次数设为1。由于我们要找的数字出现的次数比其他所有数字出现的次数之和还要多,那么要找的数字肯定是最后一次把次数设为1时对应的数字。 487 | 488 | **参考代码** 489 | ```c++ 490 | class Solution { 491 | public: 492 | int MoreThanHalfNum_Solution(vector numbers) { 493 | if(numbers.empty()){ 494 | return 0; 495 | } 496 | // 遍历每个元素,并记录次数;若与前一个元素相同,则次数加1,否则次数减1 497 | int result = numbers[0]; 498 | int times = 1; 499 | for(int i = 1; i < numbers.size(); ++i){ 500 | if(times == 0){ 501 | // 更新result的值为当前元素,并置次数为1 502 | result = numbers[i]; 503 | times = 1; 504 | } 505 | else if(numbers[i] == result){ 506 | times++; 507 | } 508 | else{ 509 | times--; 510 | } 511 | } 512 | // 判断result是否符合条件,即出现次数大于数组长度的一半 513 | times = 0; 514 | for(int i = 0; i < numbers.size(); ++i) 515 | { 516 | if(numbers[i] == result){ 517 | times++; 518 | } 519 | } 520 | return (times > (numbers.size() >> 1)) ? result : 0; 521 | } 522 | }; 523 | ``` 524 | 525 | ##### 3. 032-把数组排成最小的数 526 | **题目描述** 527 | 528 | 输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。 529 | 530 | **解题思路** 531 | 532 | 遇到这个题,全排列当然可以做,但是时间复杂度为O(n!)。 533 | 534 | 在这里我们自己定义一个规则,对拼接后的字符串进行比较。 535 | 536 | 排序规则如下: 537 | 538 | 若ab > ba 则 a 大于 b, 539 | 若ab < ba 则 a 小于 b, 540 | 若ab = ba 则 a 等于 b; 541 | 根据上述规则,我们需要先将数字转换成字符串再进行比较,因为需要串起来进行比较。比较完之后,按顺序输出即可。 542 | 543 | **参考代码** 544 | 545 | ```C++ 546 | class Solution { 547 | public: 548 | string PrintMinNumber(vector numbers) { 549 | int length = numbers.size(); 550 | if(length == 0){ 551 | return ""; 552 | } 553 | sort(numbers.begin(), numbers.end(), cmp); 554 | string res; 555 | for(int i = 0; i < length; i++){ 556 | res += to_string(numbers[i]); 557 | } 558 | return res; 559 | } 560 | private: 561 | // 升序排序 562 | static bool cmp(int a, int b){ 563 | string A = to_string(a) + to_string(b); 564 | string B = to_string(b) + to_string(a); 565 | return A < B; 566 | } 567 | }; 568 | ``` 569 | 570 | ##### 4. 050-数组中重复的数字 571 | **题目描述** 572 | 573 | 在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。 574 | 575 | **解题思路** 576 | 577 | 把当前序列当成是一个下标和下标对应值是相同的数组(时间复杂度为O(n),空间复杂度为O(1)); 578 | 579 | 遍历数组, 580 | 581 | 判断当前位的值和下标是否相等: 582 | 583 | 若相等,则遍历下一位; 584 | 585 | 若不等,则将当前位置i上的元素和a[i]位置上的元素比较:若它们相等,则找到了第一个相同的元素;若不等,则将它们交换。换完之后a[i]位置上的值和它的下标是对应的,但i位置上的元素和下标并不一定对应;重复上面的操作,直到当前位置i的值也为i,将i向后移一位,再重复操作。 586 | 587 | **参考代码** 588 | 589 | ```C++ 590 | class Solution { 591 | public: 592 | // Parameters: 593 | // numbers: an array of integers 594 | // length: the length of array numbers 595 | // duplication: (Output) the duplicated number in the array number 596 | // Return value: true if the input is valid, and there are some duplications in the array number 597 | // otherwise false 598 | bool duplicate(int numbers[], int length, int* duplication) { 599 | // 非法输入 600 | if(numbers == NULL || length <= 0){ 601 | return false; 602 | } 603 | // 非法输入 604 | for(int i = 0; i < length; i++){ 605 | if(numbers[i] < 0 || numbers[i] > length - 1){ 606 | return false; 607 | } 608 | } 609 | // 遍历查找第一个重复的数 610 | for(int i = 0; i < length; i++){ 611 | while(numbers[i] != i){ 612 | if(numbers[i] == numbers[numbers[i]]){ 613 | *duplication = numbers[i]; 614 | return true; 615 | } 616 | swap(numbers[i], numbers[numbers[i]]); 617 | } 618 | } 619 | return false; 620 | } 621 | }; 622 | ``` 623 | 624 | ##### 5. 051-构建乘积数组 625 | **题目描述** 626 | 627 | 给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。 628 | 629 | **解题思路** 630 | 631 | 观察下公式,你会发现,B[i]公式中没有A[i]项, 632 | 633 | 现在要求不能使用除法。一个直观的解法是用连乘n-1个数字得到B[i]。显然这个方法需要O(n*n)的时间复杂度。 634 | 635 | 好在还有更高效的算法。可以把B[i]=A[0]*A[1]*.....*A[i-1]*A[i+1]*.....*A[n-1]。看成A[0]*A[1]*.....*A[i-1]和A[i+1]*.....A[n-2]*A[n-1]两部分的乘积。 636 | 637 | 即通过A[i]项将B[i]分为两部分的乘积。效果如下图所示: 638 | 639 | ![basis_51_1](assets/basis_51_1.png) 640 | 641 | 不妨设定C[i]=A[0]*A[1]*...*A[i-1],D[i]=A[i+1]*...*A[n-2]*A[n-1]。C[i]可以用自上而下的顺序计算出来,即C[i]=C[i-1]*A[i-1]。类似的,D[i]可以用自下而上的顺序计算出来,即D[i]=D[i+1]*A[i+1]。 642 | 643 | ![basis_51_2](assets/basis_51_2.png) 644 | 645 | 第一个for循环用来计算上图1范围的数,第二个for循环用来计算上图2范围的数。 646 | 647 | **参考代码** 648 | 649 | ```c++ 650 | class Solution { 651 | public: 652 | vector multiply(const vector& A) { 653 | int length = A.size(); 654 | vector B(length); 655 | if(length <= 0){ 656 | return B; 657 | } 658 | B[0] = 1; 659 | for(int i = 1; i < length; i++){ 660 | B[i] = B[i - 1] * A[i - 1]; 661 | } 662 | int temp = 1; 663 | for(int i = length - 2; i >= 0; i--){ 664 | temp *= A[i + 1]; 665 | B[i] *= temp; 666 | } 667 | return B; 668 | } 669 | }; 670 | ``` 671 | 672 | 673 | 674 | #### 3. 字符串(4) 675 | 676 | 677 | ##### 1. 002-替换空格 678 | **题目描述** 679 | 680 | 请实现一个函数,将一个字符串中的空格替换成“%20”。 681 | 682 | 例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。 683 | 684 | **解题思路** 685 | 686 | 采用一种时间复杂度为O(n)的方法。 687 | 688 | 我们可以先遍历一次字符串,这样就可以统计出字符串空格的总数,并可以由此计算出替换之后的字符串的总长度。每替换一个空格,长度增加2,因此替换以后字符串的长度等于原来的长度加上2倍的空格数目。以"We are happy"为例,"We are happy"这个字符串的长度为14(包括结尾符号"\n"),里面有两个空格,因此替换之后字符串的长度是18。 689 | 690 | 我们从字符串的尾部开始复制和替换。首先准备两个指针,P1和P2,P1指向原始字符串的末尾,而P2指向替换之后的字符串的末尾。接下来我们向前移动指针P1,逐个把它指向的字符复制到P2指向的位置,直到碰到第一个空格为止。碰到第一个空格之后,把P1向前移动1格,在P2之前插入字符串"%20"。由于"%20"的长度为3,同时也要把P2向前移动3格。 691 | 692 | **参考代码** 693 | 694 | ```c++ 695 | class Solution { 696 | public: 697 | void replaceSpace(char *str,int length) { 698 | if(str == NULL || length <= 0){ 699 | return; 700 | } 701 | /*original_length为字符串str的实际长度*/ 702 | int original_length = 0; //原始长度 703 | int number_blank = 0; //空格数 704 | int i; 705 | while(str[i++] != '\0'){ //遍历字符串 706 | ++original_length; //长度+1 707 | if(str[i] == ' '){ 708 | ++number_blank; //遇到空格+1 709 | } 710 | } 711 | /*new_length为把空格替换成'%20'之后的长度*/ 712 | int new_length = original_length + 2 * number_blank; 713 | 714 | int index_original = original_length-1; //原始字符串末尾索引值 715 | int index_new = new_length-1; //计算长度后的字符串末尾索引值 716 | 717 | /*index_original指针开始向前移动,如果遇到空格,替换成'%20',否则进行复制操作*/ 718 | while(index_original >= 0 && index_new > index_original){ 719 | if(str[index_original] == ' '){ 720 | str[index_new--] = '0'; 721 | str[index_new--] = '2'; 722 | str[index_new--] = '%'; 723 | } 724 | else{ 725 | str[index_new--] = str[index_original]; 726 | } 727 | --index_original; 728 | } 729 | } 730 | }; 731 | ``` 732 | 733 | 734 | 735 | ##### 2. 043-左旋转字符串 736 | **题目描述** 737 | 738 | 汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它! 739 | 740 | **解题思路** 741 | 742 | 例如:输入字符串"abcdefg"和数字2,该函数将返回左旋转2位得到的结果"cdefgab"; 743 | 744 | 745 | 746 | 第一步:翻转字符串“ab”,得到"ba"; 747 | 748 | 第二步:翻转字符串"cdefg",得到"gfedc"; 749 | 750 | 第三步:翻转字符串"bagfedc",得到"cdefgab"; 751 | 752 | 或者: 753 | 754 | 第一步:翻转整个字符串"abcdefg",得到"gfedcba" 755 | 756 | 第二步:翻转字符串“gfedc”,得到"cdefg" 757 | 758 | 第三步:翻转字符串"ba",得到"ab" 759 | 760 | **参考代码** 761 | 762 | ```c++ 763 | class Solution { 764 | public: 765 | string LeftRotateString(string str, int n) { 766 | string result = str; 767 | int length = result.size(); 768 | if(length < 0){ 769 | return NULL; 770 | } 771 | if(0 <= n <= length){ 772 | int pFirstBegin = 0, pFirstEnd = n - 1; 773 | int pSecondBegin = n, pSecondEnd = length - 1; 774 | ReverseString(result, pFirstBegin, pFirstEnd); 775 | ReverseString(result, pSecondBegin, pSecondEnd); 776 | ReverseString(result, pFirstBegin, pSecondEnd); 777 | } 778 | return result; 779 | } 780 | private: 781 | void ReverseString(string &str, int begin, int end){ 782 | while(begin < end){ 783 | swap(str[begin++], str[end--]); 784 | } 785 | } 786 | }; 787 | ``` 788 | 789 | 790 | 791 | ##### 3. 049-把字符串转换成整数 792 | **题目描述** 793 | 794 | 将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。 795 | 796 | *输入描述:*输入一个字符串,包括数字字母符号,可以为空 797 | 798 | *输出描述:*如果是合法的数值表达则返回该数字,否则返回0 799 | 800 | **解题思路** 801 | 802 | 这道题要考虑全面,对异常值要做出处理。 803 | 804 | 对于这个题目,需要注意的要点有: 805 | 806 | - 字符串是否为空字符串; 807 | - 字符串对于正负号的处理; 808 | - 输入值是否为合法值,即小于等于'9',大于等于'0'; 809 | - int为32位,需要判断是否溢出; 810 | - 最好使用错误标志,区分合法值0和非法值0。 811 | 812 | **参考代码** 813 | 814 | ```c++ 815 | class Solution { 816 | public: 817 | int StrToInt(string str) { 818 | /* 819 | flag:标记第一位是否是负号; 820 | 821 | 首先判断第一个字符是不是正负号,如果是正负号,表明数字从str[1]开始; 822 | 另外如果是负号,要用flag记住,后面得出数字需要补充符号。 823 | */ 824 | if(str.empty()){ 825 | return 0; 826 | } 827 | int flag=0; 828 | char* c_str=&str[0]; 829 | if(*c_str=='+'||*c_str=='-'){ 830 | if(*c_str=='-'){ 831 | flag=1; 832 | } 833 | *c_str++; 834 | } 835 | int result=0; 836 | while(*c_str!='\0'){ 837 | result*=10; 838 | if(*c_str>='0'&&*c_str<='9'){ 839 | result+=int(*c_str++-'0'); 840 | if((!flag&&result>0x7fffffff)||(flag&&result<(signed int)0x80000000)){ 841 | result=0; 842 | break; 843 | } 844 | } 845 | else{ 846 | result=0; 847 | break; 848 | } 849 | } 850 | if(flag==1){ 851 | result*=-1; 852 | } 853 | return result; 854 | } 855 | }; 856 | ``` 857 | 858 | 859 | 860 | 861 | ##### 4. 053-表示数值的字符串 862 | **题目描述** 863 | 864 | 请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。 865 | 866 | **解题思路** 867 | 868 | 这道题还是比较简单的。表示数值的字符串遵循如下模式: 869 | 870 | ```makefile 871 | [sign]integral-digits[.[fractional-digits]][e|E[sign]exponential-digits] 872 | ``` 873 | 874 | 875 | 876 | 其中,('['和']'之间的为可有可无的部分)。 877 | 878 | 在数值之前可能有一个表示正负的'+'或者'-'。接下来是若干个0到9的数位表示数值的整数部分(在某些小数里可能没有数值的整数部分)。如果数值是一个小数,那么在小数后面可能会有若干个0到9的数位表示数值的小数部分。如果数值用科学记数法表示,接下来是一个'e'或者'E',以及紧跟着的一个整数(可以有正负号)表示指数。 879 | 880 | 判断一个字符串是否符合上述模式时,首先看第一个字符是不是正负号。如果是,在字符串上移动一个字符,继续扫描剩余的字符串中0到9的数位。如果是一个小数,则将遇到小数点。另外,如果是用科学记数法表示的数值,在整数或者小数的后面还有可能遇到'e'或者'E'。 881 | 882 | **参考代码** 883 | 884 | ```c++ 885 | class Solution { 886 | public: 887 | // 数字的格式可以用A[.[B]][e|EC]或者.B[e|EC]表示, 888 | // 其中A和C都是整数(可以有正负号,也可以没有) 889 | // 而B是一个无符号整数 890 | bool isNumeric(char* string){ 891 | // 非法输入 892 | if(string == NULL || *string == '\0'){ 893 | return false; 894 | } 895 | // 正负号判断 896 | if(*string == '+' || *string == '-'){ 897 | ++string; 898 | } 899 | bool numeric = true; 900 | scanDigits(&string); 901 | if(*string != '\0'){ 902 | // 小数判断 903 | if(*string == '.'){ 904 | ++string; 905 | scanDigits(&string); 906 | if(*string == 'e' || *string == 'E'){ 907 | numeric = isExponential(&string); 908 | } 909 | } 910 | // 整数判断 911 | else if(*string == 'e' || *string == 'E'){ 912 | numeric = isExponential(&string); 913 | } 914 | else{ 915 | numeric = false; 916 | } 917 | } 918 | return numeric && *string == '\0'; 919 | } 920 | private: 921 | // 扫描数字,对于合法数字,直接跳过 922 | void scanDigits(char** string){ 923 | while(**string != '\0' && **string >= '0' && **string <= '9'){ 924 | ++(*string); 925 | } 926 | } 927 | // 用来判断科学计数法表示的数值的结尾部分是否合法 928 | bool isExponential(char** string){ 929 | ++(*string); 930 | if(**string == '+' || **string == '-'){ 931 | ++(*string); 932 | } 933 | if(**string == '\0'){ 934 | return false; 935 | } 936 | scanDigits(string); 937 | // 判断是否结尾,如果没有结尾,说明还有其他非法字符串 938 | return (**string == '\0') ? true : false; 939 | } 940 | }; 941 | ``` 942 | 943 | 944 | 945 | #### 4. 栈与队列(5) 946 | 947 | ##### 结构体定义 948 | 949 | 950 | ##### 1. 005-用两个栈实现队列 951 | **题目描述** 952 | 953 | 用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。 954 | 955 | **解题思路** 956 | 957 | 创建两个栈stack1和stack2,使用两个“先进后出”的栈实现一个“先进先出”的队列。 958 | 959 | 当stack2中不为空时,在stack2中的栈顶元素是最先进入队列的元素,可以弹出。如果stack2为空时,我们把stack1中的元素逐个弹出并压入stack2。由于先进入队列的元素被压倒stack1的栈底,经过弹出和压入之后就处于stack2的栈顶,有可以直接弹出。如果有新元素d插入,我们直接把它压入stack1即可。 960 | 961 | **参考代码** 962 | 963 | ```c++ 964 | class Solution 965 | { 966 | public: 967 | void push(int node) { 968 | stack1.push(node); 969 | } 970 | 971 | int pop() { 972 | if(stack2.empty()){ 973 | while(stack1.size() > 0){ 974 | int data = stack1.top(); 975 | stack1.pop(); 976 | stack2.push(data); 977 | } 978 | } 979 | int pop_element = stack2.top(); 980 | stack2.pop(); 981 | return pop_element; 982 | } 983 | 984 | private: 985 | stack stack1; 986 | stack stack2; 987 | }; 988 | ``` 989 | 990 | 991 | 992 | ##### 2. 020-包含min函数的栈 993 | **题目描述** 994 | 995 | 定义栈的数据结构,请在类型中实现一个能够得到栈最小元素的min函数。 996 | 997 | **解题思路** 998 | 999 | 使用两个stack,一个为数据栈,另一个为辅助栈。数据栈用于存储所有数据,辅助栈用于存储最小值。 1000 | 1001 | 举个例子: 1002 | 1003 | 入栈的时候:首先往空的数据栈里压入数字3,显然现在3是最小值,我们也把最小值压入辅助栈。接下来往数据栈里压入数字4。由于4大于之前的最小值,因此我们只要入数据栈,不压入辅助栈。 1004 | 1005 | 出栈的时候:当数据栈和辅助栈的栈顶元素相同的时候,辅助栈的栈顶元素出栈。否则,数据栈的栈顶元素出栈。 1006 | 1007 | 获得栈顶元素的时候:直接返回数据栈的栈顶元素。 1008 | 1009 | 栈最小元素:直接返回辅助栈的栈顶元素。 1010 | 1011 | **参考代码** 1012 | 1013 | ```c++ 1014 | class Solution { 1015 | public: 1016 | void push(int value) { 1017 | Data.push(value); 1018 | if(Min.empty()){ 1019 | Min.push(value); 1020 | } 1021 | if(Min.top() > value){ 1022 | Min.push(value); 1023 | } 1024 | } 1025 | void pop() { 1026 | if(Data.top() == Min.top()){ 1027 | Min.pop(); 1028 | } 1029 | Data.pop(); 1030 | } 1031 | int top() { 1032 | return Data.top(); 1033 | } 1034 | int min() { 1035 | return Min.top(); 1036 | } 1037 | private: 1038 | stack Data; //数据栈 1039 | stack Min; //最小栈 1040 | }; 1041 | ``` 1042 | 1043 | 1044 | 1045 | ##### 3. 021-栈的压入、弹出序列 1046 | **题目描述** 1047 | 1048 | 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的) 1049 | 1050 | **解题思路** 1051 | 1052 | 借用一个辅助的栈,先将第一个元素放入栈中,这里是1,然后判断栈顶元素是不是出栈顺序的第一个元素,这里是4,很显然1≠4,所以我们继续压栈,直到相等以后开始出栈,出栈一个元素,则将出栈顺序向后移动一位,直到不相等,这样循环等压栈顺序遍历完成,如果辅助栈还不为空,说明弹出序列不是该栈的弹出顺序。 1053 | 1054 | **参考代码** 1055 | 1056 | ```c++ 1057 | class Solution { 1058 | public: 1059 | bool IsPopOrder(vector pushV,vector popV) { 1060 | if(pushV.size() == 0){ 1061 | return false; 1062 | } 1063 | for(int i = 0, j = 0; i < pushV.size();i++){ 1064 | stackData.push(pushV[i]); 1065 | //注意此处j stackData; 1075 | }; 1076 | ``` 1077 | 1078 | 1079 | 1080 | ##### 4.044-翻转单词顺序列(栈) 1081 | **题目描述** 1082 | 1083 | 牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么? 1084 | 1085 | **解题思路** 1086 | 1087 | 观察字符串变化规律,你会发现这道题很简单。只需要对每个单词做翻转,然后再整体做翻转就得到了正确的结果。 1088 | 1089 | **参考代码** 1090 | 1091 | ```c++ 1092 | class Solution { 1093 | public: 1094 | string ReverseSentence(string str) { 1095 | string result = str; 1096 | int length = result.size(); 1097 | if(length == 0){ 1098 | return ""; 1099 | } 1100 | // 追加一个空格,作为反转标志位 1101 | result += ' '; 1102 | int mark = 0; 1103 | // 根据空格,反转所有单词 1104 | for(int i = 0; i < length + 1; i++){ 1105 | if(result[i] == ' '){ 1106 | Reverse(result, mark, i - 1); 1107 | mark = i + 1; 1108 | } 1109 | } 1110 | // 去掉添加的空格 1111 | result = result.substr(0, length); 1112 | // 整体反转 1113 | Reverse(result, 0, length - 1); 1114 | 1115 | return result; 1116 | } 1117 | private: 1118 | void Reverse(string &str, int begin, int end){ 1119 | while(begin < end){ 1120 | swap(str[begin++], str[end--]); 1121 | } 1122 | } 1123 | }; 1124 | ``` 1125 | 1126 | 1127 | 1128 | ##### 5. 064-滑动窗口的最大值(双端队列) 1129 | **题目描述** 1130 | 1131 | 给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。 1132 | 1133 | ![basis_64_1](assets/basis_64_1.png) 1134 | 1135 | **解题思路** 1136 | 1137 | 使用一个双端队列,用STL中的deque来实现。 1138 | 1139 | 接下来我们以数组{2,3,4,2,6,2,5,1}为例,来细说整体思路。 1140 | 1141 | 数组的第一个数字是2,把它存入队列中。第二个数字是3,比2大,所以2不可能是滑动窗口中的最大值,因此把2从队列里删除,再把3存入队列中。第三个数字是4,比3大,同样的删3存4。此时滑动窗口中已经有3个数字,而它的最大值4位于队列的头部。 1142 | 1143 | 第四个数字2比4小,但是当4滑出之后它还是有可能成为最大值的,所以我们把2存入队列的尾部。下一个数字是6,比4和2都大,删4和2,存6。就这样依次进行,最大值永远位于队列的头部。 1144 | 1145 | 但是我们怎样判断滑动窗口是否包括一个数字?应该在队列里存入数字在数组里的下标,而不是数值。当一个数字的下标与当前处理的数字的下标之差大于或者相等于滑动窗口大小时,这个数字已经从窗口中滑出,可以从队列中删除。 1146 | 1147 | 整体过程示意图: 1148 | 1149 | ![basis_64_2](assets/basis_64_2.png) 1150 | 1151 | **参考代码** 1152 | 1153 | ```c++ 1154 | class Solution { 1155 | public: 1156 | vector maxInWindows(const vector& num, unsigned int size) 1157 | { 1158 | vector maxInWindows; 1159 | // 数组大小要大于等于窗口大小,并且窗口大小大于等于1 1160 | if(num.size() >= size && size >= 1){ 1161 | deque index; 1162 | for(unsigned int i = 0; i < size; i++){ 1163 | // 如果index非空,并且新添加的数字大于等于队列中最小的数字,则删除队列中最小的数字 1164 | while(!index.empty() && num[i] >= num[index.back()]){ 1165 | index.pop_back(); 1166 | } 1167 | index.push_back(i); 1168 | } 1169 | for(unsigned int i = size; i < num.size(); i++){ 1170 | maxInWindows.push_back(num[index.front()]); 1171 | while(!index.empty() && num[i] >= num[index.back()]){ 1172 | index.pop_back(); 1173 | } 1174 | // 控制窗口大小为size 1175 | if(!index.empty() && index.front() <= int(i - size)){ 1176 | index.pop_front(); 1177 | } 1178 | index.push_back(i); 1179 | } 1180 | maxInWindows.push_back(num[index.front()]); 1181 | } 1182 | return maxInWindows; 1183 | } 1184 | }; 1185 | ``` 1186 | 1187 | 1188 | 1189 | #### 5. 二叉树(12) 1190 | 1191 | ##### 结构体定义 1192 | 1193 | ```c++ 1194 | Definition for binary tree 1195 | struct TreeNode { 1196 | int val; 1197 | TreeNode *left; 1198 | TreeNode *right; 1199 | TreeNode(int x) : val(x), left(NULL), right(NULL) {} 1200 | }; 1201 | ``` 1202 | 1203 | 1204 | 1205 | 1206 | ##### 1. 004-重建二叉树 1207 | **题目描述** 1208 | 1209 | 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。 1210 | 1211 | **解题思路** 1212 | 1213 | 通常树有如下几种遍历方式: 1214 | 1215 | - 前序遍历:先访问根结点,再访问左子结点,最后访问右子结点。 1216 | - 中序遍历:先访问左子结点,再访问根结点,最后访问右子结点。 1217 | - 后序遍历:先访问左子结点,再访问右子结点,最后访问根结点。 1218 | 1219 | 本题为前序遍历和中序遍历,最少需要两种遍历方式,才能重建二叉树。 1220 | 1221 | 前序遍历序列中,第一个数字总是树的根结点的值。在中序遍历序列中,根结点的值在序列的中间,左子树的结点的值位于根结点的值的左边,而右子树的结点的值位于根结点的值的右边。剩下的我们可以递归来实现. 1222 | 1223 | **参考代码** 1224 | 1225 | ```c++ 1226 | class Solution { 1227 | public: 1228 | TreeNode* reConstructBinaryTree(vector pre,vector vin) { 1229 | if(pre.size() == 0){ //如果为空,返回NULL 1230 | return NULL; 1231 | } 1232 | //依次是前序遍历左子树,前序遍历右子树,中序遍历左子树,中序遍历右子树 1233 | vector left_pre, right_pre, left_vin, right_vin; 1234 | //中序遍历第一个节点一定为根节点 1235 | TreeNode* head = new TreeNode(pre[0]); 1236 | //找到中序遍历的根节点 1237 | int root = 0; 1238 | //遍历找到中序遍历根节点索引值 1239 | for(int i = 0; i < pre.size(); i++){ 1240 | if(pre[0] == vin[i]){ 1241 | root = i; 1242 | break; 1243 | } 1244 | } 1245 | //利用中序遍历的根节点,对二叉树节点进行归并 1246 | for(int i = 0; i < root; i++){ 1247 | left_vin.push_back(vin[i]); 1248 | left_pre.push_back(pre[i + 1]); //前序遍历第一个为根节点 1249 | } 1250 | 1251 | for(int i = root + 1; i < pre.size(); i++){ 1252 | right_vin.push_back(vin[i]); 1253 | right_pre.push_back(pre[i]); 1254 | } 1255 | 1256 | //递归,再对其进行上述所有步骤,即再区分子树的左、右子子数,直到叶节点 1257 | head->left = reConstructBinaryTree(left_pre, left_vin); 1258 | head->right = reConstructBinaryTree(right_pre, right_vin); 1259 | return head; 1260 | } 1261 | }; 1262 | ``` 1263 | 1264 | 1265 | 1266 | ##### 2. 017-树的子结构 1267 | **题目描述** 1268 | 1269 | 输入两颗二叉树A,B,判断B是不是A的子结构。(PS:我们约定空树不是任意一个树的子结构)。 1270 | 1271 | **解题思路** 1272 | 1273 | 要查找树A中是否存在和树B结构一样的子树,我们可以分为两步:第一步在树A中找到和B的根结点的值一样的结点R,第二步再判断树A中以R为根节点的子树是不是包含和树B一样的结构。 1274 | 1275 | 这里使用递归的方法即可。 1276 | 1277 | **参考代码** 1278 | 1279 | ```c++ 1280 | class Solution { 1281 | public: 1282 | bool HasSubtree(TreeNode* pRoot1, TreeNode* pRoot2) 1283 | { 1284 | bool result = false; 1285 | if(pRoot1 != NULL && pRoot2 != NULL){ 1286 | if(pRoot1->val == pRoot2->val){ 1287 | result = DoesTree1HasTree2(pRoot1, pRoot2); 1288 | } 1289 | if(!result){ 1290 | result = HasSubtree(pRoot1->left, pRoot2); 1291 | } 1292 | if(!result){ 1293 | result = HasSubtree(pRoot1->right, pRoot2); 1294 | } 1295 | } 1296 | return result; 1297 | } 1298 | private: 1299 | bool DoesTree1HasTree2(TreeNode* pRoot1, TreeNode* pRoot2){ 1300 | if(pRoot2 == NULL){ 1301 | return true; 1302 | } 1303 | if(pRoot1 == NULL){ 1304 | return false; 1305 | } 1306 | if(pRoot1->val != pRoot2->val){ 1307 | return false; 1308 | } 1309 | return DoesTree1HasTree2(pRoot1->left, pRoot2->left) && DoesTree1HasTree2(pRoot1->right, pRoot2->right); 1310 | } 1311 | }; 1312 | ``` 1313 | 1314 | 1315 | 1316 | ##### 3. 018-二叉树的镜像 1317 | **题目描述** 1318 | 1319 | 操作给定的二叉树,将其变换为源二叉树的镜像。 1320 | 1321 | **解题思路** 1322 | 1323 | 先交换根节点的两个子结点之后,我们注意到值为10、6的结点的子结点仍然保持不变,因此我们还需要交换这两个结点的左右子结点。做完这两次交换之后,我们已经遍历完所有的非叶结点。此时变换之后的树刚好就是原始树的镜像。 1324 | 1325 | **参考代码** 1326 | 1327 | ```c++ 1328 | class Solution { 1329 | public: 1330 | void Mirror(TreeNode *pRoot) { 1331 | if((pRoot == NULL) || (pRoot->left == NULL && pRoot->right == NULL)){ 1332 | return; 1333 | } 1334 | 1335 | //交换根节点的左右结点 1336 | TreeNode *pTemp = pRoot->left; 1337 | pRoot->left = pRoot->right; 1338 | pRoot->right = pTemp; 1339 | 1340 | //递归左子树 1341 | if(pRoot->left){ 1342 | Mirror(pRoot->left); 1343 | } 1344 | //递归右子树 1345 | if(pRoot->right){ 1346 | Mirror(pRoot->right); 1347 | } 1348 | } 1349 | }; 1350 | ``` 1351 | 1352 | 1353 | 1354 | ##### 4. 022-从上往下打印二叉树 1355 | **题目描述** 1356 | 1357 | 从上往下打印出二叉树的每个节点,同层节点从左至右打印。 1358 | 1359 | **解题思路** 1360 | 1361 | 层次遍历,队列实现 1362 | 1363 | **参考代码** 1364 | 1365 | ```c++ 1366 | class Solution { 1367 | public: 1368 | vector PrintFromTopToBottom(TreeNode* root) { 1369 | TreeNode* fr; 1370 | if(root == NULL){ 1371 | return result; 1372 | } 1373 | que.push(root); 1374 | while(!que.empty()){ 1375 | fr = que.front(); 1376 | result.push_back(fr->val); 1377 | if(fr->left != NULL){ 1378 | que.push(fr->left); 1379 | } 1380 | if(fr->right != NULL){ 1381 | que.push(fr->right); 1382 | } 1383 | que.pop(); 1384 | } 1385 | return result; 1386 | } 1387 | private: 1388 | vector result; 1389 | queue que; 1390 | }; 1391 | ``` 1392 | 1393 | 1394 | 1395 | ##### 5. 024-二叉树中和为某一值的路径 1396 | 1397 | **题目描述** 1398 | 1399 | 输入一颗二叉树和一个整数,打印出二叉树中结点值的和为输入整数的所有路径。路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路径。 1400 | 1401 | **解题思路** 1402 | 1403 | 深度优先搜索。使用前序遍历,使用两个全局变量result和tmp,result来存放最终结果,tmp用来存放临时结果。 1404 | 1405 | 每次遍历,我们先把root的值压入tmp,然后判断当前root是否同时满足: 1406 | 1407 | - 与给定数值相减为0; 1408 | - 左子树为空; 1409 | - 右子树为空。 1410 | 1411 | 如果满足条件,就将tmp压入result中,否则,依次遍历左右子树。需要注意的是,遍历左右子树的时候,全局变量tmp是不清空的,直到到了根结点才请空tmp。 1412 | 1413 | **参考代码** 1414 | 1415 | ```c++ 1416 | class Solution { 1417 | public: 1418 | vector > FindPath(TreeNode* root,int expectNumber){ 1419 | if(root == NULL){ 1420 | return result; 1421 | } 1422 | 1423 | tmp.push_back(root->val); 1424 | if((expectNumber - root->val ) == 0 && root->left == NULL && root->right == NULL){ 1425 | result.push_back(tmp); 1426 | } 1427 | 1428 | //遍历左子树 1429 | FindPath(root->left, expectNumber - root->val); 1430 | //遍历右子树 1431 | FindPath(root->right, expectNumber - root->val); 1432 | 1433 | tmp.pop_back(); 1434 | return result; 1435 | } 1436 | private: 1437 | vector > result; 1438 | vector tmp; 1439 | }; 1440 | ``` 1441 | 1442 | 1443 | 1444 | 1445 | ##### 6. 038-二叉树的深度 1446 | **题目描述** 1447 | 1448 | 输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。 1449 | 1450 | **解题思路** 1451 | 1452 | 递归即可 1453 | 1454 | **参考代码** 1455 | 1456 | ```c++ 1457 | class Solution { 1458 | public: 1459 | int TreeDepth(TreeNode* pRoot) 1460 | { 1461 | if(pRoot == NULL){ 1462 | return 0; 1463 | } 1464 | int left = TreeDepth(pRoot->left); 1465 | int right = TreeDepth(pRoot->right); 1466 | return (left > right) ? (left + 1) : (right + 1); 1467 | } 1468 | }; 1469 | ``` 1470 | 1471 | 1472 | 1473 | ##### 7. 039-平衡二叉树 1474 | **题目描述** 1475 | 1476 | 输入一棵二叉树,判断该二叉树是否是平衡二叉树。 1477 | 1478 | **解题思路** 1479 | 1480 | 后序遍历,利用6-038中的深度即可 1481 | 1482 | **参考代码** 1483 | 1484 | ```c++ 1485 | class Solution { 1486 | public: 1487 | bool IsBalanced_Solution(TreeNode* pRoot) { 1488 | if(pRoot == NULL){ 1489 | return true; 1490 | } 1491 | int left = TreeDepth(pRoot->left); 1492 | int right = TreeDepth(pRoot->right); 1493 | int diff = left - right; 1494 | if(diff > 1 || diff < -1){ 1495 | return false; 1496 | } 1497 | return IsBalanced_Solution(pRoot->right) && IsBalanced_Solution(pRoot->left); 1498 | } 1499 | private: 1500 | int TreeDepth(TreeNode* pRoot) 1501 | { 1502 | if(pRoot == NULL){ 1503 | return 0; 1504 | } 1505 | int left = TreeDepth(pRoot->left); 1506 | int right = TreeDepth(pRoot->right); 1507 | return (left > right) ? (left + 1) : (right + 1); 1508 | } 1509 | }; 1510 | ``` 1511 | 1512 | 1513 | 1514 | ##### 8. 057-二叉树的下一个结点 1515 | **题目描述** 1516 | 1517 | 给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 1518 | 1519 | **解题思路** 1520 | 1521 | ![basis_57_1](assets/basis_57_1.png) 1522 | 1523 | 以上图为例进行讲解,上图二叉树的中序遍历是d,b,h,e,i,a,f,c,g。我们以这棵树为例来分析如何找出二叉树的下一个结点。 1524 | 1525 | 如果一个结点有右子树,那么它的下一个结点就是它的右子树的最左子结点。也就是说从右子结点出发一直沿着指向左子树结点的指针,我们就能找到它的下一个结点。例如,图中结点b的下一个结点是h,结点a的下一个结点是f。 1526 | 1527 | 接着我们分析一下结点没有右子树的情形。如果结点是它父结点的左子结点,那么它的下一个结点就是它的父结点。例如,途中结点d的下一个结点是b,f的下一个结点是c。 1528 | 1529 | 如果一个结点既没有右子树,并且它还是父结点的右子结点,这种情形就比较复杂。我们可以沿着指向父结点的指针一直向上遍历,直到找到一个是它父结点的左子结点的结点。如果这样的结点存在,那么这个结点的父结点就是我们要找的下一个结点。例如,为了找到结点g的下一个结点,我们沿着指向父结点的指针向上遍历,先到达结点c。由于结点c是父结点a的右结点,我们继续向上遍历到达结点a。由于结点a是树的根结点。它没有父结点。因此结点g没有下一个结点。 1530 | 1531 | **参考代码** 1532 | 1533 | ```c++ 1534 | class Solution { 1535 | public: 1536 | TreeLinkNode* GetNext(TreeLinkNode* pNode) 1537 | { 1538 | if(pNode == NULL){ 1539 | return NULL; 1540 | } 1541 | TreeLinkNode* pNext = NULL; 1542 | // 当前结点有右子树,那么它的下一个结点就是它的右子树中最左子结点 1543 | if(pNode->right != NULL){ 1544 | TreeLinkNode* pRight = pNode->right; 1545 | while(pRight->left != NULL){ 1546 | pRight = pRight-> left; 1547 | } 1548 | pNext = pRight; 1549 | } 1550 | // 当前结点无右子树,则需要找到一个是它父结点的左子树结点的结点 1551 | else if(pNode->next != NULL){ 1552 | // 当前结点 1553 | TreeLinkNode* pCur = pNode; 1554 | // 父节点 1555 | TreeLinkNode* pPar = pNode->next; 1556 | while(pPar != NULL && pCur == pPar->right){ 1557 | pCur = pPar; 1558 | pPar = pCur->next; 1559 | } 1560 | pNext = pPar; 1561 | } 1562 | return pNext; 1563 | } 1564 | }; 1565 | ``` 1566 | 1567 | 1568 | 1569 | ##### 9. 058-对称的二叉树 1570 | **题目描述** 1571 | 1572 | 请实现一个函数,用来判断一颗二叉树是不是对称的。注意,如果一个二叉树同此二叉树的镜像是同样的,定义其为对称的。 1573 | 1574 | **解题思路** 1575 | 1576 | ![basis_58_1](assets/basis_58_1.png) 1577 | 1578 | 我们通常有三种不同的二叉树遍历算法,即前序遍历、中序遍历和后序遍历。在这三种遍历算法中,都是先遍历左子结点再遍历右子结点。以前序遍历为例,我们可以定义一个遍历算法,先遍历右子结点再遍历左子结点,暂且称其为前序遍历的对称遍历。 1579 | 1580 | 遍历第一棵树,前序遍历的遍历序列为{8,6,5,7,6,7,5},其对称遍历的遍历序列为{8,6,5,7,6,7,5}。 1581 | 1582 | 遍历第二颗树,前序遍历的遍历序列为{8,6,5,7,9,7,5},其对称遍历的遍历序列为{8,9,5,7,6,7,5}。 1583 | 1584 | 可以看到,使用此方法可以区分前两棵树,第一棵树为对称树,第二颗树不是对称树。但是当使用此方法,你会发现第三颗树的前序遍历和对称前序遍历的遍历序列是一样的。 1585 | 1586 | 怎么区分第三颗树呢?解决办法就是我们也要考虑NULL指针。此时,前序遍历的遍历序列{7,7,7,NULL,NULL,7,NULL,NULL,7,7,NLL,NULL,NULL},其对称遍历的遍历序列为{7,7,NULL,7,NULL,NULL,7,7,NULL,NULL,7,NULL,NULL}。因为两种遍历的序列不同,因此这棵树不是对称树。 1587 | 1588 | **参考代码** 1589 | 1590 | ```c++ 1591 | class Solution { 1592 | public: 1593 | bool isSymmetrical(TreeNode* pRoot) 1594 | { 1595 | if(pRoot == NULL){ 1596 | return true; 1597 | } 1598 | return isSymmetriacalCor(pRoot, pRoot); 1599 | } 1600 | private: 1601 | bool isSymmetriacalCor(TreeNode* pRoot1, TreeNode* pRoot2){ 1602 | if(pRoot1 == NULL && pRoot2 == NULL){ 1603 | return true; 1604 | } 1605 | if(pRoot1 == NULL || pRoot2 == NULL){ 1606 | return false; 1607 | } 1608 | if(pRoot1->val != pRoot2->val){ 1609 | return false; 1610 | } 1611 | return isSymmetriacalCor(pRoot1->left, pRoot2->right) && isSymmetriacalCor(pRoot1->right, pRoot2->left); 1612 | } 1613 | }; 1614 | ``` 1615 | 1616 | 1617 | 1618 | ##### 10. 059-按之字形顺序打印二叉树 1619 | **题目描述** 1620 | 1621 | 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推。 1622 | 1623 | **解题思路** 1624 | 1625 | 为了达到这样打印的效果,我们需要使用两个栈。我们在打印某一行结点时,把下一层的子结点保存到相应的栈里。如果当前打印的是奇数层(第一层、第三层等),则先保存左子树结点再保存右子树结点到第一个栈里。如果当前打印的是偶数层(第二层、第四层等),则则先保存右子树结点再保存左子树结点到第二个栈里。 1626 | 1627 | **参考代码** 1628 | 1629 | ```c++ 1630 | class Solution { 1631 | public: 1632 | vector > Print(TreeNode* pRoot) { 1633 | vector > result; 1634 | if(pRoot == NULL){ 1635 | return result; 1636 | } 1637 | stack s[2]; 1638 | s[0].push(pRoot); 1639 | while(!s[0].empty() || !s[1].empty()){ 1640 | vector v[2]; 1641 | // 偶数行 1642 | while(!s[0].empty()){ 1643 | v[0].push_back(s[0].top()->val); 1644 | if(s[0].top()->left != NULL){ 1645 | s[1].push(s[0].top()->left); 1646 | } 1647 | if(s[0].top()->right != NULL){ 1648 | s[1].push(s[0].top()->right); 1649 | } 1650 | s[0].pop(); 1651 | } 1652 | if(!v[0].empty()){ 1653 | result.push_back(v[0]); 1654 | } 1655 | // 奇数行 1656 | while(!s[1].empty()){ 1657 | v[1].push_back(s[1].top()->val); 1658 | if(s[1].top()->right != NULL){ 1659 | s[0].push(s[1].top()->right); 1660 | } 1661 | if(s[1].top()->left != NULL){ 1662 | s[0].push(s[1].top()->left); 1663 | } 1664 | s[1].pop(); 1665 | } 1666 | if(!v[1].empty()){ 1667 | result.push_back(v[1]); 1668 | } 1669 | } 1670 | return result; 1671 | } 1672 | }; 1673 | ``` 1674 | 1675 | 1676 | 1677 | ##### 11. 060-把二叉树打印成多行 1678 | **题目描述** 1679 | 1680 | 从上到下按层打印二叉树,同一层结点从左至右输出。每一层输出一行。 1681 | 1682 | **解题思路** 1683 | 1684 | 思路和上一道题一样,区别在于,这把是先入先出,使用队列即可。 1685 | 1686 | **参考代码** 1687 | 1688 | ```c++ 1689 | class Solution { 1690 | public: 1691 | vector > Print(TreeNode* pRoot) { 1692 | vector > result; 1693 | if(pRoot == NULL){ 1694 | return result; 1695 | } 1696 | queue nodes[2]; 1697 | nodes[0].push(pRoot); 1698 | while(!nodes[0].empty() || !nodes[1].empty()){ 1699 | vector v[2]; 1700 | while(!nodes[0].empty()){ 1701 | v[0].push_back(nodes[0].front()->val); 1702 | if(nodes[0].front()->left != NULL){ 1703 | nodes[1].push(nodes[0].front()->left); 1704 | } 1705 | if(nodes[0].front()->right != NULL){ 1706 | nodes[1].push(nodes[0].front()->right); 1707 | } 1708 | nodes[0].pop(); 1709 | } 1710 | if(!v[0].empty()){ 1711 | result.push_back(v[0]); 1712 | } 1713 | while(!nodes[1].empty()){ 1714 | v[1].push_back(nodes[1].front()->val); 1715 | if(nodes[1].front()->left != NULL){ 1716 | nodes[0].push(nodes[1].front()->left); 1717 | } 1718 | if(nodes[1].front()->right != NULL){ 1719 | nodes[0].push(nodes[1].front()->right); 1720 | } 1721 | nodes[1].pop(); 1722 | } 1723 | if(!v[1].empty()){ 1724 | result.push_back(v[1]); 1725 | } 1726 | } 1727 | return result; 1728 | } 1729 | }; 1730 | ``` 1731 | 1732 | 1733 | 1734 | ##### 12. 061-序列化二叉树 1735 | **题目描述** 1736 | 1737 | 请实现两个函数,分别用来序列化和反序列化二叉树。 1738 | 1739 | **解题思路** 1740 | 1741 | 使用前序遍历来序列化和反序列化即可。可以使用$符号表示NULL,同时每个结点之间,需要添加逗号,即','进行分隔。 1742 | 1743 | **参考代码** 1744 | 1745 | ```c++ 1746 | class Solution { 1747 | public: 1748 | char* Serialize(TreeNode *root) { 1749 | if(!root){ 1750 | return NULL; 1751 | } 1752 | string str; 1753 | SerializeCore(root, str); 1754 | // 把str流中转换为字符串返回 1755 | int length = str.length(); 1756 | char* res = new char[length+1]; 1757 | // 把str流中转换为字符串返回 1758 | for(int i = 0; i < length; i++){ 1759 | res[i] = str[i]; 1760 | } 1761 | res[length] = '\0'; 1762 | return res; 1763 | } 1764 | TreeNode* Deserialize(char *str) { 1765 | if(!str){ 1766 | return NULL; 1767 | } 1768 | TreeNode* res = DeserializeCore(&str); 1769 | return res; 1770 | } 1771 | void SerializeCore(TreeNode* root, string& str){ 1772 | // 如果指针为空,表示左子节点或右子节点为空,则在序列中用#表示 1773 | if(!root){ 1774 | str += '#'; 1775 | return; 1776 | } 1777 | string tmp = to_string(root->val); 1778 | str += tmp; 1779 | // 加逗号,用于区分每个结点 1780 | str += ','; 1781 | SerializeCore(root->left, str); 1782 | SerializeCore(root->right, str); 1783 | } 1784 | // 递归时改变了str值使其指向后面的序列,因此要声明为char** 1785 | TreeNode* DeserializeCore(char** str){ 1786 | // 到达叶节点时,调用两次,都返回null,所以构建完毕,返回父节点的构建 1787 | if(**str == '#'){ 1788 | (*str)++; 1789 | return NULL; 1790 | } 1791 | // 因为整数是用字符串表示,一个字符表示一位,先进行转换 1792 | int num = 0; 1793 | while(**str != ',' && **str != '\0'){ 1794 | num = num * 10 + ((**str) - '0'); 1795 | (*str)++; 1796 | } 1797 | TreeNode* root = new TreeNode(num); 1798 | if(**str == '\0'){ 1799 | return root; 1800 | } 1801 | else{ 1802 | (*str)++; 1803 | } 1804 | root->left = DeserializeCore(str); 1805 | root->right = DeserializeCore(str); 1806 | return root; 1807 | } 1808 | }; 1809 | ``` 1810 | 1811 | 1812 | 1813 | #### 6. 二叉搜索树(3) 1814 | 1815 | ##### 结构体定义 1816 | 1817 | ##### 1. 023-二叉搜索树的后序遍历序列 1818 | 1819 | **题目描述** 1820 | 1821 | 输入一个整数数组,判断该数组是不是某二叉搜索树的后序遍历的结果。如果是则输出Yes,否则输出No。假设输入的数组的任意两个数字都互不相同。 1822 | 1823 | **解题思路** 1824 | 1825 | ![basis_23_1](assets/basis_23_1.jpg) 1826 | 1827 | 以{5,7,6,9,11,10,8}为例,后序遍历结果的最后一个数字8就是根结点的值。在这个数组中,前3个数字5、7和6都比8小,是值为8的结点的左子树结点;后3个数字9、11和10都比8大,是值为8的结点的右子树结点。 1828 | 1829 | 我们接下来用同样的方法确定与数组每一部分对应的子树的结构。这其实就是一个递归的过程。对于序列5、7、6,最后一个数字6是左子树的根结点的值。数字5比6小,是值为6的结点的左子结点,而7则是它的右子结点。同样,在序列9、11、10中,最后一个数字10是右子树的根结点,数字9比10小,是值为10的结点的左子结点,而11则是它的右子结点。 1830 | 1831 | 我们使用递归的方法,先判断数组的左子树和右子树的位置,然后再判断左子树、右子树是不是二叉搜索树。 1832 | 1833 | **参考代码** 1834 | 1835 | ```c++ 1836 | class Solution { 1837 | public: 1838 | bool VerifySquenceOfBST(vector sequence) { 1839 | return bst(sequence, 0, sequence.size() - 1); 1840 | } 1841 | private: 1842 | bool bst(vector seq, int begin, int end){ 1843 | if(seq.empty() || begin > end){ 1844 | return false; 1845 | } 1846 | 1847 | //根结点 1848 | int root = seq[end]; 1849 | 1850 | //在二叉搜索树中左子树的结点小于根结点 1851 | int i = begin; 1852 | for(; i < end; ++i){ 1853 | if(seq[i] > root){ 1854 | break; 1855 | } 1856 | } 1857 | 1858 | //在二叉搜索书中右子树的结点大于根结点 1859 | for(int j = i; j < end; ++j){ 1860 | if(seq[j] < root){ 1861 | return false; 1862 | } 1863 | } 1864 | 1865 | //判断左子树是不是二叉搜索树 1866 | bool left = true; 1867 | if(i > begin){ 1868 | left = bst(seq, begin, i - 1); 1869 | } 1870 | 1871 | //判断右子树是不是二叉搜索树 1872 | bool right = true; 1873 | if(i < end - 1){ 1874 | right = bst(seq, i , end - 1); 1875 | } 1876 | 1877 | return left && right; 1878 | } 1879 | }; 1880 | ``` 1881 | 1882 | 1883 | 1884 | ##### 2. 026-二叉搜索树与双向链表 1885 | **题目描述** 1886 | 1887 | 输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。 1888 | 1889 | **解题思路** 1890 | 1891 | ![basis_26_1](assets/basis_26_1.jpg) 1892 | 1893 | 二叉搜索树如上图所示,我们将其转换为配需双向链表。 1894 | 1895 | 根据二叉搜索树的特点:左结点的值<根结点的值<右结点的值,我们不难发现,使用二叉树的中序遍历出来的数据的数序,就是排序的顺序。因此,首先,确定了二叉搜索树的遍历方法。 1896 | 1897 | 接下来,我们看下图,我们可以把树分成三个部分:值为10的结点、根结点为6的左子树、根结点为14的右子树。根据排序双向链表的定义,值为10的结点将和它的左子树的最大一个结点链接起来,同时它还将和右子树最小的结点链接起来。 1898 | 1899 | ![basis_26_3](assets/basis_26_3.jpg) 1900 | 1901 | 按照中序遍历的顺序,当我们遍历到根结点时,它的左子树已经转换成一个排序的好的双向链表了,并且处在链表中最后一个的结点是当前值最大的结点。我们把值为8的结点和根结点链接起来,10就成了最后一个结点,接着我们就去遍历右子树,并把根结点和右子树中最小的结点链接起来。 1902 | 1903 | **参考代码** 1904 | 1905 | ```c++ 1906 | class Solution { 1907 | public: 1908 | TreeNode* Convert(TreeNode* pRootOfTree) 1909 | { 1910 | //用于记录双向链表尾结点 1911 | TreeNode* pLastNodeInList = NULL; 1912 | 1913 | //开始转换结点 1914 | ConvertNode(pRootOfTree, &pLastNodeInList); 1915 | 1916 | //pLastNodeInList指向双向链表的尾结点,我们需要重新返回头结点 1917 | TreeNode* pHeadOfList = pLastNodeInList; 1918 | while(pHeadOfList != NULL && pHeadOfList->left != NULL){ 1919 | pHeadOfList = pHeadOfList->left; 1920 | } 1921 | return pHeadOfList; 1922 | } 1923 | 1924 | void ConvertNode(TreeNode* pNode, TreeNode** pLastNodeInList){ 1925 | //叶结点直接返回 1926 | if(pNode == NULL){ 1927 | return; 1928 | } 1929 | TreeNode* pCurrent = pNode; 1930 | //递归左子树 1931 | if(pCurrent->left != NULL) 1932 | ConvertNode(pCurrent->left, pLastNodeInList); 1933 | 1934 | //左指针 1935 | pCurrent->left = *pLastNodeInList; 1936 | //右指针 1937 | if(*pLastNodeInList != NULL){ 1938 | (*pLastNodeInList)->right = pCurrent; 1939 | } 1940 | //更新双向链表尾结点 1941 | *pLastNodeInList = pCurrent; 1942 | //递归右子树 1943 | if(pCurrent->right != NULL){ 1944 | ConvertNode(pCurrent->right, pLastNodeInList); 1945 | } 1946 | } 1947 | }; 1948 | ``` 1949 | 1950 | 1951 | 1952 | ##### 3. 062-二叉搜索树的第k个结点 1953 | **题目描述** 1954 | 1955 | 给定一棵二叉搜索树,请找出其中的第k小的结点。例如, (5,3,7,2,4,6,8) 中,按结点数值大小顺序第三小结点的值为4。 1956 | 1957 | **解题思路** 1958 | 1959 | 这棵树是二叉搜索树,首先想到的是二叉搜索树的一个特点:左子结点的值 < 根结点的值 < 右子结点的值。 1960 | 1961 | 如果使用中序遍历,则得到的序列式为{2,3,4,5,6,7,8}。因此,只需要用中序遍历一棵二叉搜索树,就很容易找出它的第k大结点 1962 | 1963 | **参考代码** 1964 | 1965 | ```c++ 1966 | class Solution { 1967 | public: 1968 | TreeNode* KthNode(TreeNode* pRoot, int k) 1969 | { 1970 | if(pRoot == NULL || k == 0){ 1971 | return NULL; 1972 | } 1973 | return KthNodeCore(pRoot, k); 1974 | } 1975 | private: 1976 | TreeNode* KthNodeCore(TreeNode* pRoot, int &k){ 1977 | TreeNode* target = NULL; 1978 | // 先遍历左结点 1979 | if(pRoot->left != NULL){ 1980 | target = KthNodeCore(pRoot->left, k); 1981 | } 1982 | // 如果没有找到target,则继续减小k,如果k等于1,说明到了第k大的数 1983 | if(target == NULL){ 1984 | if(k == 1){ 1985 | target = pRoot; 1986 | } 1987 | k--; 1988 | } 1989 | // 如果没有找到target,继续找右结点 1990 | if(pRoot->right != NULL && target == NULL){ 1991 | target = KthNodeCore(pRoot->right, k); 1992 | } 1993 | return target; 1994 | } 1995 | }; 1996 | ``` 1997 | 1998 | 1999 | 2000 | #### 7.Hash Table 2001 | 2002 | ##### 结构体定义 2003 | 2004 | 2005 | ##### 1. 034-第一个只出现一次的字符 2006 | **题目描述** 2007 | 2008 | 在一个字符串(1<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置。 2009 | 2010 | **解题思路** 2011 | 2012 | 建立一个哈希表,第一次扫描的时候,统计每个字符的出现次数。第二次扫描的时候,如果该字符出现的次数为1,则返回这个字符的位置。 2013 | 2014 | **参考代码** 2015 | 2016 | ```c++ 2017 | class Solution { 2018 | public: 2019 | int FirstNotRepeatingChar(string str) { 2020 | int length = str.size(); 2021 | if(length == 0){ 2022 | return -1; 2023 | } 2024 | map item; 2025 | for(int i = 0; i < length; i++){ 2026 | item[str[i]]++; 2027 | } 2028 | for(int i = 0; i < length; i++){ 2029 | if(item[str[i]] == 1){ 2030 | return i; 2031 | } 2032 | } 2033 | return -1; 2034 | } 2035 | }; 2036 | ``` 2037 | 2038 | 2039 | 2040 | ##### 2. 054-字符流中第一个不重复的字符 2041 | **题目描述** 2042 | 2043 | 请实现一个函数用来找出字符流中第一个只出现一次的字符。例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g"。当从该字符流中读出前六个字符“google"时,第一个只出现一次的字符是"l"。 2044 | *输出描述:* 2045 | 如果当前字符流没有存在出现一次的字符,返回#字符。 2046 | 2047 | **解题思路** 2048 | 2049 | 将字节流保存起来,通过哈希表统计字符流中每个字符出现的次数,顺便将字符流保存在string中,然后再遍历string,从哈希表中找到第一个出现一次的字符。 2050 | 2051 | **参考代码** 2052 | 2053 | ```c++ 2054 | class Solution 2055 | { 2056 | public: 2057 | //Insert one char from stringstream 2058 | void Insert(char ch) 2059 | { 2060 | s += ch; 2061 | count[ch]++; 2062 | } 2063 | //return the first appearence once char in current stringstream 2064 | char FirstAppearingOnce() 2065 | { 2066 | int length = s.size(); 2067 | for(int i = 0; i < length; i++){ 2068 | if(count[s[i]] == 1){ 2069 | return s[i]; 2070 | } 2071 | } 2072 | return '#'; 2073 | } 2074 | private: 2075 | string s; 2076 | int count[256] = {0}; 2077 | }; 2078 | ``` 2079 | 2080 | 2081 | 2082 | 2083 | 2084 | --- 2085 | 2086 | ### 二、算法类(27) 2087 | 2088 | #### 1. 递归(4) 2089 | 2090 | 2091 | ##### 1. 007-斐波那契数列 2092 | **题目描述** 2093 | 2094 | 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项。(n<=39) 2095 | 2096 | f(0)=0 2097 | 2098 | f(1)=1 2099 | 2100 | f(n)=f(n-1)+f(n-2),n>1 2101 | 2102 | **解题思路** 2103 | 2104 | 递归重复计算,效率低 2105 | 2106 | 使用简单的循环方法来实现。 2107 | 2108 | **参考代码** 2109 | 2110 | ```c++ 2111 | class Solution { 2112 | public: 2113 | int Fibonacci(int n) { 2114 | if(n <= 0) 2115 | return 0; 2116 | if(n == 1) 2117 | return 1; 2118 | int first = 0, second = 1, third = 0; 2119 | for (int i = 2; i <= n; i++) { 2120 | third = first + second; 2121 | first = second; 2122 | second = third; 2123 | } 2124 | return third; 2125 | } 2126 | }; 2127 | ``` 2128 | 2129 | 2130 | 2131 | ##### 2. 008-跳台阶 2132 | **题目描述** 2133 | 2134 | 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 2135 | 2136 | **解题思路** 2137 | 2138 | 首先我们考虑最简单的情况。如果只有1级台阶,那么显然只一种跳法。如果有2级台阶,那就有两种跳法:一种是分两次跳,每次跳1级;另一种是一次跳2级。 2139 | 2140 | 接着,我们来讨论一般情况。我们把n级台阶时的跳法看成是n的函数,记为f(n)。当n>2时,第一次跳的时候就有两种不同的选择:一是第一次只跳1级,此时跳法数目等于后面剩下的n-1级台阶的跳法数目,即为f(n-1);另外一种选择是跳一次跳2级,此时跳法数目等于后面剩下的n-2级台阶的跳法数目,即为f(n-2)。因此n级台阶的不同跳法的总数f(n)=f(n-1)+f(n-2)。分析到这里,我们不难看出这实际上就是斐波那契数列了。 2141 | 2142 | **参考代码** 2143 | 2144 | ```c++ 2145 | class Solution { 2146 | public: 2147 | int jumpFloor(int number) { 2148 | if(number <= 0){ 2149 | return 0; 2150 | } 2151 | else if(number < 3){ 2152 | return number; 2153 | } 2154 | int first = 1, second = 2, third = 0; 2155 | for(int i = 3; i <= number; i++){ 2156 | third = first + second; 2157 | first = second; 2158 | second = third; 2159 | } 2160 | return third; 2161 | } 2162 | }; 2163 | ``` 2164 | 2165 | 2166 | 2167 | ##### 3. 009-变态跳台阶 2168 | **题目描述** 2169 | 2170 | 一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。 2171 | 2172 | **解题思路** 2173 | 2174 | 当n=1时,结果为1; 2175 | 当n=2时,结果为2; 2176 | 当n=3时,结果为4; 2177 | 以此类推,我们使用数学归纳法不难发现,跳法f(n)=2^(n-1)。 2178 | 2179 | **参考代码** 2180 | 2181 | ```c++ 2182 | class Solution { 2183 | public: 2184 | int jumpFloorII(int number) { 2185 | if(number == 0){ 2186 | return 0; 2187 | } 2188 | int total = 1; 2189 | for(int i = 1; i < number; i++){ 2190 | total *= 2; 2191 | } 2192 | return total; 2193 | } 2194 | }; 2195 | ``` 2196 | 2197 | 2198 | 2199 | 2200 | ##### 4. 010-矩形覆盖 2201 | **题目描述** 2202 | 2203 | 我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法? 2204 | 2205 | **解题思路** 2206 | 2207 | 以2x8的矩形为例,先把2x8的覆盖方法记为f(8)。用第一个1x2小矩阵覆盖大矩形的最左边时有两个选择,竖着放或者横着放。当竖着放的时候,右边还剩下2x7的区域,这种情况下的覆盖方法记为f(7)。接下来考虑横着放的情况。当1x2的小矩形横着放在左上角的时候,左下角和横着放一个1x2的小矩形,而在右边还剩下2x6的区域,这种情况下的覆盖方法记为f(6)。因此f(8)=f(7)+f(6)。此时我们可以看出,这仍然是斐波那契数列。 2208 | 2209 | **参考代码** 2210 | 2211 | ```c++ 2212 | class Solution { 2213 | public: 2214 | int rectCover(int number) { 2215 | if(number <= 2){ 2216 | return number; 2217 | } 2218 | int first = 1, second = 2, third = 0; 2219 | for(int i = 3; i <= number; i++){ 2220 | third = first + second; 2221 | first = second; 2222 | second = third; 2223 | } 2224 | return third; 2225 | } 2226 | }; 2227 | ``` 2228 | 2229 | 2230 | 2231 | ##### 5. 047-求1+2+...+n 2232 | **题目描述** 2233 | 2234 | 求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。 2235 | 2236 | **解题思路** 2237 | 2238 | 递归 2239 | 2240 | **参考代码** 2241 | 2242 | ```c++ 2243 | class Solution { 2244 | public: 2245 | int Sum_Solution(int n) { 2246 | int ans = n; 2247 | // &&就是逻辑与,逻辑与有个短路特点,前面为假,后面不计算。即递归终止条件 2248 | ans && (ans += Sum_Solution(n - 1)); 2249 | return ans; 2250 | } 2251 | }; 2252 | ``` 2253 | 2254 | 2255 | 2256 | 2257 | #### 2. 排序(3) 2258 | 2259 | 2260 | ##### 1. 029-最小的k个数 2261 | **题目描述** 2262 | 2263 | 输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4。 2264 | 2265 | **解题思路** 2266 | 2267 | O(nlogk)的算法是基于堆排序的,特别适合处理海量数据。 2268 | 2269 | 我们可以先创建一个大小为k的数据容器来存储最小的k个数字,接下来我们每次从输入的n个整数中的n个整数中读入一个数。如果容器中已有的数字少于k个,则直接把这次读入的整数放入容器之中;如果容器已经有k个数字了,也就是容器满了,此时我们不能再插入新的数字而只能替换已有的数字。找出这已有的k个数中的最大值,然后拿这次待插入的整数和最大值进行比较。如果待插入的值比当前已有的最大值小,则用这个数替换当前已有的最大值;如果待插入的值比当前已有的最大值还要大,那么这个数不可能是最小的k个整数之一,于是我们可以抛弃这个整数。 2270 | 2271 | 因此当容器满了之后,我们要做3件事情:一是在k个整数中找到最大数;二是有可能在这个容器中删除最大数;三是有可能要插入一个新的数字。如果用一个二叉树来实现这个数据容器,那么我们在O(logk)时间内实现这三步操作。因此对于n个输入数字而言,总的时间效率就是O(nlogk)。 2272 | 2273 | 2274 | 2275 | **参考代码** 2276 | 2277 | ```c++ 2278 | class Solution { 2279 | public: 2280 | vector GetLeastNumbers_Solution(vector input, int k) { 2281 | vector result; 2282 | int length = input.size(); 2283 | if(length <= 0 || k <= 0 || k > length){ 2284 | return result; 2285 | } 2286 | 2287 | for(int i = 0; i < input.size(); i++){ 2288 | if(result.size() < k){ 2289 | result.push_back(input[i]); 2290 | } 2291 | else{ 2292 | for(int j = k / 2; j >= 0; j--){ 2293 | HeadAdjust(result, j, k); 2294 | } 2295 | for(int j = k - 1; j > 0; j--){ 2296 | swap(result[0], result[j]); 2297 | HeadAdjust(result, 0, j); 2298 | } 2299 | if(result[k-1] > input[i]){ 2300 | result[k-1] = input[i]; 2301 | } 2302 | } 2303 | } 2304 | return result; 2305 | } 2306 | private: 2307 | void HeadAdjust(vector &input, int parent, int length){ 2308 | int temp = input[parent]; 2309 | int child = 2 * parent + 1; 2310 | while(child < length){ 2311 | if(child + 1 < length && input[child] < input[child+1]){ 2312 | child++; 2313 | } 2314 | if(temp >= input[child]){ 2315 | break; 2316 | } 2317 | input[parent] = input[child]; 2318 | 2319 | parent = child; 2320 | child = 2 * parent + 1; 2321 | } 2322 | input[parent] = temp; 2323 | } 2324 | }; 2325 | ``` 2326 | 2327 | 2328 | 2329 | ##### 2. 035-数组中的逆序对 2330 | **题目描述** 2331 | 2332 | 在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007。 2333 | 2334 | 输入描述: 2335 | 2336 | 题目保证输入的数组中没有的相同的数字 2337 | 2338 | 数据范围: 2339 | 2340 | 对于%50的数据,size<=10^4 2341 | 2342 | 对于%75的数据,size<=10^5 2343 | 2344 | 对于%100的数据,size<=2*10^5 2345 | 2346 | **解题思路** 2347 | 2348 | 分治思想,采用[归并排序](https://cuijiahua.com/blog/tag/%e5%bd%92%e5%b9%b6%e6%8e%92%e5%ba%8f/)的思路来处理,如下图,先分后治: 2349 | 2350 | ![basis_35_1](assets/basis_35_1.png) 2351 | 2352 | 先把数组分解成两个长度为2的子数组,再把这两个子数组分解成两个长度为1的子数组。接下来一边合并相邻的子数组,一边统计逆序对的数目。在第一对长度为1的子数组{7}、{5}中7>5,因此(7,5)组成一个逆序对。同样在第二对长度为1的子数组{6},{4}中也有逆序对(6,4),由于已经统计了这两对子数组内部的逆序对,因此需要把这两对子数组进行排序,避免在之后的统计过程中重复统计。 2353 | 2354 | ![basis_35_2](assets/basis_35_2.png) 2355 | 2356 | **逆序对的总数** = 左边数组中的逆序对的数量 + 右边数组中逆序对的数量 + 左右结合成新的顺序数组时中出现的逆序对的数量 2357 | 2358 | 总结一下: 2359 | 2360 | 这是一个归并排序的合并过程,主要是考虑合并两个有序序列时,计算逆序对数。 2361 | 2362 | 对于两个升序序列,设置两个下标:两个有序序列的末尾。每次比较两个末尾值,如果前末尾大于后末尾值,则有”后序列当前长度“个逆序对;否则不构成逆序对。然后把较大值拷贝到辅助数组的末尾,即最终要将两个有序序列合并到辅助数组并有序。 2363 | 2364 | 这样,每次在合并前,先递归地处理左半段、右半段,则左、右半段有序,且左右半段的逆序对数可得到,再计算左右半段合并时逆序对的个数。 2365 | 2366 | **注意:**InversePairsCore形参的顺序是(data,copy),而递归调用时实参是(copy,data)。 2367 | 2368 | 要明白递归函数InversePairsCore的作用就行了,它是对data的左右半段进行合并,复制到辅助数组copy中有序。最后,data和copy两个数组都是有序的。 2369 | 2370 | **参考代码** 2371 | 2372 | ```c++ 2373 | class Solution { 2374 | public: 2375 | int InversePairs(vector data) { 2376 | if(data.size() == 0){ 2377 | return 0; 2378 | } 2379 | // 排序的辅助数组 2380 | vector copy; 2381 | for(int i = 0; i < data.size(); ++i){ 2382 | copy.push_back(data[i]); 2383 | } 2384 | return InversePairsCore(data, copy, 0, data.size() - 1) % 1000000007; 2385 | } 2386 | long InversePairsCore(vector &data, vector ©, int begin, int end){ 2387 | // 如果指向相同位置,则没有逆序对。 2388 | if(begin == end){ 2389 | copy[begin] = data[end]; 2390 | return 0; 2391 | } 2392 | // 求中点 2393 | int mid = (end + begin) >> 1; 2394 | // 使data左半段有序,并返回左半段逆序对的数目 2395 | long leftCount = InversePairsCore(copy, data, begin, mid); 2396 | // 使data右半段有序,并返回右半段逆序对的数目 2397 | long rightCount = InversePairsCore(copy, data, mid + 1, end); 2398 | 2399 | int i = mid; // i初始化为前半段最后一个数字的下标 2400 | int j = end; // j初始化为后半段最后一个数字的下标 2401 | int indexcopy = end; // 辅助数组复制的数组的最后一个数字的下标 2402 | long count = 0; // 计数,逆序对的个数,注意类型 2403 | 2404 | while(i >= begin && j >= mid + 1){ 2405 | if(data[i] > data[j]){ 2406 | copy[indexcopy--] = data[i--]; 2407 | count += j - mid; 2408 | } 2409 | else{ 2410 | copy[indexcopy--] = data[j--]; 2411 | } 2412 | } 2413 | for(;i >= begin; --i){ 2414 | copy[indexcopy--] = data[i]; 2415 | } 2416 | for(;j >= mid + 1; --j){ 2417 | copy[indexcopy--] = data[j]; 2418 | } 2419 | return leftCount + rightCount + count; 2420 | } 2421 | }; 2422 | ``` 2423 | 2424 | 2425 | 2426 | ##### 3. 063-数据流中的中位数 2427 | **题目描述** 2428 | 2429 | 如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。 2430 | 2431 | **解题思路** 2432 | 2433 | 主要思想: 2434 | 2435 | 最大堆 | 最小堆 2436 | 2437 | 我们将数据分为两部分,位于左边最大堆的数据比右边最小堆的数据要小,左、右两边内部的数据没有排序,也可以根据左边最大的数及右边最小的数得到中位数。 2438 | 2439 | 接下来考虑用最大堆和最小堆实现的一些细节。 2440 | 2441 | 首先要保证数据平均分配到两个堆中,因此两个堆中数据的数目之差不能超过1.为了实现平均分配,可以在数据的总数目是偶数时把新数据插入到最小堆中,否则插入到最大堆中。 2442 | 2443 | 此外,还要保证最大堆中所有数据小于最小堆中数据。所以,新传入的数据需要先和最大堆的最大值或者最小堆中的最小值进行比较。以总数目为偶数为例,按照我们制定的规则,新的数据会被插入到最小堆中,但是在这之前,我们需要判断这个数据和最大堆中的最大值谁更大,如果最大堆中的数据比较大,那么我们就需要把当前数据插入最大堆,然后弹出新的最大值,再插入到最小堆中。由于最终插入到最小堆的数字是原最大堆中最大的数字,这样就保证了最小堆中所有数字都大于最大堆的数字。 2444 | 下面代码中,我们基于stl中的函数push_heap、pop_heap以及vector实现堆。比较仿函数less和greater分别用来实现最大堆和最小堆。 2445 | 2446 | **参考代码** 2447 | 2448 | ```c++ 2449 | class Solution { 2450 | public: 2451 | void Insert(int num) 2452 | { 2453 | // 如果已有数据为偶数,则放入最小堆 2454 | if(((max.size() + min.size()) & 1) == 0){ 2455 | // 如果插入的数字小于最大堆里的最大的数,则将数字插入最大堆 2456 | // 并将最大堆中的最大的数字插入到最小堆 2457 | if(max.size() > 0 && num < max[0]){ 2458 | // 插入数据插入到最大堆数组 2459 | max.push_back(num); 2460 | // 调整最大堆 2461 | push_heap(max.begin(), max.end(), less()); 2462 | // 拿出最大堆中的最大数 2463 | num = max[0]; 2464 | // 删除最大堆的栈顶元素 2465 | pop_heap(max.begin(), max.end(), less()); 2466 | max.pop_back(); 2467 | } 2468 | // 将数据插入最小堆数组 2469 | min.push_back(num); 2470 | // 调整最小堆 2471 | push_heap(min.begin(), min.end(), greater()); 2472 | } 2473 | // 已有数据为奇数,则放入最大堆 2474 | else{ 2475 | if(min.size() > 0 && num > min[0]){ 2476 | // 将数据插入最小堆 2477 | min.push_back(num); 2478 | // 调整最小堆 2479 | push_heap(min.begin(), min.end(), greater()); 2480 | // 拿出最小堆的最小数 2481 | num = min[0]; 2482 | // 删除最小堆的栈顶元素 2483 | pop_heap(min.begin(), min.end(), greater()); 2484 | min.pop_back(); 2485 | } 2486 | // 将数据插入最大堆 2487 | max.push_back(num); 2488 | push_heap(max.begin(), max.end(), less()); 2489 | } 2490 | } 2491 | double GetMedian() 2492 | { 2493 | // 统计数据大小 2494 | int size = min.size() + max.size(); 2495 | if(size == 0){ 2496 | return 0; 2497 | } 2498 | // 如果数据为偶数 2499 | if((size & 1) == 0){ 2500 | return (min[0] + max[0]) / 2.0; 2501 | } 2502 | // 奇数 2503 | else{ 2504 | return min[0]; 2505 | } 2506 | } 2507 | private: 2508 | // 使用vector建立最大堆和最小堆,min是最小堆数组,max是最大堆数组 2509 | vector min; 2510 | vector max; 2511 | }; 2512 | ``` 2513 | 2514 | 2515 | 2516 | 2517 | #### 3. 搜索(3) 2518 | 2519 | 2520 | ##### 1. 001-二维数组中的查找 2521 | **题目描述** 2522 | 2523 | 在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。 2524 | 2525 | **解题思路** 2526 | 2527 | 首先选取数组中右上角的数字。如果该数字等于要查找的数字,查找过程结束;如果该数字大于要查找的数组,剔除这个数字所在的列;如果该数字小于要查找的数字,剔除这个数字所在的行。也就是说如果要查找的数字不在数组的右上角,则每一次都在数组的查找范围中剔除一行或者一列,这样每一步都可以缩小查找的范围,直到找到要查找的数字,或者查找范围为空。 2528 | 2529 | **参考代码** 2530 | 2531 | ```c++ 2532 | class Solution { 2533 | public: 2534 | bool Find(int target, vector > array) { 2535 | int rows = array.size(); 2536 | int cols = array[0].size(); 2537 | if(!array.empty() && rows > 0 && cols > 0){ 2538 | int row = 0; 2539 | int col = cols - 1; 2540 | while(row < rows && col >= 0){ 2541 | if(array[row][col] == target){ 2542 | return true; 2543 | } 2544 | else if(array[row][col] > target){ 2545 | --col; 2546 | } 2547 | else{ 2548 | ++row; 2549 | } 2550 | } 2551 | } 2552 | return false; 2553 | } 2554 | }; 2555 | ``` 2556 | 2557 | 2558 | 2559 | ##### 2. 006-旋转数组的最小数字(二分查找) 2560 | **题目描述** 2561 | 2562 | 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。 2563 | 2564 | **解题思路** 2565 | 2566 | 我们注意到旋转之后的数组实际上可以划分为两个排序的字数组,而且前面的字数组的元素大于或者等于后面字数组的元素。我们还注意到最小的元素刚好是这两个字数组的分界线。在排序的数组中可以用[二分查找](https://cuijiahua.com/blog/tag/%e4%ba%8c%e5%88%86%e6%9f%a5%e6%89%be/)实现O(logn)的查找。本题给出的数组在一定程度上是排序的,因此我们可以试着用[二分查找](https://cuijiahua.com/blog/tag/%e4%ba%8c%e5%88%86%e6%9f%a5%e6%89%be/)法的思路来寻找这个最小的元素。 2567 | 2568 | - 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。 2569 | - 接着我们可以找到数组中间的元素。如果中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时最小元素应该位于该中间元素之后,然后我们把第一个指针指向该中间元素,移动之后第一个指针仍然位于前面的递增子数组中。 2570 | - 同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时最小元素应该位于该中间元素之前,然后我们把第二个指针指向该中间元素,移动之后第二个指针仍然位于后面的递增子数组中。 2571 | - 第一个指针总是指向前面递增数组的元素,第二个指针总是指向后面递增数组的元素。最终它们会指向两个相邻的元素,而第二个指针指向的刚好是最小的元素,这就是循环结束的条件。 2572 | 2573 | 示意图如下: 2574 | 2575 | ![basis_6_1](assets/basis_6_1.jpg) 2576 | 2577 | 特殊情况: 2578 | 2579 | - 如果把排序数组的0个元素搬到最后面,这仍然是旋转数组,我们的代码需要支持这种情况。如果发现数组中的一个数字小于最后一个数字,就可以直接返回第一个数字了。 2580 | - 下面这种情况,即第一个指针指向的数字、第二个指针指向的数字和中间的数字三者相等,我们无法判断中间的数字1是数以前面的递增子数组还是后面的递增子数组。正样的话,我们只能进行顺序查找。 2581 | 2582 | ![basis_6_2](assets/basis_6_2.jpg) 2583 | 2584 | **参考代码** 2585 | 2586 | ```c++ 2587 | class Solution { 2588 | public: 2589 | int minNumberInRotateArray(vector rotateArray) { 2590 | int size = rotateArray.size(); //数组长度 2591 | if(size == 0){ 2592 | return 0; 2593 | } 2594 | int left = 0; //左指针 2595 | int right = size - 1; //右指针 2596 | int mid = 0; //中间指针 2597 | while(rotateArray[left] >= rotateArray[right]){ //确保旋转 2598 | if(right - left == 1){ //左右指针相邻 2599 | mid = right; 2600 | break; 2601 | } 2602 | mid = left + (right - left) / 2; //计算中间指针位置 2603 | //特殊情况:如果无法确定中间元素是属于前面还是后面的递增子数组,只能顺序查找 2604 | if(rotateArray[left] == rotateArray[right] && rotateArray[mid] == rotateArray[left]){ 2605 | return MinInOrder(rotateArray, left, right); 2606 | } 2607 | //中间元素位于前面的递增子数组,此时最小元素位于中间元素的后面 2608 | if(rotateArray[mid] >= rotateArray[left]){ 2609 | left = mid; 2610 | } 2611 | //中间元素位于后面的递增子数组,此时最小元素位于中间元素的前面 2612 | else{ 2613 | right = mid; 2614 | } 2615 | } 2616 | return rotateArray[mid]; 2617 | } 2618 | private: 2619 | //顺序寻找最小值 2620 | int MinInOrder(vector &num, int left, int right){ 2621 | int result = num[left]; 2622 | for(int i = left + 1; i < right; i++){ 2623 | if(num[i] < result){ 2624 | result = num[i]; 2625 | } 2626 | } 2627 | return result; 2628 | } 2629 | }; 2630 | ``` 2631 | 2632 | 2633 | 2634 | ##### 3. 037-数字在排序数组中出现的次数(二分) 2635 | **题目描述** 2636 | 2637 | 统计一个数字在排序数组中出现的次数。 2638 | 2639 | **解题思路** 2640 | 2641 | 既然是已经排序好的数组,那么第一个想到的就是二分查找法。 2642 | 2643 | 做法就是使用二分法找到数字在数组中出现的第一个位置,再利用二分法找到数字在数组中出现的第二个位置。时间复杂度为O(nlogn + nlogn),最终的时间复杂度为O(nlogn)。 2644 | 2645 | 举个例子,找到数字k在数组data中出现的次数。 2646 | 2647 | 数组data中,数字k出现的第一个位置: 2648 | 2649 | 我们对数组data进行二分,如果数组中间的数字小于k,说明k应该出现在中间位置的右边;如果数组中间的数字大于k,说明k应该出现在中间位置的左边;如果数组中间的数字等于k,并且中间位置的前一个数字不等于k,说明这个中间数字就是数字k出现的第一个位置。 2650 | 2651 | 同理,数字k出现的最后一个位置,也是这样找的。但是判断少有不同。我们使用两个函数分别获得他们。 2652 | 2653 | **参考代码** 2654 | 2655 | ```c++ 2656 | class Solution { 2657 | public: 2658 | int GetNumberOfK(vector data ,int k) { 2659 | int length = data.size(); 2660 | if(length == 0){ 2661 | return 0; 2662 | } 2663 | int first = GetFirstK(data, k, 0, length - 1); 2664 | int last = GetLastK(data, k, 0, length - 1); 2665 | if(first != -1 && last != -1){ 2666 | return last - first + 1; 2667 | } 2668 | return 0; 2669 | } 2670 | private: 2671 | // 迭代实现找到第一个K 2672 | int GetFirstK(vector data, int k, int begin, int end){ 2673 | if(begin > end){ 2674 | return -1; 2675 | } 2676 | int middleIndex = (begin + end) >> 1; 2677 | int middleData = data[middleIndex]; 2678 | 2679 | if(middleData == k){ 2680 | if((middleIndex > 0 && data[middleIndex - 1] != k) || middleIndex == 0){ 2681 | return middleIndex; 2682 | } 2683 | else{ 2684 | end = middleIndex - 1; 2685 | } 2686 | } 2687 | else if (middleData > k){ 2688 | end = middleIndex - 1; 2689 | } 2690 | else{ 2691 | begin = middleIndex + 1; 2692 | } 2693 | return GetFirstK(data, k, begin, end); 2694 | } 2695 | // 循环实现找到最后一个K 2696 | int GetLastK(vector data, int k, int begin, int end){ 2697 | int length = data.size(); 2698 | int middleIndex = (begin + end) >> 1; 2699 | int middleData = data[middleIndex]; 2700 | 2701 | while(begin <= end){ 2702 | if(middleData == k){ 2703 | if((middleIndex < length - 1 && data[middleIndex + 1] != k) || middleIndex == length - 1){ 2704 | return middleIndex; 2705 | } 2706 | else{ 2707 | begin = middleIndex + 1; 2708 | } 2709 | } 2710 | else if(middleData > k){ 2711 | end = middleIndex - 1; 2712 | } 2713 | else{ 2714 | begin = middleIndex + 1; 2715 | } 2716 | middleIndex = (begin + end) >> 1; 2717 | middleData = data[middleIndex]; 2718 | } 2719 | return -1; 2720 | } 2721 | }; 2722 | ``` 2723 | 2724 | 2725 | 2726 | 2727 | #### 4. 回溯(2) 2728 | 2729 | 2730 | ##### 1. 065-矩阵中的路径 2731 | **题目描述** 2732 | 2733 | 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如在下面的3x4的矩阵中包含一条字符串"bcced"的路径(路径中的字母用斜体表示)。但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。 2734 | 2735 | **解题思路** 2736 | 2737 | 首先,遍历这个矩阵,我们很容易就能找到与字符串str中第一个字符相同的矩阵元素ch。然后遍历ch的上下左右四个字符,如果有和字符串str中下一个字符相同的,就把那个字符当作下一个字符(下一次遍历的起点),如果没有,就需要回退到上一个字符,然后重新遍历。为了避免路径重叠,需要一个辅助矩阵来记录路径情况。 2738 | 2739 | 下面代码中,当矩阵坐标为(row,col)的格子和路径字符串中下标为pathLength的字符一样时,从4个相邻的格子(row,col-1)、(row-1,col)、(row,col+1)以及(row+1,col)中去定位路径字符串中下标为pathLength+1的字符。 2740 | 2741 | 如果4个相邻的格子都没有匹配字符串中下标为pathLength+1的字符,表明当前路径字符串中下标为pathLength的字符在矩阵中的定位不正确,我们需要回到前一个字符串(pathLength-1),然后重新定位。 2742 | 2743 | 一直重复这个过程,直到路径字符串上所有字符都在矩阵中找到格式的位置(此时str[pathLength] == '\0')。 2744 | 2745 | **参考代码** 2746 | 2747 | ```c++ 2748 | class Solution { 2749 | public: 2750 | bool hasPath(char* matrix, int rows, int cols, char* str) 2751 | { 2752 | if(matrix == NULL || rows < 1 || cols < 1 || str == NULL){ 2753 | return false; 2754 | } 2755 | bool* visited = new bool[rows*cols]; 2756 | memset(visited, 0, rows*cols); 2757 | int pathLength = 0; 2758 | for(int row = 0; row < rows; row++){ 2759 | for(int col = 0; col < cols; col++){ 2760 | if(hasPathCore(matrix, rows, cols, row, col, str, pathLength, visited)){ 2761 | delete[] visited; 2762 | return true; 2763 | } 2764 | } 2765 | } 2766 | delete[] visited; 2767 | return false; 2768 | } 2769 | private: 2770 | bool hasPathCore(char* matrix, int rows, int cols, int row, int col, char* str, int& pathLength, bool* visited){ 2771 | if(str[pathLength] == '\0'){ 2772 | return true; 2773 | } 2774 | bool hasPath = false; 2775 | if(row >= 0 && row < rows && col >= 0 && col < cols && matrix[row*cols+col] == str[pathLength] && !visited[row*cols+col]){ 2776 | ++pathLength; 2777 | visited[row*cols+col] = true; 2778 | hasPath = hasPathCore(matrix, rows, cols, row-1, col, str, pathLength, visited) 2779 | || hasPathCore(matrix, rows, cols, row+1, col, str, pathLength, visited) 2780 | || hasPathCore(matrix, rows, cols, row, col-1, str, pathLength, visited) 2781 | || hasPathCore(matrix, rows, cols, row, col+1, str, pathLength, visited); 2782 | if(!hasPath){ 2783 | --pathLength; 2784 | visited[row*cols+col] = false; 2785 | } 2786 | } 2787 | return hasPath; 2788 | } 2789 | }; 2790 | ``` 2791 | 2792 | 2793 | 2794 | ##### 2. 066-机器人的运动范围 2795 | **题目描述** 2796 | 2797 | 地上有一个m行和n列的方格。一个机器人从坐标0,0的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于k的格子。 例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7 = 18。但是,它不能进入方格(35,38),因为3+5+3+8 = 19。请问该机器人能够达到多少个格子? 2798 | 2799 | **解题思路** 2800 | 2801 | 和上一道题十分相似,只不过这次的限制条件变成了坐标位数之和。对于求坐标位数之和,我们单独用一个函数实现,然后套入上一道题的代码中即可。 2802 | 2803 | **参考代码** 2804 | 2805 | ```c++ 2806 | class Solution { 2807 | public: 2808 | int movingCount(int threshold, int rows, int cols) 2809 | { 2810 | int count = 0; 2811 | if(threshold < 1 || rows < 1 || cols < 1){ 2812 | return count; 2813 | } 2814 | bool* visited = new bool[rows*cols]; 2815 | memset(visited, 0, rows*cols); 2816 | count = movingCountCore(threshold, rows, cols, 0, 0, visited); 2817 | delete[] visited; 2818 | return count; 2819 | } 2820 | private: 2821 | int movingCountCore(int threshold, int rows, int cols, int row, int col, bool* visited){ 2822 | int count = 0; 2823 | if(row >= 0 && row < rows && col >= 0 && col < cols && getDigitSum(row)+getDigitSum(col) <= threshold && !visited[row*cols+col]){ 2824 | visited[row*cols+col] = true; 2825 | count = 1 + movingCountCore(threshold, rows, cols, row+1, col, visited) 2826 | + movingCountCore(threshold, rows, cols, row-1, col, visited) 2827 | + movingCountCore(threshold, rows, cols, row, col+1, visited) 2828 | + movingCountCore(threshold, rows, cols, row, col-1, visited); 2829 | } 2830 | return count; 2831 | } 2832 | int getDigitSum(int num){ 2833 | int sum = 0; 2834 | while(num){ 2835 | sum += num % 10; 2836 | num /= 10; 2837 | } 2838 | return sum; 2839 | } 2840 | }; 2841 | ``` 2842 | 2843 | 2844 | 2845 | #### 5. 位运算(3) 2846 | 2847 | 2848 | ##### 1. 011-二进制中1的个数 2849 | **题目描述** 2850 | 2851 | 输入一个整数,输出该数二进制表示中1的个数。其中负数用补码表示。 2852 | 2853 | **解题思路** 2854 | 2855 | 如果一个整数不为0,那么这个整数至少有一位是1。如果我们把这个整数减1,那么原来处在整数最右边的1就会变为0,原来在1后面的所有的0都会变成1(如果最右边的1后面还有0的话)。其余所有位将不会受到影响。 2856 | 2857 | 举个例子:一个二进制数1100,从右边数起第三位是处于最右边的一个1。减去1后,第三位变成0,它后面的两位0变成了1,而前面的1保持不变,因此得到的结果是1011.我们发现减1的结果是把最右边的一个1开始的所有位都取反了。这个时候如果我们再把原来的整数和减去1之后的结果做与运算,从原来整数最右边一个1那一位开始所有位都会变成0。如1100&1011=1000.也就是说,把一个整数减去1,再和原整数做与运算,会把该整数最右边一个1变成0.那么一个整数的二进制有多少个1,就可以进行多少次这样的操作。 2858 | 2859 | **参考代码** 2860 | 2861 | ```c++ 2862 | class Solution { 2863 | public: 2864 | int NumberOf1(int n) { 2865 | int count = 0; 2866 | while(n){ 2867 | ++count; 2868 | n = (n - 1) & n; 2869 | } 2870 | return count; 2871 | } 2872 | }; 2873 | ``` 2874 | 2875 | 2876 | 2877 | ##### 2. 012-数值的整数次方 2878 | **题目描述** 2879 | 2880 | 给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。 2881 | 2882 | **解题思路** 2883 | 2884 | 当指数为负数的时候,可以先对指数求绝对值,然后算出次方的结果之后再取倒数。如果底数为0,则直接返回0。此时的次方在数学上是没有意义的。 2885 | 2886 | 除此之外,我们要**注意**:由于计算机表示小数(包括float和double型小数)都有误差,我们不能直接用等号(==)判断两个小数是否相等。如果两个小数的差的绝对值很小,比如小于0.0000001,就可以认为它们相等。 2887 | 2888 | 在计算次方的时候,除了简单的遍历,我们可以使用如下公式进行计算,来减少计算量: 2889 | 2890 | ![basis_12_1](assets/basis_12_1.jpg) 2891 | 2892 | **参考代码** 2893 | 2894 | ```c++ 2895 | class Solution { 2896 | public: 2897 | double Power(double base, int exponent) { 2898 | if(equal(base, 0.0)){ 2899 | return 0.0; 2900 | } 2901 | unsigned int absExponent = 0; 2902 | if(exponent > 0){ 2903 | absExponent = (unsigned int)(exponent); 2904 | } 2905 | else{ 2906 | absExponent = (unsigned int)(-exponent); 2907 | } 2908 | double result = PowerWithUnsignedExponent(base, absExponent); 2909 | if(exponent < 0){ 2910 | result = 1.0 / result; 2911 | } 2912 | return result; 2913 | } 2914 | 2915 | private: 2916 | bool equal(double num1, double num2){ 2917 | if(num1 - num2 > -0.0000001 && (num1 - num2) < 0.0000001){ 2918 | return true; 2919 | } 2920 | else{ 2921 | return false; 2922 | } 2923 | } 2924 | 2925 | double PowerWithUnsignedExponent(double base, unsigned int exponent){ 2926 | if(exponent == 0){ 2927 | return 1; 2928 | } 2929 | if(exponent == 1){ 2930 | return base; 2931 | } 2932 | double result = PowerWithUnsignedExponent(base, exponent >> 1); 2933 | result *= result; 2934 | if(exponent & 0x1 == 1){ 2935 | result *= base; 2936 | } 2937 | return result; 2938 | } 2939 | }; 2940 | ``` 2941 | 2942 | 2943 | 2944 | ##### 3. 040-数组中只出现一次的数字 2945 | **题目描述** 2946 | 2947 | 一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。 2948 | 2949 | **解题思路** 2950 | 2951 | 我们可以想一想“异或”运算的一个性质,我们直接举例说明。 2952 | 2953 | 举例:{2,4,3,6,3,2,5,5} 2954 | 2955 | 这个数组中只出现一次的两个数分别是4和6。怎么找到这个两个数字呢? 2956 | 2957 | 我们先不看找到俩个的情况,先看这样一个问题,如何在一个数组中找到一个只出现一次的数字呢?比如数组:{4,5,5},唯一一个只出现一次的数字是4。 2958 | 2959 | 我们知道异或的一个性质是:任何一个数字异或它自己都等于0。也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现一次的数字。比如数组{4,5,5},我们先用数组中的第一个元素4(二进制形式:0100)和数组中的第二个元素5(二进制形式:0101)进行异或操作,0100和0101异或得到0001,用这个得到的元素与数组中的三个元素5(二进制形式:0101)进行异或操作,0001和0101异或得到0100,正好是结果数字4。这是因为数组中相同的元素异或是为0的,因此就只剩下那个不成对的孤苦伶仃元素。 2960 | 2961 | 现在好了,我们已经知道了如何找到一个数组中找到一个只出现一次的数字,那么我们如何在一个数组中找到两个只出现一次的数字呢?如果,我们可以将原始数组分成两个子数组,使得每个子数组包含一个只出现一次的数字,而其他数字都成对出现。这样,我们就可以用上述方法找到那个孤苦伶仃的元素。 2962 | 2963 | 我们还是从头到尾一次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数组的异或结果。因为其他数字都出现了两次,在异或中全部抵消了。由于两个数字肯定不一样,那么异或的结果肯定不为0,也就是说这个结果数组的二进制表示至少有一个位为1。我们在结果数组中找到第一个为1的位的位置,记为第n位。现在我们以第n位是不是1为标准把元数组中的数字分成两个子数组,第一个子数组中每个数字的第n位都是1,而第二个子数组中每个数字的第n位都是0。 2964 | 2965 | 举例:{2,4,3,6,3,2,5,5} 2966 | 2967 | 我们依次对数组中的每个数字做异或运行之后,得到的结果用二进制表示是0010。异或得到结果中的倒数第二位是1,于是我们根据数字的倒数第二位是不是1分为两个子数组。第一个子数组{2,3,6,3,2}中所有数字的倒数第二位都是1,而第二个子数组{4,5,5}中所有数字的倒数第二位都是0。接下来只要分别两个子数组求异或,就能找到第一个子数组中只出现一次的数字是6,而第二个子数组中只出现一次的数字是4。 2968 | 2969 | **参考代码** 2970 | 2971 | ```c++ 2972 | class Solution { 2973 | public: 2974 | void FindNumsAppearOnce(vector data,int* num1,int *num2) { 2975 | int length = data.size(); 2976 | if(length < 2){ 2977 | return; 2978 | } 2979 | 2980 | // 对原始数组每个元素求异或 2981 | int resultExclusiveOR = 0; 2982 | for(int i = 0; i < length; ++i){ 2983 | resultExclusiveOR ^= data[i]; 2984 | } 2985 | 2986 | unsigned int indexOf1 = FindFirstBitIs1(resultExclusiveOR); 2987 | 2988 | *num1 = *num2 = 0; 2989 | for(int j = 0; j < length; j++){ 2990 | if(IsBit1(data[j], indexOf1)){ 2991 | *num1 ^= data[j]; 2992 | } 2993 | else{ 2994 | *num2 ^= data[j]; 2995 | } 2996 | } 2997 | } 2998 | private: 2999 | // 找到二进制数num第一个为1的位数,比如0010,第一个为1的位数是2。 3000 | unsigned int FindFirstBitIs1(int num){ 3001 | unsigned int indexBit = 0; 3002 | // 只判断一个字节的 3003 | while((num & 1) == 0 && (indexBit < 8 * sizeof(unsigned int))){ 3004 | num = num >> 1; 3005 | indexBit++; 3006 | } 3007 | return indexBit; 3008 | } 3009 | // 判断第indexBit位是否为1 3010 | bool IsBit1(int num, unsigned int indexBit){ 3011 | num = num >> indexBit; 3012 | return (num & 1); 3013 | } 3014 | }; 3015 | ``` 3016 | 3017 | 3018 | 3019 | #### 6. 全排列序(1) 3020 | 3021 | 3022 | ##### 1. 027-字符串的排列 3023 | **题目描述** 3024 | 3025 | 输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。 3026 | 3027 | 输入描述: 3028 | 3029 | 输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。 3030 | 3031 | **解题思路** 3032 | 3033 | 我们求整个字符串的排列,可以看成两步:首先求所有可能出现在第一个位置的字符,即把第一个字符和后面所有的字符交换。如下图所示: 3034 | 3035 | ![basis_27_1](assets/basis_27_1.jpg) 3036 | 3037 | 上图就是分别把第一个字符a和后面的b、c等字符交换的情形。首先固定第一个字符,求后面所有字符的排列。这个时候我们仍把后面的所有字符分为两部分:后面的字符的第一个字符,以及这个字符之后的所有字符。然后把第一个字符逐一和它后面的字符交换。 3038 | 3039 | 这个思路,是典型的递归思路。 3040 | 3041 | **参考代码** 3042 | 3043 | ```c++ 3044 | class Solution { 3045 | public: 3046 | vector Permutation(string str) { 3047 | //判断输入 3048 | if(str.length() == 0){ 3049 | return result; 3050 | } 3051 | PermutationCore(str, 0); 3052 | //对结果进行排序 3053 | sort(result.begin(), result.end()); 3054 | return result; 3055 | } 3056 | 3057 | private: 3058 | void PermutationCore(string str, int begin){ 3059 | //递归结束的条件:第一位和最后一位交换完成 3060 | if(begin == str.length()){ 3061 | result.push_back(str); 3062 | return; 3063 | } 3064 | for(int i = begin; i < str.length(); i++){ 3065 | //如果字符串相同,则不交换 3066 | if(i != begin && str[i] == str[begin]){ 3067 | continue; 3068 | } 3069 | //位置交换 3070 | swap(str[begin], str[i]); 3071 | //递归调用,前面begin+1的位置不变,后面的字符串全排列 3072 | PermutationCore(str, begin + 1); 3073 | } 3074 | } 3075 | vector result; 3076 | }; 3077 | ``` 3078 | 3079 | 3080 | 3081 | 3082 | #### 7.动态规划(2) 3083 | 3084 | 3085 | ##### 1. 030-连续子数组的最大和 3086 | **题目描述** 3087 | 3088 | HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。你会不会被他忽悠住?(子向量的长度至少是1) 3089 | 3090 | **解题思路** 3091 | 3092 | 数组分析:下图是我们计算数组(1,-2,3,10,-4,7,2,-5)中子数组的最大和的过程。通过分析我们发现,累加的子数组和,如果大于零,那么我们继续累加就行;否则,则需要剔除原来的累加和重新开始。 3093 | 3094 | 过程如下: 3095 | 3096 | ![basis_30_1](assets/basis_30_1.jpg) 3097 | 3098 | **参考代码** 3099 | 3100 | ```c++ 3101 | class Solution { 3102 | public: 3103 | int FindGreatestSumOfSubArray(vector array) { 3104 | if(array.empty()){ 3105 | return 0; 3106 | } 3107 | // 初始化变量,maxSum为最大和,curSum为当前和 3108 | int maxSum = array[0]; 3109 | int curSum = array[0]; 3110 | // 遍历所有元素 3111 | for(int i = 1; i < array.size(); i++){ 3112 | // 如果当前和小于等于0,说明之前的是负数,则抛弃前面的和,重新计算 3113 | if(curSum <= 0){ 3114 | curSum = array[i]; 3115 | } 3116 | // 如果没有问题,直接累加 3117 | else{ 3118 | curSum += array[i]; 3119 | } 3120 | // 更新最大和 3121 | if(curSum > maxSum){ 3122 | maxSum = curSum; 3123 | } 3124 | } 3125 | return maxSum; 3126 | } 3127 | }; 3128 | ``` 3129 | 3130 | 3131 | 3132 | ##### 2. 052-正则表达式匹配 3133 | **题目描述** 3134 | 3135 | 请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配。 3136 | 3137 | **解题思路** 3138 | 3139 | 这道题有些绕,需要好好思考下。 3140 | 3141 | 我们先来分析下如何匹配一个字符,现在只考虑字符'.',不考虑'*'看一下: 3142 | 3143 | 如果字符串和模式串的当前字符相等,那么我们继续匹配它们的下一个字符;如果模式串中的字符是'.',那么它可以匹配字符串中的任意字符,我们也可以继续匹配它们的下一个字符。 3144 | 3145 | 接下来,把字符'*'考虑进去,它可以匹配任意次的字符,当然出现0次也可以。 3146 | 3147 | 我们分两种情况来看: 3148 | 3149 | - 模式串的下一个字符不是'*',也就是上面说的只有字符'.'的情况。 3150 | 3151 | 如果字符串中的第一个字符和模式串中的第一个字符相匹配,那么字符串的模式串都向后移动一个字符,然后匹配剩余的字符串和模式串。如果字符串中的第一个字符和模式中的第一个字符不相匹配,则直接返回false。 3152 | 3153 | - 模式串的下一个字符是'*',此时就要复杂一些。 3154 | 3155 | 因为可能有多种不同的匹配方式。 3156 | 3157 | 选择一:无论字符串和模式串当前字符相不相等,我们都将模式串后移两个字符,相当于把模式串中的当前字符和'*'忽略掉,因为'*'可以匹配任意次的字符,所以出现0次也可以。 3158 | 3159 | 选择二:如果字符串和模式串当前字符相等,则字符串向后移动一个字符。而模式串此时有两个选择: 3160 | 3161 | 1、我们可以在模式串向后移动两个字符,继续匹配; 3162 | 3163 | 2、也可以保持模式串不变,这样相当于用字符'*'继续匹配字符串,也就是模式串中的字符'*'匹配字符串中的字符多个的情况。 3164 | 3165 | 用一张图表示如下: 3166 | 3167 | ![basis_52_1](assets/basis_52_1.png) 3168 | 3169 | 如上图所示,当匹配进入状态2,并且字符串中的字符是'a'时,我们有两个选择:可以进入状态3(在模式串向后移动两个字符),也可以回到状态2(模式串保持不变)。 3170 | 3171 | 除此之外,还要注意对空指针的处理。 3172 | 3173 | **参考代码** 3174 | 3175 | ```c++ 3176 | class Solution { 3177 | public: 3178 | bool match(char* str, char* pattern) 3179 | { 3180 | // 指针为空,返回false 3181 | if(str == NULL || pattern == NULL){ 3182 | return false; 3183 | } 3184 | return matchCore(str, pattern); 3185 | } 3186 | private: 3187 | bool matchCore(char* str, char* pattern){ 3188 | // 字符串和模式串都运行到了结尾,返回true 3189 | if(*str == '\0' && *pattern == '\0'){ 3190 | return true; 3191 | } 3192 | // 字符串没有到结尾,模式串到了,则返回false 3193 | // 模式串没有到结尾,字符串到了,则根据后续判断进行,需要对'*'做处理 3194 | if((*str != '\0' && *pattern == '\0')){ 3195 | return false; 3196 | } 3197 | // 如果模式串的下一个字符是'*',则进入状态机的匹配 3198 | if(*(pattern + 1) == '*'){ 3199 | // 如果字符串和模式串相等,或者模式串是'.',并且字符串没有到结尾,则继续匹配 3200 | if(*str == *pattern || (*pattern == '.' && *str != '\0')){ 3201 | // 进入下一个状态,就是匹配到了一个 3202 | return matchCore(str + 1, pattern + 2) || 3203 | // 保持当前状态,就是继续那这个'*'去匹配 3204 | matchCore(str + 1, pattern) || 3205 | // 跳过这个'*' 3206 | matchCore(str, pattern + 2); 3207 | } 3208 | // 如果字符串和模式串不相等,则跳过当前模式串的字符和'*',进入新一轮的匹配 3209 | else{ 3210 | // 跳过这个'*' 3211 | return matchCore(str, pattern + 2); 3212 | } 3213 | } 3214 | // 如果字符串和模式串相等,或者模式串是'.',并且字符串没有到结尾,则继续匹配 3215 | if(*str == *pattern || (*pattern == '.' && *str != '\0')){ 3216 | return matchCore(str + 1, pattern + 1); 3217 | } 3218 | return false; 3219 | } 3220 | }; 3221 | ``` 3222 | 3223 | 3224 | 3225 | #### 8. 双指针(2) 3226 | 3227 | 3228 | ##### 1. 041-和为S的连续正数序列 3229 | **题目描述** 3230 | 3231 | 小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck! 3232 | 3233 | 输出描述: 3234 | 3235 | 输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序。 3236 | 3237 | **解题思路** 3238 | 3239 | 设定两个指针,一个指向第一个数,一个指向最后一个数,在此之前需要设定第一个数和最后一个数的值,由于是正数序列,所以可以把第一个数设为1,最后一个数为2(因为是要求是连续正数序列,最后不可能和第一个数重合)。下一步就是不断改变第一个数和最后一个数的值,如果从第一个数到最后一个数的和刚好是要求的和,那么把所有的数都添加到一个序列中;如果大于要求的和,则说明从第一个数到最后一个数之间的范围太大,因此减小范围,需要把第一个数的值加1,同时把当前和减去原来的第一个数的值;如果小于要求的和,说明范围太小,因此把最后一个数加1,同时把当前的和加上改变之后的最后一个数的值。这样,不断修改第一个数和最后一个数的值,就能确定所有连续正数序列的和等于S的序列了。 3240 | 3241 | 注意:初中的求和公式应该记得吧,首项加尾项的和乘以个数除以2,即sum = (a + b) * n / 2。 3242 | 3243 | **参考代码** 3244 | 3245 | ```c++ 3246 | class Solution { 3247 | public: 3248 | vector > FindContinuousSequence(int sum) { 3249 | vector > result; 3250 | // 高位指针和低位指针 3251 | int phigh = 2, plow = 1; 3252 | 3253 | // 终止条件是phigh等于sum 3254 | while(phigh > plow){ 3255 | // 当前和,使用求和公式s = (a+b) * n / 2 3256 | int curSum = (plow + phigh) * (phigh - plow + 1) >> 1; 3257 | if(curSum < sum){ 3258 | phigh++; 3259 | } 3260 | if(curSum == sum){ 3261 | vector temp; 3262 | for(int i = plow; i <= phigh; i++){ 3263 | temp.push_back(i); 3264 | } 3265 | result.push_back(temp); 3266 | plow++; 3267 | } 3268 | if(curSum > sum){ 3269 | plow++; 3270 | } 3271 | } 3272 | return result; 3273 | } 3274 | }; 3275 | ``` 3276 | 3277 | 3278 | 3279 | ##### 2. 042-和为S的两个数字 3280 | **题目描述** 3281 | 3282 | 输入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。 3283 | 3284 | 输出描述: 3285 | 3286 | 对应每个测试案例,输出两个数,小的先输出。 3287 | 3288 | **解题思路** 3289 | 3290 | 输入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。 3291 | 3292 | 输出描述: 3293 | 3294 | 对应每个测试案例,输出两个数,小的先输出。 3295 | 3296 | **参考代码** 3297 | 3298 | ```c++ 3299 | class Solution { 3300 | public: 3301 | vector FindNumbersWithSum(vector array,int sum) { 3302 | vector result; 3303 | int length = array.size(); 3304 | if(length < 1){ 3305 | return result; 3306 | } 3307 | int pright = length - 1; 3308 | int pleft = 0; 3309 | 3310 | while(pright > pleft){ 3311 | int curSum = array[pleft] + array[pright]; 3312 | if(curSum == sum){ 3313 | result.push_back(array[pleft]); 3314 | result.push_back(array[pright]); 3315 | break; 3316 | } 3317 | else if(curSum < sum){ 3318 | pleft++; 3319 | } 3320 | else{ 3321 | pright--; 3322 | } 3323 | } 3324 | return result; 3325 | } 3326 | }; 3327 | ``` 3328 | 3329 | 3330 | 3331 | 3332 | #### 9. 限制运算(3) 3333 | 3334 | ##### 1. 045-扑克牌顺子 3335 | **题目描述** 3336 | 3337 | LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张😊)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何。为了方便起见,你可以认为大小王是0。 3338 | 3339 | **解题思路** 3340 | 3341 | 这题说了一堆,提取主要信息,我们不难整理出,满足如下条件才可以认为是顺子: 3342 | 3343 | - 输入数据个数为5; 3344 | - 输入数据都在0-13之间; 3345 | - 没有相同的数字; 3346 | - 最大值与最小值的差值不大于5。 3347 | 3348 | PS:大小王可以当成任意数。 3349 | 3350 | 这里可以使用一个技巧,即利用一个flag记录每个数字出现的次数。具体实现直接看代码吧,代码有详细注释。 3351 | 3352 | **参考代码** 3353 | 3354 | ```c++ 3355 | class Solution { 3356 | public: 3357 | bool IsContinuous( vector numbers ) { 3358 | if(numbers.size() < 5){ 3359 | return false; 3360 | } 3361 | int max = -1, min = 14; 3362 | int flag = 0; 3363 | for(int i = 0; i < numbers.size(); i++){ 3364 | int curNum = numbers[i]; 3365 | if(curNum < 0 || curNum > 13){ 3366 | return false; 3367 | } 3368 | // 大小王,可以模拟任意数 3369 | if(curNum == 0){ 3370 | continue; 3371 | } 3372 | 3373 | // 如果数字出现了一次 3374 | if((flag >> curNum) & 1 == 1){ 3375 | return false; 3376 | } 3377 | 3378 | // 按位保存数字出现次数,比如0110表示,0出现0次,1出现1次,2出现1次,3出现0次。 3379 | flag |= 1 << curNum; 3380 | 3381 | // 更新最小值 3382 | if(curNum < min){ 3383 | min = curNum; 3384 | } 3385 | // 更新最大值 3386 | if(curNum > max){ 3387 | max = curNum; 3388 | } 3389 | // 超过范围一定不是顺子 3390 | if(max - min >= 5){ 3391 | return false; 3392 | } 3393 | } 3394 | return true; 3395 | } 3396 | }; 3397 | ``` 3398 | 3399 | 3400 | 3401 | ##### 2. 048-不用加减乘除的加法 3402 | **题目描述** 3403 | 3404 | 写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。 3405 | 3406 | **解题思路** 3407 | 3408 | 首先看十进制是如何做的: 5+7=12, 3409 | 3410 | 可以使用三步走: 3411 | 3412 | 第一步:相加各位的值,不算进位,得到2。 3413 | 3414 | 第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。 3415 | 3416 | 第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。 同样我们可以 3417 | 3418 | 三步走的方式计算二进制值相加: 5-101,7-111 3419 | 3420 | 第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。 3421 | 3422 | 第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。 3423 | 3424 | 第三步:重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。 3425 | 3426 | 继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。 3427 | 3428 | **参考代码** 3429 | 3430 | ```c++ 3431 | class Solution { 3432 | public: 3433 | int Add(int num1, int num2) 3434 | { 3435 | return num2 ? Add(num1 ^ num2, (num1 & num2) << 1) : num1; 3436 | } 3437 | }; 3438 | ``` 3439 | 3440 | 3441 | #### 10. 找规律(4) 3442 | 3443 | ##### 1. 019-顺时针打印矩阵 3444 | **题目描述** 3445 | 3446 | 输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字,例如,如果输入如下矩阵: 3447 | 3448 | ![basis_19_1](assets/basis_19_1.jpg) 3449 | 3450 | 则依次打印出数组:1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10。 3451 | 3452 | **解题思路** 3453 | 3454 | 将结果存入vector数组,从左到右,再从上到下,再从右到左,最后从下到上遍历。 3455 | 3456 | **参考代码** 3457 | 3458 | ```c++ 3459 | class Solution { 3460 | public: 3461 | vector printMatrix(vector > matrix) { 3462 | int rows = matrix.size(); //行数 3463 | int cols = matrix[0].size(); //列数 3464 | vector result; 3465 | 3466 | if(rows == 0 && cols == 0){ 3467 | return result; 3468 | } 3469 | int left = 0, right = cols - 1, top = 0, bottom = rows - 1; 3470 | 3471 | while(left <= right && top <= bottom){ 3472 | //从左到右 3473 | for(int i = left; i <= right; ++i){ 3474 | result.push_back(matrix[top][i]); 3475 | } 3476 | //从上到下 3477 | for(int i = top + 1; i <= bottom; ++i){ 3478 | result.push_back(matrix[i][right]); 3479 | } 3480 | //从右到左 3481 | if(top != bottom){ 3482 | for(int i = right - 1; i >= left; --i){ 3483 | result.push_back(matrix[bottom][i]); 3484 | } 3485 | } 3486 | //从下到上 3487 | if(left != right){ 3488 | for(int i = bottom - 1; i > top; --i){ 3489 | result.push_back(matrix[i][left]); 3490 | } 3491 | } 3492 | left++, top++, right--, bottom--; 3493 | } 3494 | return result; 3495 | } 3496 | }; 3497 | ``` 3498 | 3499 | 3500 | 3501 | ##### 2. 031-从1到n整数中1出现的次数 3502 | **题目描述** 3503 | 3504 | 输入一个整数n,求从1到n这n个整数的十进制表示中1出现的次数。例如输入12,从1到12这些整数中包含1的数字有1,10,11和12,1一共出现了5次。 3505 | 3506 | **解题思路** 3507 | 3508 | 两种方法,一种是从1到n遍历,每次通过对10求余数判断整数的个位数字是不是1,大于10的除以10之后再判断。我们对每个数字都要做除法和求余运算以求出该数字中1出现的次数。如果输入数字n,n有O(logn)位,我们需要判断每一位是不是1,那么时间复杂度为O(n*logn)。这样做,计算量大,效率不高。 3509 | 3510 | 本文采用数学之美上面提出的方法,设定整数点(如1、10、100等等)作为位置点i(对应n的各位、十位、百位等等),分别对每个数位上有多少包含1的点进行分析。 3511 | 3512 | - 根据设定的整数位置,对n进行分割,分为两部分,高位n/i,低位n%i 3513 | - 当i表示百位,且百位对应的数>=2,如n=31456,i=100,则a=314,b=56,此时百位为1的次数有a/10+1=32(最高两位0~31),每一次都包含100个连续的点,即共有(a/10+1)*100个点的百位为1 3514 | - 当i表示百位,且百位对应的数为1,如n=31156,i=100,则a=311,b=56,此时百位对应的就是1,则共有a/10(最高两位0-30)次是包含100个连续点,当最高两位为31(即a=311),本次只对应局部点00~56,共b+1次,所有点加起来共有(a/10*100)+(b+1),这些点百位对应为1 3515 | - 当i表示百位,且百位对应的数为0,如n=31056,i=100,则a=310,b=56,此时百位为1的次数有a/10=31(最高两位0~30) 3516 | - 综合以上三种情况,当百位对应0或>=2时,有(a+8)/10次包含所有100个点,还有当百位为1(a%10==1),需要增加局部点b+1 3517 | - 之所以补8,是因为当百位为0,则a/10==(a+8)/10,当百位>=2,补8会产生进位位,效果等同于(a/10+1) 3518 | 3519 | **参考代码** 3520 | 3521 | ```c++ 3522 | class Solution { 3523 | public: 3524 | int NumberOf1Between1AndN_Solution(int n) 3525 | { 3526 | // 统计次数 3527 | int count = 0; 3528 | for(int i = 1; i <= n; i *= 10){ 3529 | // 计算高位和低位 3530 | int a = n / i, b = n % i; 3531 | count += (a + 8) / 10 * i + (a % 10 == 1) * (b + 1); 3532 | } 3533 | return count; 3534 | } 3535 | }; 3536 | ``` 3537 | 3538 | 3539 | 3540 | ##### 3. 033-丑数 3541 | **题目描述** 3542 | 3543 | 把只包含因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。 3544 | 3545 | **解题思路** 3546 | 3547 | 所谓的一个数m是另一个数n的因子,是指n能被m整除,也就是n%m==0。根据丑数的定义,丑数只能被2、3和5整除。根据丑数的定义,丑数应该是另一个丑数乘以2、3或者5的结果(1除外)。因此我们可以创建一个数组,里面的数字是排好序的丑数,每一个丑数都是前面的丑数乘以2、3或者5得到的。 3548 | 3549 | 这个思路的关键问题在于怎样保证数组里面的丑数是排好序的。对乘以2而言,肯定存在某一个丑数T2,排在它之前的每一个丑数乘以2得到的结果都会小于已有最大的丑数,在它之后的每一个丑数乘以乘以2得到的结果都会太大。我们只需要记下这个丑数的位置,同时每次生成新的丑数的时候,去更新这个T2。对乘以3和5而言,也存在着同样的T3和T5。 3550 | 3551 | **参考代码** 3552 | 3553 | ```c++ 3554 | class Solution { 3555 | public: 3556 | int GetUglyNumber_Solution(int index) { 3557 | if(index < 7){ 3558 | return index; 3559 | } 3560 | vector res(index); 3561 | for(int i = 0; i < 6; i++){ 3562 | res[i] = i + 1; 3563 | } 3564 | int t2 = 3, t3 = 2, t5 = 1; 3565 | for(int i = 6; i < index; i++){ 3566 | res[i] = min(res[t2] * 2, min(res[t3] * 3, res[t5] * 5)); 3567 | while(res[i] >= res[t2] * 2){ 3568 | t2++; 3569 | } 3570 | while(res[i] >= res[t3] * 3){ 3571 | t3++; 3572 | } 3573 | while(res[i] >= res[t5] * 5){ 3574 | t5++; 3575 | } 3576 | } 3577 | return res[index - 1]; 3578 | } 3579 | }; 3580 | ``` 3581 | 3582 | 3583 | 3584 | ##### 4. 046-孩子们的游戏(圆圈中最后剩下的数) 3585 | **题目描述** 3586 | 3587 | 每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1) 3588 | 3589 | **解题思路** 3590 | 3591 | 如果只求最后一个报数胜利者的话,我们可以用数学归纳法解决该问题,为了讨论方便,先把问题稍微改变一下,并不影响原意: 3592 | 3593 | 问题描述:n个人(编号0~(n-1)),从0开始报数,报到(m-1)的退出,剩下的人继续从0开始报数。求胜利者的编号。 3594 | 3595 | 我们知道第一个人(编号一定是m%n-1) 出列之后,剩下的n-1个人组成了一个新的约瑟夫环(以编号为k=m%n的人开始): 3596 | 3597 | k k+1 k+2 ... n-2, n-1, 0, 1, 2, ... k-2并且从k开始报0。 3598 | 3599 | 现在我们把他们的编号做一下转换: 3600 | 3601 | k --> 0 3602 | 3603 | k+1 --> 1 3604 | 3605 | k+2 --> 2 3606 | 3607 | ... 3608 | 3609 | ... 3610 | 3611 | k-2 --> n-2 3612 | 3613 | k-1 --> n-1 3614 | 3615 | 变换后就完完全全成为了(n-1)个人报数的子问题,假如我们知道这个子问题的解: 3616 | 3617 | 例如x是最终的胜利者,那么根据上面这个表把这个x变回去不刚好就是n个人情况的解吗? 3618 | 3619 | 变回去的公式很简单,相信大家都可以推出来:x'=(x+k)%n。 3620 | 3621 | 令f[i]表示i个人玩游戏报m退出最后胜利者的编号,最后的结果自然是f[n]。 3622 | 3623 | 递推公式: 3624 | 3625 | f[1]=0; 3626 | 3627 | f[i]=(f[i-1]+m)%i; (i>1) 3628 | 3629 | **参考代码** 3630 | 3631 | ```c++ 3632 | class Solution { 3633 | public: 3634 | int LastRemaining_Solution(int n, int m) 3635 | { 3636 | if(n < 1 || m < 1){ 3637 | return -1; 3638 | } 3639 | int last = 0; 3640 | for(int i = 2; i <= n; i++){ 3641 | last = (last + m) % i; 3642 | } 3643 | return last; 3644 | } 3645 | }; 3646 | ``` 3647 | 3648 | 3649 | 3650 | --- 3651 | 3652 | ### 三、总结 3653 | 3654 | 1.vector如果需要用长度,需要先定义,如vector array(100) 3655 | 3656 | 如果未定义长度vector array; 那么在插入值之前使用array[m] (其中m>0)会数组越界。 3657 | 3658 | 2.区分vector array[100]和vector array(100) 3659 | 3660 | 3.char *str不能像string一样调用.size()/.length(),通过while(str[i++] != '\0')来获取其长度 3661 | 3662 | 3663 | 3664 | ### 四、参考资料 3665 | 3666 | [剑指offer系列刷题笔记汇总]() 3667 | 3668 | [CS-Notes-剑指offer]() 3669 | 3670 | 何海涛. 剑指 Offer[M]. 电子工业出版社, 2012. 3671 | --------------------------------------------------------------------------------