基础算法题
基础算法题
- 链表
1.1反转链表
描述:
描述
给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。
数据范围: 0≤�≤10000≤n≤1000
要求:空间复杂度 �(1)O(1) ,时间复杂度 �(�)O(n) 。
如当输入链表{1,2,3}时,
经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。
以上转换过程如下图所示:
示例1
输入:
{1,2,3}
复制
返回值:
{3,2,1}
复制
示例2
输入:
{}
复制
返回值:
{}
复制
说明:
空链表则输出空
解答:
1,使用栈解决
链表的反转是老生常谈的一个问题了,同时也是面试中常考的一道题。最简单的一种方式就是使用栈,因为栈是先进后出的。实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点串成一个新的链表。原理如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
2,双链表求解
双链表求解是把原链表的结点一个个摘掉,每次摘掉的链表都让他成为新的链表的头结点,然后更新新链表。下面以链表1→2→3→4为例画个图来看下。
他每次访问的原链表节点都会成为新链表的头结点,最后再来看下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
3,递归解决
我们再来回顾一下递归的模板,终止条件,递归调用,逻辑处理。
1 2 3 4 5 6 7 8 9 10 11 |
|
终止条件就是链表为空,或者是链表没有尾结点的时候,直接返回
1 2 |
|
递归调用是要从当前节点的下一个结点开始递归。逻辑处理这块是要把当前节点挂到递归之后的链表的末尾,看下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
因为递归调用之后head.next节点就会成为reverse节点的尾结点,我们可以直接让head.next.next = head;,这样代码会更简洁一些,看下代码
1 2 3 4 5 6 7 8 |
|
这种递归往下传递的时候基本上没有逻辑处理,当往回反弹的时候才开始处理,也就是从链表的尾端往前开始处理的。我们还可以再来改一下,在链表递归的时候从前往后处理,处理完之后直接返回递归的结果,这就是所谓的尾递归,这种运行效率要比上一种好很多
1 2 3 4 5 6 7 8 9 10 11 |
|
尾递归虽然也会不停的压栈,但由于最后返回的是递归函数的值,所以在返回的时候都会一次性出栈,不会一个个出栈这么慢。但如果我们再来改一下,像下面代码这样又会一个个出栈了
1 2 3 4 5 6 7 8 9 10 11 12 |
|
我把部分算法题整理成了PDF文档,截止目前总共有900多页,大家可以下载阅读
链接:百度网盘 请输入提取码
提取码:6666
- 二分查找排序
2.1、二分查找-I
描述
数据范围:0≤���(����)≤2×1050≤len(nums)≤2×105 , 数组中任意值满足 ∣���∣≤109∣val∣≤109
进阶:时间复杂度 �(log�)O(logn) ,空间复杂度 �(1)O(1)
示例1
输入:
[-1,0,3,4,6,10,13,14],13
复制
返回值:
6
复制
说明:
13 出现在nums中并且下标为 6
示例2
输入:
[],3
复制
返回值:
-1
复制
说明:
nums为空,返回-1
示例3
输入:
[-1,0,3,4,6,10,13,14],2
复制
返回值:
-1
复制
说明:
2 不存在nums中因此返回 -1
备注:
数组元素长度在[0,10000]之间
数组每个元素都在 [-9999, 9999]之间。
解答
题目的主要信息:
- 给定一个元素升序的、无重复数字的整型数组 nums 和一个目标值 target
- 找到目标值的下标
- 如果找不到返回-1
举一反三:
学习完本题的思路你可以解决如下题目:
BM18.二维数组中的查找
BM19.寻找峰值
BM21.旋转数组
方法:二分法(推荐使用)
知识点:分治
分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的子问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并才能得到原问题的解,因此整个分治过程经常用递归来实现。
思路:
本来我们可以遍历数组直接查找,每次检查当前元素是不是要找的值。
1 2 3 |
|
但是这样这个有序的数组我们就没有完全利用起来。我们想想,若是目标值比较小,肯定在前半区间,若是目标值比较大,肯定在后半区间,怎么评价大小?我们可以用中点值作为一个标杆,将整个数组分为两个区间,目标值与中点值比较就能知道它会在哪个区间,这就是分治的思维。
具体做法:
- step 1:从数组首尾开始,每次取中点值。
- step 2:如果中间值等于目标即找到了,可返回下标,如果中点值大于目标,说明中点以后的都大于目标,因此目标在中点左半区间,如果中点值小于目标,则相反。
- step 3:根据比较进入对应的区间,直到区间左右端相遇,意味着没有找到。
图示:
Java实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
C++实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Python代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
复杂度分析:
- 时间复杂度:�(���2�)O(log2n),对长度为�n的数组进行二分,最坏情况就是取2的对数
- 空间复杂度:�(1)O(1),常数级变量,无额外辅助空间
- 二叉树
3.1、二叉树的前序遍历
描述
给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
数据范围:二叉树的节点数量满足 1≤�≤100 1≤n≤100 ,二叉树节点的值满足 1≤���≤100 1≤val≤100 ,树的各节点的值各不相同
示例 1:
示例1
输入:
{1,#,2,3}
复制
返回值:
[1,2,3]
解答
题目的主要信息:
- 给定一颗二叉树的根节点,输出其前序遍历的结果
举一反三:
学习完本题的思路你可以解决如下题目:
BM24.二叉树的中序遍历
BM25.二叉树的后序遍历
方法一:递归(推荐使用)
知识点:二叉树递归
递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。
而二叉树的递归,则是将某个节点的左子树、右子树看成一颗完整的树,那么对于子树的访问或者操作就是对于原树的访问或者操作的子问题,因此可以自我调用函数不断进入子树。
思路:
什么是二叉树的前序遍历?简单来说就是“根左右”,展开来说就是对于一颗二叉树优先访问其根节点,然后访问它的左子树,等左子树全部访问完了再访问其右子树,而对于子树也按照之前的访问方式,直到到达叶子节点。
从上述前序遍历的解释中我们不难发现,它存在递归的子问题:每次访问一个节点之后,它的左子树是一个要前序遍历的子问题,它的右子树同样是一个要前序遍历的子问题。那我们可以用递归处理:
- 终止条件: 当子问题到达叶子节点后,后一个不管左右都是空,因此遇到空节点就返回。
- 返回值: 每次处理完子问题后,就是将子问题访问过的元素返回,依次存入了数组中。
- 本级任务: 每个子问题优先访问这棵子树的根节点,然后递归进入左子树和右子树。
具体做法:
- step 1:准备数组用来记录遍历到的节点值,Java可以用List,C++可以直接用vector。
- step 2:从根节点开始进入递归,遇到空节点就返回,否则将该节点值加入数组。
- step 3:依次进入左右子树进行递归。
Java实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
C++实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Python实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
复杂度分析:
- 时间复杂度:�(�)O(n),其中�n为二叉树的节点数,遍历二叉树所有节点
- 空间复杂度:�(�)O(n),最坏情况下二叉树化为链表,递归栈深度为�n
方法二:非递归(扩展思路)
知识点:栈
栈是一种仅支持在表尾进行插入和删除操作的线性表,这一端被称为栈顶,另一端被称为栈底。元素入栈指的是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;元素出栈指的是从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
思路:
我们都知道递归,是不断调用自己,计算内部实现递归的时候,是将之前的父问题存储在栈中,先去计算子问题,等到子问题返回给父问题后再从栈中将父问题弹出,继续运算父问题。因此能够递归解决的问题,我们似乎也可以用栈来试一试。
根据前序遍历“根左右”的顺序,首先要遍历肯定是根节点,然后先遍历左子树,再遍历右子树。递归中我们是先进入左子节点作为子问题,等左子树结束,再进入右子节点作为子问题。
1 2 3 4 5 6 |
|
这里我们同样可以这样做,它无非相当于把父问题挂进了栈中,等子问题结束再从栈中弹出父问题,从父问题进入右子树,我们这里已经访问过了父问题,不妨直接将右子节点挂入栈中,然后下一轮先访问左子节点。要怎么优先访问左子节点呢?同样将它挂入栈中,依据栈的后进先出原则,下一轮循环必然它要先出来,如此循环,原先父问题的右子节点被不断推入栈深处,只有左子树全部访问完毕,才会弹出继续访问。
1 2 3 4 5 6 7 |
|
具体做法:
- step 1:优先判断树是否为空,空树不遍历。
- step 2:准备辅助栈,首先记录根节点。
- step 3:每次从栈中弹出一个元素,进行访问,然后验证该节点的左右子节点是否存在,存的话的加入栈中,优先加入右节点。
图示:
Java实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
C++实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Python代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
复杂度分析:
- 时间复杂度:�(�)O(n),其中�n为二叉树的节点数,遍历二叉树所有节点
- 空间复杂度:�(�)O(n),辅助栈空间最坏为链表所有节点数
- 堆、栈、队列
4.1、用两个栈实现队列
描述
用两个栈来实现一个队列,使用n个元素来完成 n 次在队列尾部插入整数(push)和n次在队列头部删除整数(pop)的功能。 队列中的元素为int类型。保证操作合法,即保证pop操作时队列内已有元素。
数据范围: �≤1000n≤1000
要求:存储n个元素的空间复杂度为 �(�)O(n) ,插入与删除的时间复杂度都是 �(1)O(1)
示例1
输入:
["PSH1","PSH2","POP","POP"]
复制
返回值:
1,2
复制
说明:
"PSH1":代表将1插入队列尾部
"PSH2":代表将2插入队列尾部
"POP“:代表删除一个元素,先进先出=>返回1
"POP“:代表删除一个元素,先进先出=>返回2
示例2
输入:
["PSH2","POP","PSH1","POP"]
复制
返回值:
2,1
解答
算法思想:双栈(此题已明确解题方法即双栈)
解题思路:
借助栈的先进后出规则模拟实现队列的先进先出
1、当插入时,直接插入 stack1
2、当弹出时,当 stack2 不为空,弹出 stack2 栈顶元素,如果 stack2 为空,将 stack1 中的全部数逐个出栈入栈 stack2,再弹出 stack2 栈顶元素
图解:
代码展示:
Python版本
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
JAVA版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
C++版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
复杂度分析:
时间复杂度:对于插入和删除操作,时间复杂度均为 O(1)。插入不多说,对于删除操作,虽然看起来是 O(n)O(n) 的时间复杂度,但是仔细考虑下每个元素只会「至多被插入和弹出 stack2 一次」,因此均摊下来每个元素被删除的时间复杂度仍为 O(1)。
空间复杂度O(N):辅助栈的空间,最差的情况下两个栈共存储N个元素
- 哈希
5.1、两数之和
描述
给出一个整型数组 numbers 和一个目标值 target,请在数组中找出两个加起来等于目标值的数的下标,返回的下标按升序排列。
(注:返回的数组下标从1开始算起,保证target一定可以由数组里面2个数字相加得到)
数据范围:2≤���(�������)≤1052≤len(numbers)≤105,−10≤��������≤109−10≤numbersi≤109,0≤������≤1090≤target≤109
要求:空间复杂度 �(�)O(n),时间复杂度 �(�����)O(nlogn)
示例1
输入:
[3,2,4],6
复制
返回值:
[2,3]
复制
说明:
因为 2+4=6 ,而 2的下标为2 , 4的下标为3 ,又因为 下标2 < 下标3 ,所以返回[2,3]
示例2
输入:
[20,70,110,150],90
复制
返回值:
[1,2]
复制
说明:
20+70=90
解答
思路:
从题中给出的有效信息:
- 找出下标对应的值相加为target
- 数组中存在唯一解
故此 可以使用 直接遍历 或者 hash表 来解答
方法一:直接遍历
具体做法:
循环遍历数组的每一个数,如果遍历的两数之和等于target,则返回两个数的下标;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
复杂度分析:
- 时间复杂度:O(n^2) 遍历两次数组
- 空间复杂度:O(1) 未申请额外空间
方法二 hash表
具体做法:
使用Map来降低时间复杂度,遍历数组,如果没有 (target - 当前值) 就将当前数字存入哈希表,如果有,返回该数字下标即可。
哈希表可以优化第二遍循环中对元素的检索速度,
具体过程如下图所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
复杂度分析:
- 时间复杂度:O(n) 一次遍历hash索引查找时间复杂度为O(1)
- 空间复杂度:O(n) 申请了n大小的map空间
- 递归、回溯
6.1、没有重复项数字的全排列
描述
给出一组数字,返回该组数字的所有排列
例如:
[1,2,3]的所有排列如下
[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2], [3,2,1].
(以数字在数组中的位置靠前为优先级,按字典序排列输出。)
数据范围:数字个数 0<�≤60<n≤6
要求:空间复杂度 �(�!)O(n!) ,时间复杂度 �(�!)O(n!)
示例1
输入:
[1,2,3]
复制
返回值:
[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
复制
示例2
输入:
[1]
复制
返回值:
[[1]]
解答
题目思路:
这道题目就是很典型的回溯类题目。
回溯其实也是暴力解法,但是又一些题目可以通过剪枝对算法进行优化,这道题目要找出所有的排列,其实还是比较简单的。
算法的思路主要就是:选择与撤销
例如:1开头的有,[1,2,3],接着3撤销,2撤销,然后选择3,再选择2,就有了[1,3,2]。
整体用一个图来观看整个过程
方法一:递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import java.util.*; public class Solution { // 存所有排列的集合 ArrayList<ArrayList<Integer>> res = new ArrayList<>(); public ArrayList<ArrayList<Integer>> permute(int[] num) { // 存一种排列 LinkedList<Integer> list = new LinkedList<>(); // 递归进行 backTrack(num,list); return res; }
public void backTrack(int[] num, LinkedList<Integer> list){ // 当list中的长度等于数组的长度,则证明此时已经找到一种排列了 if(list.size() == num.length){ // add进返回结果集中 res.add(new ArrayList<>(list)); return; } // 遍历num数组 for(int i = 0; i < num.length; i++){ // 若当前位置中的数已经添加过了则跳过 if(list.contains(num[i])) continue; // 选择该数 list.add(num[i]); // 继续寻找 backTrack(num,list); // 撤销最后一个 list.removeLast(); } } } |
复杂度分析:
时间复杂度: 。n为num数组的长度。
空间复杂度: 。返回的结果的空间。
方法二:不递归版
这种方法不使用递归,其实也是一个选择和撤销的过程,只是不使用递归来完成。
通过插入的方式,一次性找到所有的情况。
例如:第一次选择1,接着可以在1前面和后面插入2,则变为 1,2 和 2,1;接着可选择3,3插入到1,2中有三种分别为 3,1,2;1,3,2;1,2,3;然后3插入2,1也有三种。
其实就是找到能插的位置,同一个数可以插在不同的位置,则构成了另外的排列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class Solution { // 所有的排列结果集 ArrayList<ArrayList<Integer>> res = new ArrayList<>(); public ArrayList<ArrayList<Integer>> permute(int[] num) { ArrayList<Integer> list = new ArrayList<>(); // 先对res中加入一个空的list,给第一次插入制造一个空间。 res.add(list); // 整个循环的次数为num的元素个数 for(int i = 0; i < num.length; i++){
ArrayList<ArrayList<Integer>> tmp = new ArrayList<>(); // 遍历此时的排列结果 for(ArrayList<Integer> r:res){ // 根据集合的大小,使用for循环在可插入的位置进行插入 for(int j = 0; j < r.size()+1; j++){ // 在第j个位置插入 r.add(j,num[i]); // 此时构成新的排列集合,可能是不完整的排列集合(例如:[1,2];[2,1]这类) ArrayList<Integer> temp = new ArrayList<>(r); // 放进去tmp临时集合中 tmp.add(temp); // 将刚插入的数移除掉,为了将同样的这个插入不同的位置 r.remove(j); } } // 最后赋给res进行返回 res = new ArrayList<>(tmp); } return res; } } |
复杂度分析:
时间复杂度: 。n为num数组的长度。
空间复杂度: 。返回的结果的空间。
- 动态规划
7.1、 斐波那契数列
描述
大家都知道斐波那契数列,现在要求输入一个正整数 n ,请你输出斐波那契数列的第 n 项。
斐波那契数列是一个满足 ���(�)={1�=1,2���(�−1)+���(�−2)�>2fib(x)={1fib(x−1)+fib(x−2)x=1,2x>2 的数列
数据范围:1≤�≤401≤n≤40
要求:空间复杂度 �(1)O(1),时间复杂度 �(�)O(n) ,本题也有时间复杂度 �(����)O(logn) 的解法
输入描述:
一个正整数n
返回值描述:
输出一个正整数。
示例1
输入:
4
复制
返回值:
3
复制
说明:
根据斐波那契数列的定义可知,fib(1)=1,fib(2)=1,fib(3)=fib(3-1)+fib(3-2)=2,fib(4)=fib(4-1)+fib(4-2)=3,所以答案为3。
示例2
输入:
1
复制
返回值:
1
复制
示例3
输入:
2
复制
返回值:
1
解答
此题是非常经典的入门题了。我记得第一次遇到此题是在课堂上,老师拿来讲“递归”的(哈哈哈)。同样的类型的题还有兔子繁殖的问题。大同小异。此题将用三个方法来解决,从入门到会做。 考察知识:递归,记忆化搜索,动态规划和动态规划的空间优化。 难度:一星
#题解 ###方法一:递归 题目分析,斐波那契数列公式为:f[n] = f[n-1] + f[n-2], 初始值f[0]=0, f[1]=1,目标求f[n] 看到公式很亲切,代码秒秒钟写完。
1 2 3 4 5 6 7 |
|
优点,代码简单好写,缺点:慢,会超时 时间复杂度:O(2^n) 空间复杂度:递归栈的空间 ###
方法二:记忆化搜索 拿求f[5] 举例 
通过图会发现,方法一中,存在很多重复计算,因为为了改进,就把计算过的保存下来。 那么用什么保存呢?一般会想到map, 但是此处不用牛刀,此处用数组就好了。
1 2 3 4 5 6 7 8 9 |
|
时间复杂度:O(n), 没有重复的计算 空间复杂度:O(n)和递归栈的空间
方法三:动态规划
虽然方法二可以解决此题了,但是如果想让空间继续优化,那就用动态规划,优化掉递归栈空间。 方法二是从上往下递归的然后再从下往上回溯的,最后回溯的时候来合并子树从而求得答案。 那么动态规划不同的是,不用递归的过程,直接从子树求得答案。过程是从下往上。
1 2 3 4 5 6 7 8 9 |
|
时间复杂度:O(n) 空间复杂度:O(n) ###继续优化 发现计算f[5]的时候只用到了f[4]和f[3], 没有用到f[2]...f[0],所以保存f[2]..f[0]是浪费了空间。 只需要用3个变量即可。
1 2 3 4 5 6 7 8 9 10 |
|
时间复杂度:O(n) 空间复杂度:O(1) 完美!
- 字符串
8.1、字符串变形
描述
对于一个长度为 n 字符串,我们需要对它做一些变形。
首先这个字符串中包含着一些空格,就像"Hello World"一样,然后我们要做的是把这个字符串中由空格隔开的单词反序,同时反转每个字符的大小写。
比如"Hello World"变形后就变成了"wORLD hELLO"。
数据范围: 1≤�≤1061≤n≤106 , 字符串中包括大写英文字母、小写英文字母、空格。
进阶:空间复杂度 �(�)O(n) , 时间复杂度 �(�)O(n)
输入描述:
给定一个字符串s以及它的长度n(1 ≤ n ≤ 10^6)
返回值描述:
请返回变形后的字符串。题目保证给定的字符串均由大小写字母和空格构成。
示例1
输入:
"This is a sample",16
复制
返回值:
"SAMPLE A IS tHIS"
复制
示例2
输入:
"nowcoder",8
复制
返回值:
"NOWCODER"
复制
示例3
输入:
"iOS",3
复制
返回值:
"Ios"
解答
题目主要信息:
- 将字符串大小写反转
- 将整个字符串的所有单词位置反转
举一反三:
学会了本题的思路,你将可以解决类似的字符串问题:
BM84. 最长公共前缀
BM85. 验证IP地址
方法一:双逆转(推荐使用)
思路:
将单词位置的反转,那肯定前后都是逆序,不如我们先将整个字符串反转,这样是不是单词的位置也就随之反转了。但是单词里面的成分也反转了啊,既然如此我们再将单词里面的部分反转过来就行。
具体做法:
- step 1:遍历字符串,遇到小写字母,转换成大写,遇到大写字母,转换成小写,遇到空格正常不变。
- step 2:第一次反转整个字符串,这样基本的单词逆序就有了,但是每个单词的字符也是逆的。
- step 3:再次遍历字符串,以每个空间为界,将每个单词反转回正常。
图示:
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
C++代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
Python实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
复杂度分析:
- 时间复杂度:�(�)O(n),虽有多个循环,但是每个循环都只有一层�(�)O(n)
- 空间复杂度:�(�)O(n),res是存储变换的临时字符串,也可以直接用s直接变换,这样就为�(1)O(1)
方法二:分割字符串+栈(扩展思路)
知识点:栈
栈是一种仅支持在表尾进行插入和删除操作的线性表,这一端被称为栈顶,另一端被称为栈底。元素入栈指的是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;元素出栈指的是从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
思路:
题目要求将单词逆序,逆序我们就可以想到先进后出的栈,单词之间分开逆序我们需要整个字符串分割。
具体做法:
- step 1:遍历字符串,遇到小写字母,转换成大写,遇到大写字母,转换成小写,遇到空格正常不变。
- step 2:按照空格把字符串分割成一个个单词.
- step 3:遍历分割好的单词,将单词依次存入栈中。
- step 4:再从栈中弹出单词,拼接成字符串。
图示:
java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
C++代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
Python实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
复杂度分析:
- 时间复杂度:�(�)O(n),所有循环最多遍历一次
- 空间复杂度:�(�)O(n),栈空间的大小最坏为�(�)O(n)
- 双指针
9.1、合并两个有序的数组
描述
给出一个有序的整数数组 A 和有序的整数数组 B ,请将数组 B 合并到数组 A 中,变成一个有序的升序数组
数据范围: 0≤�,�≤1000≤n,m≤100,∣��∣<=100∣Ai∣<=100, ∣��∣<=100∣Bi∣<=100
注意:
1.保证 A 数组有足够的空间存放 B 数组的元素, A 和 B 中初始的元素数目分别为 m 和 n,A的数组空间大小为 m+n
2.不要返回合并的数组,将数组 B 的数据合并到 A 里面就好了,且后台会自动将合并后的数组 A 的内容打印出来,所以也不需要自己打印
3. A 数组在[0,m-1]的范围也是有序的
示例1
输入:
[4,5,6],[1,2,3]
复制
返回值:
[1,2,3,4,5,6]
复制
说明:
A数组为[4,5,6],B数组为[1,2,3],后台程序会预先将A扩容为[4,5,6,0,0,0],B还是为[1,2,3],m=3,n=3,传入到函数merge里面,然后请同学完成merge函数,将B的数据合并A里面,最后后台程序输出A数组
示例2
输入:
[1,2,3],[2,5,6]
复制
返回值:
[1,2,2,3,5,6]
解答
题目主要信息:
- A与B是两个升序的整型数组,长度分别为�n和�m
- 需要将数组B的元素合并到数组A中,保证依旧是升序
- 数组A已经开辟了�+�m+n的空间,只是前半部分存储的数组A的内容
举一反三:
学习完本题的思路你可以解决如下题目:
BM4. 合并两个有序链表
BM88. 判断是否是回文字符串
BM91. 反转字符串
方法:归并排序思想(推荐使用)
知识点:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路:
既然是两个已经排好序的数组,如果可以用新的辅助数组,那很容易我们可以借助归并排序的思想,将排好序的两个子数组合并到一起。但是这道题要求我们在数组A上面添加,那因为数组A后半部分相当于为空,则我们可以考虑逆向使用归并排序思想,从较大的开始排。对于两个数组每次选取较大的值,因此需要使用两个同时向前遍历的双指针。
具体做法:
- step 1:使用三个指针,i指向数组A的最大元素,j指向数组B的最大元素,k指向数组A空间的结尾处。
- step 2:从两个数组最大的元素开始遍历,直到某一个结束,每次取出较大的一个值放入数组A空间的最后,然后指针一次往前。
- step 3:如果数组B先遍历结束,数组A前半部分已经存在了,不用管;但是如果数组A先遍历结束,则需要把数组B剩余的前半部分依次逆序加入数组A前半部分,类似归并排序最后的步骤。
图示:
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
C++代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
Python实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
复杂度分析:
- 时间复杂度:�(�+�)O(n+m),其中�m、�n分别为两个数组的长度,最坏情况遍历整个数组A和数组B
- 空间复杂度:�(1)O(1),常数级变量,无额外辅助空间
- 贪心算法
10.1、分糖果问题
描述
一群孩子做游戏,现在请你根据游戏得分来发糖果,要求如下:
1. 每个孩子不管得分多少,起码分到一个糖果。
2. 任意两个相邻的孩子之间,得分较多的孩子必须拿多一些糖果。(若相同则无此限制)
给定一个数组 ���arr 代表得分数组,请返回最少需要多少糖果。
要求: 时间复杂度为 �(�)O(n) 空间复杂度为 �(�)O(n)
数据范围: 1≤�≤1000001≤n≤100000 ,1≤��≤10001≤ai≤1000
示例1
输入:
[1,1,2]
复制
返回值:
4
复制
说明:
最优分配方案为1,1,2
示例2
输入:
[1,1,1]
复制
返回值:
3
复制
说明:
最优分配方案是1,1,1
解答
题目主要信息:
- 给定一个数组,每个元素代表孩子的得分,每个孩子至少分得一个糖果
- 相邻两个位置得分高的要比得分低的分得多,得分相同没有限制
- 求最少总共需要多少糖果数
举一反三:
学习完本题的思路你可以解决如下题目:
BM89. 合并区间
BM96. 主持人调度
方法:贪心算法(推荐使用)
知识点:贪心思想
贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。
思路:
要想分出最少的糖果,利用贪心思想,肯定是相邻位置没有增加的情况下,大家都分到1,相邻位置有增加的情况下,分到糖果数加1就好。什么情况下会增加糖果,相邻位置有得分差异,可能是递增可能是递减,如果是递增的话,糖果依次加1,如果是递减糖果依次减1?这不符合最小,因为减到最后一个递减的位置可能不是1,必须从1开始加才是最小,那我们可以从最后一个递减的位置往前反向加1.
具体做法:
- step 1:使用一个辅助数组记录每个位置的孩子分到的糖果,全部初始化为1.
- step 2:从左到右遍历数组,如果右边元素比相邻左边元素大,意味着在递增,糖果数就是前一个加1,否则保持1不变。
- step 3:从右到左遍历数组,如果左边元素比相邻右边元素大, 意味着在原数组中是递减部分,如果左边在上一轮中分到的糖果数更小,则更新为右边的糖果数+1,否则保持不变。
- step 4:将辅助数组中的元素累加求和。
图示:
Java代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
C++代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Python实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
复杂度分析:
- 时间复杂度:�(�)O(n),单独遍历两次
- 空间复杂度:�(�)O(n),记录每个位置糖果数的辅助数组
- 模拟
11.1、旋转数组
描述
一个数组A中存有 n 个整数,在不允许使用另外数组的前提下,将每个整数循环向右移 M( M >=0)个位置,即将A中的数据由(A0 A1 ……AN-1 )变换为(AN-M …… AN-1 A0 A1 ……AN-M-1 )(最后 M 个数循环移至最前面的 M 个位置)。如果需要考虑程序移动数据的次数尽量少,要如何设计移动的方法?
数据范围:0<�≤1000<n≤100,0≤�≤10000≤m≤1000
进阶:空间复杂度 �(1)O(1),时间复杂度 �(�)O(n)
示例1
输入:
6,2,[1,2,3,4,5,6]
复制
返回值:
[5,6,1,2,3,4]
复制
示例2
输入:
4,0,[1,2,3,4]
复制
返回值:
[1,2,3,4]
复制
备注:
(1<=N<=100,M>=0)
解答
题目主要信息:
- 一个长度为�n的数组,将数组整体循环右移�m个位置(�m可能大于�n)
- 循环右移即最后�m个元素放在数组最前面,前�−�n−m个元素依次后移
- 不能使用额外的数组空间
举一反三:
学习完本题的思路你可以解决如下题目:
BM99. 顺时针旋转矩阵
方法:三次翻转(推荐使用)
思路:
循环右移相当于从第�m个位置开始,左右两部分视作整体翻转。即abcdefg右移3位efgabcd可以看成AB翻转成BA(这里小写字母看成数组元素,大写字母看成整体)。既然是翻转我们就可以用到reverse函数。
具体做法:
- step 1:因为�m可能大于�n,因此需要对�n取余,因为每次长度为�n的旋转数组相当于没有变化。
- step 2:第一次将整个数组翻转,得到数组的逆序,它已经满足了右移的整体出现在了左边。
- step 3:第二次就将左边的�m个元素单独翻转,因为它虽然移到了左边,但是逆序了。
- step 4:第三次就将右边的�−�n−m个元素单独翻转,因此这部分也逆序了。
图示:
Java代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
C++代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Python实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
复杂度分析:
- 时间复杂度:�(�)O(n),三次reverse函数的复杂度都最坏为�(�)O(n)
- 空间复杂度:�(1)O(1),没有使用额外的辅助空间