最近开始刷LeetCode,回顾了一下被我遗忘在角落里的数据结构和算法,包括java的基础语法。为了避免刷完了题又忘,所以在这里总结一下做过的题目,文中出现的所有代码均是用java编写,有不对的地方欢迎指正。
“业精于勤,荒于嬉;行成于思,毁于随。” 每天保持思考、学无止境、持续更新……
(以下内容均为本人总结内容,仅供学习参考)
LeetCode经典算法题目二(树、排序、查找、动态规划、回溯、贪心)
回文数是指:正序和倒序都是一样的数。
算法: 初始化y(倒序)为0,循环计算: y=y*10+rem(原数除以10的余数)
与回文数算法思想相同,但需要判断32位整数是否溢出,因为int类型占4个字节,取值范围为:-2147483648~2147483647,所以有可能出现原数为1999999999,如果反转就溢出了。
解决办法: 对结果变量定义为long类型,int 32位 long 64位,判断if((int)y == y)
用switch case语句将罗马数字与整数一一对应即可,需要考虑一下两个罗马数字组合的特殊情形,细节不再描述。
记一些有关字符串、数组和列表的常用方法:
1.int length(); //注意:字符串中是length()方法,数组中是length属性
2.char charAt(int index); //取字符串中索引为index的字符元素
3.String indexOf(String str2); //该方法在str1中检索str2,返回其在str1中第一次出现的位置索引,若找不到则返回-1。
4.String substring(int beginIndex, int endIndex); //截取索引为 [beginIndex,endIndex) 的字符串
5.char[] toCharArray(); //把字符串转化为字符数组
6.System.arraycopy(原数组,原数组的开始位置,目标数组,目标数组的开始位置,拷贝的元素个数); //(二/6)属于java.lang.System包中。
7.Arrays.sort(nums); //(二/6)关于Arrays类中封装的排序方法(采用归并排序)。
//我在java api中整理了一些常用的,这里出现的数组类型都以int作为示例,当然其他的基本数组类型都是通用的。(见下图)
8.String replace(char oldChar, char newChar); //(一/5)替换字符串中字符或者字符串,char也可以是String
题目描述:
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 “”。
算法一: 横向+纵向扫描,for循环嵌套。第一个for循环纵向扫描首个字符串的每一位字符,第二个for循环横向扫描数组中剩余字符串的对应位,若扫描到任意字符串与第一个字符串对应位的字符不同,则跳出两层for循环,用标记符号:break outter;
实现。由于两个for循环,时间复杂度较高,性能较差。
将相同的字符添加到字符串str结尾。
StringBuilder sb = new StringBuilder(str); //创建一个StringBuilder对象
sb.append( c); //再添加字符到尾部
str = sb.toString(); //转化成字符串
考虑数组越界: 因为输入的字符串数组的元素可能为0,即啥也不输入。所以要有判断if(strs.length==0)
的语句(strs是字符串数组,String[] strs)
算法二: 横向扫描。锁定第一个字符串str,把它作为比较对象与剩余的字符串进行对比,每一次对比的是整个str ,若任意字符串不能检索到它,则让str长度减一,减去尾部字符,如此从后往前逐次去掉尾部不相同的字符,最终找到一个与剩余字符串相同的最长前缀。
String str = strs[0];
for(i=1;i<strs.length;i++){
while(strs[i].indexOf(str) != 0){ //由于要找的是公共前缀,因此这里需要判断第一次出现的索引是否为0
str = str.substring(0,str.length()-1); // 截取减掉尾字符后剩余的字符串
}
}
如果使用for循环,在小于等于x的范围内逐个找值的话,虽然是一种解决办法,但是运行会超出时间限制。
计算平方根最好的办法是牛顿迭代法
class Solution {
public int mySqrt(int x) {
double x0,x1;
x0=x;
x1=(x0+x/x0)/2;
while(Math.abs(x0-x1)>=1){
x0=x1;
x1=(x0+x/x0)/2;
}
return (int)x1;
}
}
我没自己写算法实现,直接用的java封装好的方法str1.indexOf(str2)在str1中检索str2,个人觉得没有太大必要费工夫来写这个,因为indexOf()太好用且常用了。
算法: 将原字符串倒序循环依次取每个字符,遇到不为空格的字符则长度计数m加1;遇到空格字符但是m等于0(m初始化为0),则继续循环;遇到空格字符且m大于0,则退出while循环,返回m的值。
算法: 双指针。将left和right指向的元素互换,不断往中间走。
(我用的也是首尾元素互换的思想,但是运行要比双指针慢一点。可能因为循环条件里面我写的i
class Solution {
public void reverseString(char[] s) {
int len=s.length;
int left=0,right=len-1;
char tem;
while(left<right){ //当左右指针走到中间就结束(奇偶都成立)
tem = s[left];
s[left] = s[right];
s[right] = tem;
left++;
right--;
}
}
}
题目描述:请实现一个函数,把字符串 s 中的每个空格替换成"%20"。
方法一: 使用String类的方法:replace(String old,String new)
;
class Solution {
public String replaceSpace(String s) {
return s.replace(" ","%20");
}
}
方法二: 使用StringBuilder
在尾部逐个添加字符。
class Solution {
public String replaceSpace(String s) {
StringBuilder sb = new StringBuilder();
for(int i=0;i<s.length();i++){
if(s.charAt(i) == ' ')
sb.append("%20");
else
sb.append(s.charAt(i));
}
return sb.toString();
}
}
题目描述:请你来实现一个 atoi 函数,使其能将字符串转换成整数。
首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。接下来的转化规则如下:
1、如果第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字字符组合起来,形成一个有符号整数。
2、假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成一个整数。
3、该字符串在有效的整数部分之后也可能会存在多余的字符,那么这些字符可以被忽略,它们对函数不应该造成影响。
*注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换,即无法进行有效转换。在任何情况下,若函数不能进行有效的转换时,请返回 0 。
*提示:
本题中的空白字符只包括空格字符 ’ ’ 。
假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 − 1) 或 INT_MIN (−231) 。
调用Character
类的静态方法:static boolean isDigit(char c)
可以判断当前字符是否为数字,如果为数字,再使用:int digit = c - '0';
来实现字符转换为数字,从而可以使用:num = num * 10 + digit;
来实现将字符串转换为整数。
int的最大值:Integer.MAX_VALUE
int的最小值:Integer.MIN_VALUE
class Solution {
public int myAtoi(String str) {
int len = str.length();
int index=0;
while(index<len){ //先处理前面的空格字符
if(str.charAt(index)!=' ')
break;
index++;
}
boolean negative = false; //负号标识符
if(index<len && str.charAt(index)=='-'){ //负数
negative = true;
index++;
}
else if(index<len && str.charAt(index)=='+'){ //正数
index++;
}
int num=0;
while(index<len && Character.isDigit(str.charAt(index))){ //若当前字符为数字
int digit = str.charAt(index) - '0'; //字符转数字
if(num > (Integer.MAX_VALUE-digit)/10){ //若加上这个数字后超出int的范围
return negative?Integer.MIN_VALUE:Integer.MAX_VALUE; //返回最大或最小
}
num = num * 10 + digit;//字符串转整数
index++;
}
return negative?-num:num;
}
}
题目描述:已有方法 rand7 可生成 1 到 7 范围内的均匀随机整数,试写一个方法 rand10 生成 1 到 10 范围内的均匀随机整数。
不要使用系统的 Math.random() 方法。
/**
* The rand7() API is already defined in the parent class SolBase.
* public int rand7();
* @return a random integer in the range 1 to 7
*/
class Solution extends SolBase {
public int rand10() {
int num = (rand7()-1) * 7 + rand7();
while(num > 40){
num = (rand7()-1) * 7 + rand7();
}
return num%10 + 1;
}
}
题目描述:给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。不要使用额外的数组空间,你必须在原地修改输入数组并在使用 O(1) 额外空间的条件下完成。
算法: 利用双指针:慢指针i和快指针j。i指向的元素才是最终的新数组的元素。
时间复杂度:O(n). 假设数组的长度是 n,那么 i 和 j 分别最多遍历 n 步。
空间复杂度:O(1)
class Solution {
public int removeDuplicates(int[] nums) {
if(nums.length==0)
return 0;
int i=0;
for(int j=1;j<nums.length;j++){
if(nums[i]!=nums[j]) //若num[i]!=nums[j],则把j指向的值赋给i的下一个元素
nums[++i]=nums[j];
}
return i+1;
}
}
注意:for(j;j
跟第7题是同样的算法。利用双指针,不创建新数组,指针i指向的元素是最终新数组的结果,返回 i+1即新数组的长度。
题目描述:在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
算法思想: 使用哈希集合HashSet去重,利用add()
方法来判断要添加的元素是否重复,若返回值为true则说明没有重复,可以添加;若返回值为false,则说明已存在,无法继续添加,那么就是我们找的重复数字。
时间复杂度:O(n). 遍历数组一遍。使用哈希集合添加元素的时间复杂度为 O(1),故总的时间复杂度是 O(n)。
空间复杂度:O(n)。不重复的每个元素都可能存入集合,因此占用O(n)额外空间。
class Solution {
public int findRepeatNumber(int[] nums) {
HashSet<Integer> hs = new HashSet<>();
int repeat=nums[0];
for(int i=0;i<nums.length;i++){
if(!hs.add(nums[i])){ //若无法往HashSet中添加,说明该数字重复,返回它即可。
repeat = nums[i];
break;
}
}
return repeat;
}
}
题目描述:在一个升序排序的数组中找到给的目标值,并返回其索引,没有找到则返回它应该插入的位置。
算法: 从头扫描数组,如果target > nums[i]
则不用管,继续往后扫描,如果target <= nums[i]
则返回 i,若循环结束则返回数组长度(即目标值应该添加在末尾)。
class Solution {
public int searchInsert(int[] nums, int target) {
int i;
for(i=0;i<nums.length;i++){
if(target <= nums[i])
return i;
}
return i;
}
}
题目描述:给定一个整型数组,代表一个非负的整数。结果返回这个整数值加一的数(由数组表示)。
算法: 用一个while循环解决,从后往前 扫描原数组:(3种情况)
1.如果当前位的数字小于9,则只对当前位的值加一,返回数组。
2.否则让该位数字为0,继续往前取下一位数字,小于9则……否则……,直到遇到情况1执行return语句。
3.如果while循环结束都还没执行return,则说明原数组每一位都为9,那么每一位数值都加一后,数组的长度也应该加一,并且数组首元素应该为"0",即变为:10000….
class Solution {
public int[] plusOne(int[] digits) {
int n = digits.length;
int x;
while(n>0){
x = digits[n-1];
if(x < 9){
digits[n-1] = x +1;
return digits;
}
digits[n-1] = 0;
n--;
}
int[] nums = new int[digits.length + 1];
nums[0] = 1;
return nums;
}
}
题目描述:给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使得 num1 成为一个有序数组。
说明:
1.初始化 nums1 和 nums2 的元素数量分别为 m 和 n。
2.你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
算法一: 双指针。指针 i 依次扫描数组nums1,指针 j 扫描数组nums2,在while循环中每次都把nums[i++]与nums[j++]的值进行比较,较小的那个给nums[k++] (新建的数组,用来存储合并后的结果),当 i 扫描完或者 j 扫描完后,退出循环,让没扫描完的那个数组剩余的元素直接循环添加到nums[k]的后面,得到最终合并后的有序数组,再把nums拷贝给nums1即可。
注意: java中拷贝数组不能直接用"=",直观的方法是可以使用for循环将原数组的每个元素给到目标数组。但是比较便捷的是使用本地方法:
System.arraycopy(原数组,原数组的开始位置,目标数组,目标数组的开始位置,拷贝的元素个数);
该方法可以将原数组的某些元素直接拷贝到目标数组的某个位置,使用该方法的前提必须是目标数组是一个已经分配内存单元
的数组。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int i=0,j=0,k=0;
int[] nums=new int[m+n];
while(i<m && j<n){ //任意一个数组扫描完后都结束循环
if(nums1[i]<nums2[j]){
nums[k]=nums1[i]; //把较小的元素给nums[k]
i++;}
else{
nums[k]=nums2[j]; //把较小的元素给nums[k]
j++;}
k++;
}
while(j<n) //让没扫描完的数组nums2剩余的元素直接循环添加到nums[k]的后面
nums[k++]=nums2[j++];
while(i<m) //让没扫描完的数组nums1剩余的元素直接循环添加到nums[k]的后面
nums[k++]=nums1[i++];
System.arraycopy(nums,0,nums1,0,m+n); //将排序后的元素拷贝到nums1目标数组中
}
}
算法二:
Arrays.sort(nums1);
直接对num1数组进行升序排序。算法二代码只有两行,非常简洁,但实际上这个方法并没有有效地利用到两个原数组均有序这一特点,所以时间复杂度较差,为O((m+n)log(m+n))
题目描述:从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的。2~10为数字本身,A为1,J为11,Q为12,K为13,而大、小王为 0 ,可以看成任意数字。A 不能视为 14。
算法一: 排序
class Solution {
public boolean isStraight(int[] nums) {
Arrays.sort(nums); //java的排序方法
int i=0,j=0;
while(nums[i]==0){ //计算数组中‘0’的个数
j++;
i++;
}
i++;
while(i<nums.length){
if(nums[i]==nums[i-1]) //有重复的直接返回false
return false;
if(nums[i]>(nums[i-1]+1)) //计算不连续两数的差值,看‘0’够不够填充
j = j - nums[i] + nums[i-1] +1;
i++;
}
if(j>=0) //如果‘0’的剩余个数大于等于0,说明够填充了,返回真值
return true;
else
return false;
}
}
算法二: 不排序
class Solution {
public boolean isStraight(int[] nums) {
int max,min,i=0;
max=nums[i];
while(nums[i]==0){ //找到第一个不为0的元素,将它赋给min
i++;
}
min=nums[i];
for(i=0;i<nums.length;i++){
if(nums[i]!=0){ //在不为0的前提下进行查找最大值和最小值
max = Math.max(max,nums[i]);
min = Math.min(min,nums[i]);
for(int j=i+1;j<nums.length;j++){ //for循环嵌套,检查是否有重复元素
if(nums[i]==nums[j])
return false;
}
}
}
if(max - min <= 4) //在无重复元素的情况下,最大值和最小值差值若等于4,说明刚好是顺子;若小于4,说明0的个数肯定足够填充
return true;
else
return false;
}
}
题目描述:给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
算法一: 暴力法。两个for循环遍历,依次对数组中的每一个元素都去求它后面的每个元素与它的差值,总是保留差值最大的那个。很耗时,时间复杂度为O(n*n)
class Solution {
public int maxProfit(int[] prices) {
int m=0;
for(int i=0;i<prices.length;i++){
for(int j=i+1;j<prices.length;j++){
m = Math.max(m,prices[j]-prices[i]); //保留最大差值
}
}
return m;
}
}
算法二: 峰谷法。仅一次遍历,时间复杂度O(n)
两个变量,一个存波谷值,一个存最大利润。遍历过程中如果数组元素值比现存波谷值更小,就更新波谷值,并且每次都要更新最大利润值。
class Solution {
public int maxProfit(int[] prices) {
if(prices.length==0)
return 0;
int low=prices[0], profit=0; //初始化波谷为数组第一个元素,最大利润为0
for(int i=1;i<prices.length;i++){
if(prices[i]<low)
low = prices[i];
profit = Math.max(profit,prices[i]-low); //更新最大利润变量的值
}
return profit;
}
}
链表节点有两部分信息:
第一部分是节点保存的值,第二部分是指向的下一个节点的地址。
public class ListNode{
Object data; //每个节点的数据
ListNode next; //每个节点指向下一个节点的连接
ListNode(Object data){
this.data = data;
}
}
算法: 递归。
每次递归方法都找出值最小的结点。
如果链表1或者链表2为空,则直接返回非空链表(已经是有序的)。若都有值的话就判断谁的头结点更小,让更小的头结点的next等于下一次递归返回的结点。(因为下一次递归返回的一定又是最小结点),如此每一次递归都找出当下最小的结点,依次排序链接。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) { //每次使用该方法,都能返回当前最小结点
if(l1 == null) //l1为空则返回l2结点
return l2;
else if(l2 == null) //l2为空则返回l1结点
return l1;
else if(l1.val < l2.val){ //若l1当前结点更小,则把l1提出来,再使它的下一个结点指向递归该方法返回的最小结点。
l1.next = mergeTwoLists(l1.next,l2);
return l1;
}
else{
l2.next = mergeTwoLists(l2.next,l1);
return l2;
}
}
}
算法: 比较当前结点值与下一个结点值是否相同,如果相同则将下一个结点的next赋给当前结点的next,否则继续比较下一个结点值与下下个结点值……直到head.next==null
结束。返回head结点(由于循环中head一直在变化,所以可以提前先备份一个headnew)
注意: 链表作为参数输入时一定要判空,否则会出现空指针报错。
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null)
return null;
if(head.next == null)
return head;
ListNode headnew = head;
ListNode son = headnew.next;
while(son!=null){
if(headnew.val == son.val){
headnew.next = son.next;
son = headnew.next;
}
else{
headnew=headnew.next;
son = headnew.next;
}
}
return head;
}
}
题目描述:给定一个链表,判断链表中是否有环。
算法: 双指针。快指针每次走两个结点,即:fast = fast.next.next;
,慢指针每次走一个结点,即:slow = slow.next;
,可分为:
但要注意: 避免指针报错。结点null.next是无意义的指针,运行时会报错,所以每次使用fast.next.next
时要先判断if(fast == null || fast.next == null )
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null || head.next==null) //头结点为空或者指向为空都说明无环
return false;
ListNode fast = head.next.next;
ListNode slow = head;
while(fast != slow){
if(fast == null || fast.next == null ) //快指针为空或者它的下一个为空说明无环
return false;
fast = fast.next.next; //快指针一次走两个
slow = slow.next; //慢指针一次走一个
}
return true;
}
}
题目描述:输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
算法一: 嵌套循环。外循环
是对链表中的结点依次遍历;内循环
是让当前结点往前走 k 个单位,看是否走到空(即是否走到底),若是则该结点为倒数第k个结点,若不是则继执行外循环。
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode head2;
int k2;
while(head!=null){ //外循环,依次对链表中的结点遍历
head2 = head; //每次外循环都需要对当前结点进行备份(因为内循环要改变它的值)
k2 = k; //每次外循环都需要用最初的k值
while(k2>0){ //内循环,该结点往前走k个单位
head2=head2.next;
k2--;
}
if(head2 == null) //若走到空,则退出循环
break;
head = head.next; //否则继续遍历下一个结点
}
return head;
}
}
算法二: 双指针(时间复杂度更小)。设置两个指针:before
(前指针)和 after
(后指针),刚开始使这两个指针之间相差 k 个元素,然后两个指针同时往前移动,当前指针before
移动到为null
时,此时的after
一定指向倒数第 k 个元素 。
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
ListNode before=head, after=head;
while(k>0){ //循环 k 次,使前指针before与后指针after相差 k 个元素
before = before.next;
k--;
}
while(before!=null){ //两个指针同时往前移动,直到前指针before为null结束
before = before.next;
after = after.next;
}
return after; //返回后指针after
}
}
题目描述:给定两个(单向)链表,判定它们是否相交并返回交点。请注意相交的定义基于节点的引用,而不是基于节点的值。换句话说,如果一个链表的第k个节点与另一个链表的第j个节点是同一节点(引用完全相同),则这两个链表相交。
算法: 双指针。让nodeA和nodeB分别从它们的头结点出发,一直沿着链表往下走,直到走到null
时,让nodeA从B的头结点
开始继续走,而nodeB从A的头结点
开始往下走。这样一来,nodeA指针和nodeB指针都会把链表A和链表B全部走完,而走完的结束位置就在链表的相交位置处。可以看图片说明:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode nodeA=headA;
ListNode nodeB=headB;
while(nodeA != nodeB){ //循环直到两结点相同为止
nodeA = nodeA==null ? headB : nodeA.next; //若nodeA为null则把B的头指针给它,否则把它指向的下一个指针给它
nodeB = nodeB==null ? headA : nodeB.next; ////若nodeB为null则把A的头指针给它,否则把它指向的下一个指针给它
}
return nodeA; //返回nodeA或者nodeB都是一样滴
}
}
题目描述:定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
算法一:递归
递归的核心是:让每个小问题都以同样的方式来得到解,因此可以调用自身。
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null)
return head;
ListNode tail = reverseList(head.next); //让head的子链表再去进行反转
head.next.next = head; //实现链表指针指向反转,添加反向指针
head.next = null; //让头结点的下一个指向null,断开原指向指针
return tail; //返回原尾结点
}
}
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null || head.next==null)
return head;
ListNode pre = null;
while(head!=null){
ListNode tmp = head.next; //备份当前结点的子链表
head.next = pre; //实现链表指向转向
pre = head; //更新pre的值
head = tmp; //更新head的值
}
return pre; //返回原尾结点
}
}
Q: 迭代、递归、动态规划?
迭代 是显式
的循环。是利用变量的原值推算出变量的一个新值。
从程序结构上来讲,迭代就是与普通循环。但它与普通循环的区别是,迭代是在循环代码中不断使用变量的原值递推出变量的新值,当前变量的新值又作为下次循环的初始值。也就是说,迭代时,循环代码在不同循环轮次中始终是对同一组变量做修正。
递归 是隐式
的循环。是程序调用自身,从顶部
开始分解问题,通过解决掉所有分解出来的小问题,来解决整个问题。
从程序结构上来讲,递归是指函数重复调用自身而实现的循环。这种隐式的循环结束的方式是,当程序满足终止条件时逐层返回。在循环次数较大的时候,递归的效率明显低于迭代
。递归的特点是把多阶段问题转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解。即把原问题划分为一层层的子问题,然后逐层求解。
动态规划 通常与递归相反,其从底部
开始解决问题,将所有小问题解决掉,进而解决的整个问题。
动态规划说白了就是记忆化
的递归,它把子问题的解临时存储在堆栈中,省去了重复计算的步骤,从而提高了算法效率。递归是自顶而下的,动态规划是自底而上的。所谓动态规划,可以简单理解为先用递归找出算法的本质并给出初步解然后等效的转化为迭代的形式(因为问题规模较大时,递归的效率比迭代低,所以一般采用迭代)。
题目描述:用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )
示例:
输入:
[“CQueue” , “appendTail” , “deleteHead” , “deleteHead”]
[ [] , [3] , [] , [] ] // 这一行作为上一行函数的参数
输出:[ null , null , 3 , -1]
栈 是只能在一端
进行插入或删除操作的线性表。“后进先出
”
队列 是只能在表的一端
进行插入,另一端
进行删除。“先进先出
”
思想: 创建两个栈,stack1用于存储数据
,stack2用于每次执行插入操作的暂存器
(过渡区),
stack1.push(value);
将待插入的元素入栈,再把stack2中的元素按序回到stack1中,就实现了队列在末尾插入
元素。stack1.pop()
方法删除栈顶元素,对应删除了队列的队首元素
。java api 中对 Stack 类的描述
方法和构造器:
class CQueue {
Stack<Integer> stack1;
Stack<Integer> stack2;
public CQueue() {
stack1 = new Stack<>();
stack2 = new Stack<>();
}
//在末尾添加元素
public void appendTail(int value) {
while(!stack1.empty()){ //将栈1中的所有元素暂存到栈2中
stack2.push(stack1.pop());
}
stack1.push(value); //待插入元素入栈,置于栈底位置
while(!stack2.empty()){ //将栈2中的元素还原到栈1中
stack1.push(stack2.pop());
}
}
//删除队首元素
public int deleteHead() {
if(stack1.empty())
return -1;
return stack1.pop(); //删除栈顶元素(即队首)
}
}
Java集合类型的默认容量以及扩容机制:
ArrayList
默认容量是10
最大容量Integer.MAX_VALUE - 8
(Integer.MAX_VALUE = 231-1 )
ArrayList扩容机制,按原数组长度的1.5倍
扩容。如果扩容后的大小小于实际需要的大小,将数组扩大到实际需要的大小。
Vector
是线程安全
版的ArrayList,内部实现都是用数组实现的。Vector通过在方法前用synchronized
修饰实现了线程同步功能。
默认容量是10
最大容量Integer.MAX_VALUE - 8
Vector扩容机制,如果用户没有指定扩容步长,按原数组长度的2倍
扩容,否则按用户指定的扩容步长扩容。如果扩容后的大小小于实际需要的大小,将数组扩大到实际需要的大小。
Stack
继承自Vector。添加了同步的push(E e)
、pop()
、peek()
方法,默认容量和扩容机制同Vector。
DelayQueue、PriorityQueue
非线程安全的无界
队列。
HashMap
是基于数组和链表实现的。HashMap的容量必须是2的幂次方
默认容量是16
最大容量2的30次方
HashMap扩容机制,扩容到原数组的2倍
Hashtable
默认容量是11
最大容量Integer.MAX_VALUE - 8
Hashtable扩容机制,扩容到原数组的2倍+1
算法一: 暴力消除。使用replace()
方法将成对 的"()""[]""{}"替换成 “” 空字符串,最终判断剩余的字符串是否为空,为空则说明所有括号有效。(该题不用考虑括号优先级问题)
String replace(char oldChar, char newChar); //char也可以是String
class Solution {
public boolean isValid(String s) {
if(s.length()==0)
return true;
char c=' ';
int len=0;
while(len != s.length()){
len = s.length();
s=s.replace("()",""); //将字符串中的()替换成空字符
s=s.replace("[]",""); //将字符串中的[]替换成空字符
s=s.replace("{}",""); //将字符串中的{}替换成空字符
}
return (s=="");
}
}
算法二: 利用栈。(好方法)
class Solution {
public boolean isValid(String s) {
if(s.length()==0)
return true;
Stack<Character> st = new Stack<>();
st.push(s.charAt(0)); //先把第一个元素入栈
for(int i=1;i<s.length();i++){ //遍历字符串的每一个元素
if(!st.empty()){ //如果栈不为空,可将栈顶元素与当前元素进行匹配
if(s.charAt(i) == ')' && st.peek()=='(')
st.pop();
else if(s.charAt(i) == ']' && st.peek()=='[')
st.pop();
else if(s.charAt(i) == '}' && st.peek()=='{')
st.pop();
else
st.push(s.charAt(i));
}
else //如果栈为空,就把当前元素入栈
st.push(s.charAt(i));
}
return st.empty(); //如果栈为空则返回true,否则返回false
}
}
题目描述:给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。
Java集合主要由2大体系构成,分别是Collection
体系和Map
体系,其中Collection
和Map
分别是2大体系中的顶层接口
。
Collection主要有三个子接口,分别为List(列表)
、Set(集)
、Queue(队列)
。其中,List、Queue中的元素有序可重复,而Set中的元素无序不可重复。(有关Set集在本节的最后补充)
Map同属于java.util包中,是集合的一部分,但与Collection是相互独立的,没有任何关系。Map中都是以key-value
的形式存在,其中key必须唯一,主要有HashMap、HashTable、TreeMap三个实现类。
那么下面就来看看在java api
中如何对这些接口和类进行描述的:
动态扩容
。ArrayList
出现了,它是用于数据存储
和检索
的专用类,它的大小是按照其中存储的数据来动态扩充与收缩
的。所以,我们在声明ArrayList对象时并不需要指定它的长度。它可以很方便的进行数据的添加,插入和移除。
Queue(队列)接口:
Q1: 应该用接口类型
来引用对象还是实现类的类型
来引用对象?
结论:优先使用接口而不是类来引用对象
。
但是,当你用接口类型
来引用对象时,如果某些方法仅
存在于实现类中,那么你是不能直接调用的,否则会报错。
也就是说,要使用接口
来引用对象是有条件的——你即将要使用的方法全部是接口中的方法,不能单独使用实现类独有的方法。当然,如果你想使用实现类本身的方法时,可以选择用实现类的类型来引用对象。
Q2: double 和 Double(int 和 Interger、float 和 Float、string 和 String)?
本质区别:double
是基本数据类型,Double
是封装的类。
double i = 2.45;
Double di = new Double(2.45);
list.add(i);
list.add(di);
可以!在了解了这些接口和类之后,我们就可以开始解题了。
算法: 层次遍历的广度优先搜索。
class Solution {
public List<Double> averageOfLevels(TreeNode root) {
List<Double> average = new ArrayList<>(); //实例化一个底层为数组的列表,用来存放各层平均值
Queue<TreeNode> queue = new LinkedList<>(); //实例化一个LinkedList来实现队列接口
double sum;
queue.add(root);
while(!queue.isEmpty()){ //当队列不为空时循环
int m = queue.size(); //记录每次循环时队列的大小(该层的结点数量)
sum = 0;
for(int i=1;i<=m;i++){ //只从队列中取该层的所有结点(因为每一层的结点数量就是刚开始队列的大小m)
TreeNode node = queue.poll(); //队首元素出队
sum += node.val;
if(node.left!=null) //使左结点入队
queue.add(node.left);
if(node.right!=null) //使右结点入队
queue.add(node.right);
}
average.add( sum/m ); //计算平均值并放入数组列表中
}
return average;
}
}
补充一下Set:
实际上,在看过源码后会发现,Set的实体类主要就是以map为基础,相对应的使用环境和意义也和对应的map相同。Set主要包含三种存放数据类型的变量,分别是HashSet
、LinkedHashSet
、TreeSet
.
其中,HashSet、LinkedHashSet无序且不可重复。TreeSet是以TreeMap作为存储结构的,有序不可重复。
来看看在 java api
中如何对 Set 集进行描述的:
Set 常用的方法:
注意1:若对对象进行重复添加,是没有任何作用的,重复添加多个相同对象时,Set中只保留一个,另外,添加null
空指针也是可以的。
注意2:Set中元素因为其无序性,所以不能用 get() 方法来查找,只能通过foreach()
或者iterator()
方法遍历,并且每次遍历输出的结果顺序是不一样的。
看一下Iterator
接口的描述:
Iterator 常用的方法:
Q3: 为什么会构造Set这个集合呢?
实际上就是利用Map
的key-value键值对
的方式,通过key的唯一的特性,主要将Set构建的对象放入key中,以这样的方式来使用集合的一些特性,从而可以直接用Set来进行调用。
题目描述:输入一个字符串,打印出该字符串中字符的所有排列。你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
算法: 回溯法。
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
思想: 首先把字符串转为字符数组,寻找排列方案的思路是:依次固定第0位、第1位、……、第n位字符。比如,我们都知道第0位有n种情况,若已经固定了第0位,那么第1位有n-1种情况,若已经固定了第1位,则第2位有n-2种情况,……,最后,第n位只有1种情况。所以,关键在于:固定当前位字符,对剩余的位置进行依次固定寻找排列方案,这样相当于深度优先搜索,完成后再对当前位进行循环固定,也就是说选择其他的字符来作为当前位。
执行一次dfs()
目的是固定当前第x位,进行深度优先搜索来对剩余位找排列方案。所以初始参数为0,因为首先要固定第0位,根据第0位的情况往下找,而每一个字符都要依次作为第0位,可以使用交换法来实现,把每个字符依次放在第0个位置,完了后要交换回来。由于给的字符串中有可能含有重复字符,那么排列组合就会有重复的排列组合,所以需要固定每一位、去重,如果当前位已经有重复的元素了,那么就不用算两遍。
一定要想清楚x和i分别的作用:
x
:代表固定的第x位,因此只能是它作为dfs()
的参数。
i
:遍历数组的索引,它的作用是把数组的每个字符都拿出来,作为固定的第x个位,实现方法即: swap(i,x);
交换x和i指向的元素。
时间复杂度:O(n!)
空间复杂度 :O(N2)。全排列的递归深度为 N ,系统累计使用栈空间大小为 O(N);递归中辅助 Set 累计存储的字符数量最多为 N + (N-1) + … + 2 + 1 = (N+1)N/2N + (N−1) + … + 2 + 1 = (N+1)N/2 ,即占用 O(N2)的额外空间。
class Solution {
List<String> list = new ArrayList<>();
char[] c;
public String[] permutation(String s) {
c = s.toCharArray();
dfs(0);
return list.toArray(new String[list.size()]); //将list转为数组。参考下方第一点
}
void dfs(int x){ //固定当前第x位,进行深度优先搜索来排列剩余的位。
if(x == (c.length - 1)){
list.add(String.valueOf(c)); //把当前字符串数组添加到列表中,作为一种排列方案。参考下方第二点
return;
}
HashSet<Character> hs = new HashSet<>(); //每一次dfs都创建一个HashSet,实现去重。
for(int i=x;i<c.length;i++){ //广度遍历,依次把数组中的每一个字符都当作当前第x位(交换位置实现)
if(hs.contains(c[i])){ //如果HashSet中包含了字符c[i],说明这是重复字符,当前位已经固定过了,直接跳过。
continue;
}
hs.add(c[i]);
swap(i,x); //交换,相当于选择第i个字符来作为当前固定的第x位
dfs(x+1); //深度搜索,递归,开始固定下一位。
swap(i,x); //还原数组
}
}
void swap(int i,int j){ //交换,索引值是不变的,即x和i不变,变的是它们指向的元素值。
char tmp = c[i];
c[i] = c[j];
c[j] = tmp;
}
}
1、toArray() 和 toArray(T[] a)
List和Set接口都提供了一个转数组的非常方便的方法toArray()。toArray()有两个重载的方法:
Object[] toArray();
是将list或者set直接转为Object[] 数组
。但是如果你这样写的话:String[] array= (String[])list.toArray();
运行会报错。因为java中的强制类型转换只是针对单个对象的,想要偷懒将整个数组转换成另外一种类型的数组是不行的!因此不能直接将Object[] 转化为String[],转化的话只能是取出每一个元素再转化。T[] toArray(T[] a);
是将list或者set直接转化为你所需要类型的数组
。非常好用,且常用!一般写法:String[] list_array = list.toArray(new String[list.size()]);
2、static String valueOf(char[] data)
String类的静态方法,因此可以直接通过类名调用:String.valueOf();
作用是将 字符串数组/int整型/char字符 等等转为字符串。
3、HashSet
HashSet一般常用于去重,即:去除重复元素。
题目描述:在字符串 s 中找出第一个只出现一次的字符。如果没有,返回一个单空格。
算法一: 双指针嵌套遍历。相当于把原字符串复制一份,对两个完全相同的字符串进行比较,看是否有相同字符。(当然,索引相同的情况要排除掉。)
class Solution {
public char firstUniqChar(String s) {
if(s.length()==0)
return ' ';
int i,j=0;
char c1,c2;
for(i=0;i<s.length();i++){ //第一次对字符串进行遍历
c1 = s.charAt(i);
for(j=0;j<s.length();j++){ //第二次对字符串进行遍历
c2 = s.charAt(j);
if((c1 == c2) && (i != j)) //若当前字符相同并且索引不同的话,说明在原字符串中它是重复字符,则退出内循环。
break;
}
if(j==s.length()) //如果第二次遍历完了,都没有找到相同元素,说明我们就get到了第一个只出现一次的字符。退出外循环。
break;
}
if(i<s.length() && j==s.length()) //如果第一次遍历没有超过尾字符,而第二次超过了尾字符,说明找到了第一个只出现一次的字符,返回它即可。
return s.charAt(i);
else //否则返回空字符。
return ' ';
}
}
算法二: 哈希表
class Solution {
public char firstUniqChar(String s) {
if(s.length()==0)
return ' ';
char c=' ';
HashMap<Character, Integer> hm= new HashMap<>(); //初始化一个哈希表
for(int i=0;i<s.length();i++){
c = s.charAt(i);
if(hm.containsKey(c)) //如果哈希表中存在此键,就把它的值加1
hm.put(c,hm.get(c)+1);
else //若不存在此键,就把它的值置为1
hm.put(c,1);
}
for(int j=0;j<s.length();j++){ //遍历字符串,找到第一个键所对的值为1的元素,直接返回它
c = s.charAt(j);
if(hm.get(c)==1)
return c;
}
return ' '; //若遍历结束都没有return,则返回空格字符
}
}
Q: 什么是哈希表?
哈希表 又称散列表,其基本思路是,设要存储的元素个数为n,设置一个长度为m(m>=n)的连续内存单元,以每个元素的关键字为自变量,通过一个称为哈希函数的函数,把关键字映射为内存单元的地址,并把该元素存储在这个单元中。该映射的地址也叫哈希地址,由此构造的线性表存储结构称为哈希表。
哈希表 是数组 + 链表
的数据结构,数组中存的是键值对
,链表的存在是为了解决哈希冲突。如下图所示,当对键 key12 进行哈希函数映射后,得到内存单元索引为3,然后发现索引3并不为空,那么这时候就需要添加链表来存这个键值对。
哈希表的常用方法总结:
HashMap<Integer, String> hm = new HashMap<>();
//Integer是键的类型,String是值的类型。当然可以换成其他的,比如字符类型Character等
延迟初始化
策略,当第一次put的时候,才初始化table(此时table是null)。当第一次put的时候,HashMap会判断当前table是否为空,如果是空,会调用resize()
方法进行初始化。resize()
方法会初始化一个容量大小为 16 的数组,并赋值给table。public V put(K key, V value) //插入键值对数据
public V get(Object key) //根据键值获取键值对值数据
public int size() //获取Map中键值对的个数
public boolean containsKey(Object key) //判断Map集合中是否包含键为key的键值对
boolean containsValue(Object value) //判断Map集合中是否包含值为value的键值对
public boolean isEmpty() //判断Map集合中是否没有任何键值对
public void clear() //清空Map集合中所有的键值对
public V remove(Object key) //根据键值删除Map中键值对
算法一:暴力法
两次for循环嵌套,对每一个元素都去查找剩余的其他元素,看是否相加之和为target。
class Solution {
public int[] twoSum(int[] nums, int target) {
int n,m;
int[] a=new int[2];
for(n=0;n<nums.length;n++){
for(m=n+1;m<nums.length;m++){
if(nums[n]+nums[m]==target){
a[0]=n;
a[1]=m;
break;
}
}
}
return a;
}
}
算法二:哈希表
记住,HashMap
查找的时间复杂度为O(1)
(因为它是靠计算hashcode索引地址来查找的,并非遍历)
思想:创建一个哈希表,键
存数组元素的值,值
存数组元素的索引。遍历数组,对其每一个元素都来查找哈希表中是否有元素与其相加和为target,若有则拿出来,若无则把当前元素再放入哈希表。因此,我们只需要花O(n)
时间来遍历长度为n的数组,而每次哈希表查找的时间复杂度仅为O(1)
。
class Solution {
public int[] twoSum(int[] nums, int target) {
int[] a = new int[2];
HashMap<Integer, Integer> hm = new HashMap<>();
for(int i=0;i<nums.length;i++){ //遍历数组
int x = target - nums[i];
if(hm.containsKey(x)){ //查找哈希表中是否存在目标元素
a[0] = i;
a[1] = hm.get(x);
break;
}
hm.put(nums[i], i); //最后再把当前元素放入哈希表
}
return a;
}
}
注意:一定是在每次循环的最后才能把当前元素放入哈希表。如果刚开始就把它放进去了,那么会出现这种情况:哈希表中已经存在该key了(因为数组中的元素很有可能有重复的),再放一个相同的进去的话,原key就会被覆盖,而相应地value也会被覆盖。那么就找不到例如3 + 3 = 6
的情况了。因此必须是在没有加入哈希表的前提下来进行查找才行。
记一记:
put(key, value)
方法时,若存在相同关键字key,则会直接替换原键值对。接口
,不能用new出对象;HashMap是Map接口的实现类
,可以new出对象。