算法1.1
Posted 364.99°
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法1.1相关的知识,希望对你有一定的参考价值。
目录
前言
本篇博客是参考 学习视频 的笔记
1.复杂度分析
算法时间复杂度分析;算法空间复杂度分析;大O记法
1.时间复杂度分析
用来计算算法时间损耗情况
1.1.事后分析估算方法
将算法执行若干次,并计量执行算法所需要的时间
1.设置循环(如for循环),执行若干次算法
2.利用long start/end = System.currentTimeMills()
timeA = end - start
计算耗费时间
显然,此方法只适用于小型算法
1.2.时前分析估算方法
在计算机编写程序前,通过统计方法对算法耗时进行估算
一门高级语言编写的程序在计算机上运行所损耗的时间取决于:
1.算法采用的策略与方案 2.编译产生的代码质量 3.问题的输入规模 4.机器执行指令的速度
2.空间复杂度分析
用来计算算法内存占用情况
2.1.基本数据类型内存占用
单位:字节(Byte)= 8比特(bit)
类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 | 类型 | 内存 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
byte | 1 | short | 2 | int | 4 | long | 8 | float | 4 | double | 8 | boolean | 1 | char | 2 |
计算机访问内存的方式:一次一个字节
2.2.实例化对象的内存占用
Java中数组被先定为对象
Date date = new Date()
- .一个引用(机器地址)需要8个字节表示
对象变量
date
,需要8个字节表示
- 每个对象自身需要占用16个字节
除了对象内部存储的数据占用内存,对象的自身占用需要16个字节
new Date()
需要16个字节保存对象的头信息
- 当内存装不下数据时,会以8字节为单位,进行填充内存
如:现有17字节的数据需要装入16字节内存,装不下,系统将会自动增加8字节内存,也就是24个字节的内存来装着17个字节的数据
- Java中数组被限定为对象
一个原始数据类型的数组一般需要24字节的头信息(16字节自身对象开销,4字节保存长度,4字节填充空余的字节)
3.函数的渐进增长
对于函数f(n)、g(n),存在一个整数N,当n>N时,f(n)>g(n)
随着输入规模的增大:
1.算法的常数操作可以忽略不计
2.与最高次项相乘的常数可以忽略
3.算法中n的最高次幂越小,算法效率越高
4.大O记法
使用O()表示时间/空间复杂度的记法:O(f(n)) = T(n)
一般情况下,随着输入规模n的增大,T(n)增长最慢的算法为最优算法
执行次数=执行时间
对于Java这类在电脑这类拥有较大内存的计算机上运行的高级语言,讨论算法空间复杂度没有多大意义
4.1.推导大O阶的标识法的规则:
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数中,只保留高阶项
- 如果高阶项存在,且常数因子不为1,则 去除 与这个项相乘的常数
4.2.常见的大O阶
- 常数阶O(1)
int n = 999; //执行1次
int m = 0; //执行1次
- 线性阶O(n)
int n = 999; //执行1次
int m = 0; //执行1次
for (int i=0;i < n;i++)
m += i; //执行n次
- 平方阶O(n^2)
int n = 999; //执行1次
int m = 0; //执行1次
for (int i=0;i < n;i++)
m += i; //执行n次
for (int j=n;j > 0;j--)
m++; //执行n次
- 立方阶O(n^3)
- 对数阶O(logn)
int n = 999; //执行1次
int m = 0; //执行1次
for (int i=1;i <= n;i*=2)
m+=i; //执行log2(n)次
在大O分析时,我们会忽略底数,因为无论底数为多少,当随着n增大时,增长趋势一样
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3)
4.3.最坏情况分析
(没做特殊要求时)运行时间都是指在最坏情况下的运行时间
最坏情况
是一种保证,即使在最坏情况下,也能正常提供服务
如:在一个含有n个元素的列表中寻找目标元素
最好情况:第一个元素就是目标元素O(1)
平均情况:O(n/2)
最坏情况:查找的最后一个元素才为目标元素O(n)
2.递归简介
递归就是套娃
递归:
- 一个问题的解可以分解为几个子问题的解
- 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一致
- 存在终止条件
递归方法的组成部分:
- 终止条件: 什么条件下,方法不调用方法本身
- 递归条件: 什么条件下,方法会调用方法本身
递归的优缺点:
- 表达能力强,写起来很简洁
- 方法反复调用,大量入栈和出栈操作,空间复杂度较高,有堆栈溢出的问题
过多的方法调用,耗时也会增加,存在重复子问题计算问题
3.习题(递归 || 双指针)
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
使用递归求解
递归公式:
class Solution
//存储子问题已经计算过的结果
private Map<Integer,Integer> map = new HashMap<>();
public int climbStairs(int n)
/*
递归终止条件
*/
if(n == 1) return 1;
if(n == 2) return 2;
//如果map中已经有了结果,就直接返回结果
if(map.get(n) != null) return map.get(n);
//map中找不到结果,就进行计算
else
int result = climbStairs(n-1) + climbStairs(n-2);
map.put(n,result);
return result;
为什么要用HashMap?
使用HashMap用来存储子问题已经计算过的结果,防止二次计算:
循环求解,自底向上累加
根据递归公式:
可以发现规律:
class Solution
/**
循环求解,自底向上累加
*/
public int climbStairs(int n)
if(n == 1) return 1;
if(n == 2) return 2;
int pre = 1;
int curr = 2;
int sum = 0;
for (int i = 3; i <= n; i++)
sum = pre + curr;
pre = curr;
curr = sum;
return sum;
斐波那契数列: 1、1、2、3、5、8、13、21、34、……
两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路1——暴力穷举: 把数组中每个数字都与其他数字进行相加,并与目标值进行对比。
class Solution
/**
暴力穷举
*/
public int[] twoSum(int[] nums, int target)
//存两个目标索引
int[] result = new int[2];
for (int i = 0; i < nums.length; i++)
for (int j = i+1; j < nums.length; j++)
if (nums[i] + nums[j] == target)
result[0] = i;
result[1] = j;
return result;
return result;
思路2——hash表避免第二次扫描
暴力穷举的内部循环将之前一次循环扫描过的数重新扫描了一遍,很消耗时间,所以我们可以用一个HashMap来存储之前扫描过的数,减少扫描时间。
class Solution
public int[] twoSum(int[] nums, int target)
int[] result = new int[2];
int len = nums.length;
//map的key存数字,value存索引
Map<Integer,Integer> map = new HashMap<>(len - 1);
map.put(nums[0],0);
for (int i = 1; i < len; i++)
int another = target - nums[i];
if(map.containsKey(another))
return new int []i,map.get(another);
map.put(nums[i],i);
//代码是不会运行到这块儿,随便返回就行
return result;
合并两个有序数组
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
思路1——使用Arrays.sort
class Solution
/**
使用Arrays的sort方法
*/
public void merge(int[] nums1, int m, int[] nums2, int n)
for (int i = 0; i < n; i++)
nums1[m + i] = nums2[i];
Arrays.sort(nums1);
思路2——双指针
思路一中的方法并没有利用到两个给定数组的有序性,浪费大量时间重排。
public void merge(int[] nums1, int m, int[] nums2, int n)
int k = m+n;
//临时存储两数组
int[] tmp = new int[k];
//i1:数组1的指针,i2:数组2的指针
for (int i = 0, i1 = 0, i2 = 0; i < k; i++)
if (i1 >= m)//数组1数已经被取完,直接存数组2的数
tmp[i] = nums2[i2++];
else if (i2 >= n)//数组2数已经被取完,直接存数组1的数
tmp[i] = nums1[i1++];
else if (nums1[i1] > nums2[i2])//数组1>数组2
tmp[i] = nums2[i2++];
else
tmp[i] = nums1[i1++];
for (int i = 0; i < k; i++) //拷贝数据
nums1[i] = tmp[i];
思路3——双指针(不用临时数组)
取消临时数组,双指针都倒序比较两个数组的数字,从而进行排序,直接将数组2插入数组1,不需要引入临时数组。
public void merge(int[] nums1, int m, int[] nums2, int n)
int k = m+n;
for (int i = k-1, i1 = m-1, i2 = n-1; i >= 0; i--)
if (i1 < 0)//nums1已经取完,完全取nums2的值
nums1[i] = nums2[i2--];
else if (i2 < 0)
break;
else if (nums1[i1] > nums2[i2])
nums1[i] = nums1[i1--];
else
nums1[i] = nums2[i2--];
移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
思路——双指针
指针i用来遍历数组,第一次遍历指针j用来记录非0数字索引。
class Solution
public void moveZeroes(int[] nums)
if (nums == null) return;
int len = nums.length;
int j = 0;
//将非0数字都移到前面
for (int i=0; i < len; i++)
if (nums[i] != 0)
//将非0数字全部移动到 前面
nums[j++] = nums[i];
//填充0
for (int i = j; i < len; i++)
nums[i] = 0;
找到所有数组中消失的数字
给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。
进阶:你能在不使用额外空间且时间复杂度为 O(n) 的情况下解决这个问题吗? 你可以假定返回的数组不算在额外空间内。
思路1——做标记
说明:
- 下标先到0位,数字为4,将4-1=3 ==> 将下标为3的元素设为7+8 = 15
- 下标来到1位,数字为3,将3-1=2 ==> 将下标为2的元素设为2+8 = 10
- 按照上述步骤依次遍历玩整个数组
- 最终,为正数的索引即为没出现在数组中的数字
-1 是因为数组下标从0开始,而整数范围区间是从1开始
将数字+8(8是数组长度,也可以是其他任意好做标记的数字)是为了标记此数字在数组中出现过
注意: 可能出现数组中数字重复现象,故而需要判断索引元素之前是否被修改过。
class Solution
public List<Integer> findDisappearedNumbers(int[] nums)
int len = nums.length;
for (int num : nums)
int j = (num - 1)%len;//还原数值后再加,防止溢出
nums[j] += len;
List<Integer> res = new ArrayList<>();
for (int i = 0; i < len; i++)
if (nums[i] <= len)
res.add(i+1);
return res;
注意:当我们遍历到某个位置时,其中的数可能已经被增加过,因此需要对 nn 取模来还原出它本来的值。
合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路1——循环+双指针
/**
* Definition for singly-linked list.
* public class ListNode
* int val;
* ListNode next;
* ListNode()
* ListNode(int val) this.val = val;
* ListNode(int val, ListNode next) this.val = val; this.next = next;
*
*/
class Solution
public ListNode mergeTwoLists(ListNode list1, ListNode list2)
//当其中一个链表为空时,直接返回另一个链表
if (list1 == null) return list2;
if (list2 == null) return list1;
ListNode res = new ListNode(0);
ListNode tmp = res;
//比较链表排序插入
while (list1 != null && list2 != null)
if (list1.val > list2.val)
tmp.next = list2;
list2 = list2.next;
else
tmp.next = list1;
list1 = list1.next;
tmp = tmp.next;
//当其中一个链表依旧插完,另一个链表还有节点的时候,直接将tmp的指针指向链表
if (list1 != null)
tmp.next = list1;
if (list2 != null)
tmp.next = list2;
return res.next;
思路2——循环+双指针
与双指针思想类似,不过用的递归实现
/**
* Definition for singly-linked list.
* public class ListNode
* int val;
* ListNode next;
* ListNode()
* ListNode(int val) this.val = val;
* ListNode(int val, ListNode next) this.val = val; this.next = next;
*
*/
class Solution
public ListNode mergeTwoLists(ListNode list1, ListNode list2)
/**
递归
*/
//当其中一个链表为空时,直接返回另一个链表
if (list1 == null) return list2;
if (list2 == null) return list1;
if (list1.val < list2.val)
list1.next = mergeTwoLists(list1.next,list2);
return list1;
list2.next = mergeTwoLists(list1,list2.next);
return list2;
删除排序链表中的重复元素
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
思路1
因为是已排序链表,所以,相同的元素只会聚在一起,所以只需要将链表指针指向下下个节点就可以实现删除重复元素。
/**
* Definition for singly-linked list.
* public class ListNode
* int val;
* ListNode next;
* ListNode()
* ListNode(int val) this.val = val;
* ListNode(int val, ListNode next) this.val = val; this.next = next;
*以上是关于算法1.1的主要内容,如果未能解决你的问题,请参考以下文章