徒手挖地球一周目
从这篇博文(2019年12月3日12:40:24)正式开始数据结构和算法学习,上一次学习这块儿已经是一年前学校的DS课程了。现在也已经遗忘了百分之九十九了,已然落后于很多坚持训练同学,但是俗话说得好:“积沙成塔,积木成林”,心动不如行动,就依照着LeetCode题库开展“徒手挖穿地球”计划。循序渐进、由浅及深,前面简单部分快速训练,难点知识就放慢脚步系统学习。
NO.1 两数之和 简单
思路一:暴力法 看到题,最先想到的思路就是暴力解法,直接两层for循环遍历:
public int[] twoSum(int[] nums,int target){
for (int i=0;i<nums.length;i++){
for (int j=i+1;j<nums.length;j++){
if (nums[i]+nums[j]==target) {
return new int[]{i,j};
}
}
}
throw new IllegalArgumentException("no result!");
}
时间复杂度:O(n^2)
思路二:哈希表法 通过一个哈希表来空间换时间:1.遍历nums数组,判断每个元素和目标值的差temp是否在哈希表中。2.如果在就返回当前遍历元素的下标和哈希表中temp这个key对应的value。3.如果不在就将当前遍历元素作为key、当前遍历元素下标作为value存入哈希表。
public int[] twoSum(int[] nums,int target){
Map<Integer,Integer> map =new HashMap<Integer,Integer>();
for (int i=0;i<nums.length;i++){
int temp=target-nums[i];
// 所需要的temp是否在map中,如果在就返回map中temp值对应的value(即temp值对应的下标)和i。
if (map.containsKey(temp)){
return new int[]{map.get(temp),i};
}
// temp如果不在map中,就将nums[i]作为key、下标i作为value放入map中
map.put(nums[i],i);
}
throw new IndexOutOfBoundsException("no twoSum result!");
}
时间复杂度:O(n)
NO.2 两数相加 中等
思路一:转换法 1.将两个链表先转化成int或long类型数值x和y。2.x和y相加后的值再转换成链表。
缺点:当参数中两个链表足够长时,得到的结果很有可能会超出int或long类型的范围发生溢出。
可以将x和y用BigDecimal类型来存储尽可能避免发生溢出,需要注意的是题目中链表都是逆序的:
String s1="";
String s2="";
ListNode q=l1,p=l2;
// 将两个链表转化为字符串,逆序的链表转换成正序的字符串
while (q!=null){
s1=q.val+s1;
q=q.next;
}
while (p!=null){
s2=p.val+s2;
p=p.next;
}
BigDecimal x=new BigDecimal(s1);
BigDecimal y=new BigDecimal(s2);
BigDecimal z=x.add(y);
// 将结果转换成链表,注意此时z是正序的,需要转换成逆序的链表
char[] chars=z.toString().toCharArray();
ListNode result=new ListNode(Integer.parseInt(String.valueOf(chars[chars.length-1])));
ListNode t=result;
// 因为需要链表是逆序的,所以将正序的chars从后行前转换
for (int i=chars.length-2;i>=0;i--){
ListNode temp=new ListNode(Integer.parseInt(String.valueOf(chars[i])));
t.next=temp;
t=temp;
}
return result;
思路二:初等数学法 1.因为链表本身就是逆序的,所以从后向前按位依次加。2.用一个int变量carry来记录前一位相加后得到的进位。3.如果一个链表已经遍历完毕,在后续的按位相加时,该链表的节点值就是0。4.每次按位相加之后更新进位值carry,并将进位之后的数值加入结果链表。5.两个链表遍历相加结束之后,需要再次判断进位置,防止遗漏最高位的进位。
需要注意的是每次按位相加时,不要忘记加上进位值carry。
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
// 用哑节点来简化代码,如果没有使用哑节点就需要额外的代码来初始化表头的值
ListNode dummyHead=new ListNode(0);
ListNode q=l1,p=l2,curr=dummyHead;
// 进位标志carry
int carry=0;
while (q!=null||p!=null){
// 获取节点值,如果节点为空,值就为0
int x=q==null?0:q.val;
int y=p==null?0:p.val;
// 两个节点值和进位相加
int sum=x+y+carry;
// 获取相加之后的进位值
carry=sum/10;
// 将相加后结果加入结果链表
curr.next=new ListNode(sum%10);
// 移动到下一个节点
curr=curr.next;
if (q!=null){
q=q.next;
}
if (p!=null){
p=p.next;
}
}
// 最后判断是否仍有进位,防止进位被遗漏
if (carry>0){
curr.next=new ListNode(carry);
}
// 因为第一个节点是哑节点,
return dummyHead.next;
}
时间复杂度:O(max(m,n))
NO.204 计算质数 简单
思路一:暴力法 双层for循环。1.第一层循环遍历逐个判断[2,n)。2.第二层循环判断参数是否为素数。:
public int countPrimes(int n){
int count=0;
for (int i=2;i<n;i++){
if (isPrime(i))
count++;
}
return count;
}
public boolean isPrime(int n){
for(int i=2;i<n;i++){
if (n%i==0)return false;
}
return true;
}
时间复杂度:O(n^2)
**可改进点:**例如,12=2*6、12=3*4、12=sqrt(12)*sqrt(12)、12=4*3、12=6*2,可以观察到后面就是前面两个数反过来,说明查找可以整除12的因子时只需要找到“一半”的位置即可,如果前“一半”没有可以整除的因子,那么后“一半”也没有,这个临界点“一半”就是sqrt(12)。所以上述isPrime()方法的循环条件可以写为“i*i<n”即可,该方法时间复杂度降到了O(sqrt(n))。
思路二:厄拉多塞筛法 不难想象,所有质数的倍数都不是质数。例如,2是质数,2的倍数4、6、8、10、12。。。都不是质数;3是质数,3的倍数6、9、12、15。。。都不是质数;可以看一下维基百科中一个厄拉多塞筛的gif图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AAS7gp6w-1575380399614)(https://s2.ax1x.com/2019/12/03/QMw11J.gif)]
这种方法大概就是“排除法”,每确定一个质数,就可以排除一批非质数,那么算法就可以这么写:
public int countPrimes(int n){
boolean isPrimes[]=new boolean[n];
Arrays.fill(isPrimes,true);
// 将所有质数的倍数设置为false
for (int i=2;i<n;i++){
for (int j=i;j<n;j+=i){
isPrimes[j]=false;
}
}
int count=0;
// 统计所有质数,即isPrimes[i]==true的为质数
for (int i=2;i<n;i++){
if (isPrimes[i])count++;
}
return count;
}
上述算法还存在两处冗余:
- 在本题的暴利算法下说的:只需要判断到sqrt(n)即可。
- 例如,12不是质数,所以会被设置为false,但是12既是2的倍数,也是3的倍数,所以它被标记了两次。
解决上述两处冗余后的算法:
public int countPrimes(int n){
boolean isPrimes[]=new boolean[n];
Arrays.fill(isPrimes,true);
// 只需要判断小于sqrt(n)的数是否为质数即可,所以i*i<n
for (int i=2;i*i<n;i++){
// 这样可以把质数i的整数倍都标记为false,但是仍然存在计算冗余。
// 比如n=25,i=4时算法会标记4×2=8,4×3=12等等数字,
// 但是这两个数字已经被i=2和i=3的2×4和3×4标记了。所以使用j=i*i减少此计算的冗余。
for (int j=i*i;j<n;j+=i){
isPrimes[j]=false;
}
}
int count=0;
// 统计所有质数,即isPrimes[i]==true的为质数
// 这里要注意从2开始,因为0,1不是质数
for (int i=2;i<n;i++){
if (isPrimes[i])count++;
}
return count;
}
厄尔拉塞筛法的时间复杂度:O(nloglogn)
学习算法,还是尽量去学习更优化的算法,只有这样才能从算法学习中得到更多锻炼。