算法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)

类型内存类型内存类型内存类型内存类型内存类型内存类型内存类型内存
byte1short2int4long8float4double8boolean1char2

计算机访问内存的方式:一次一个字节

2.2.实例化对象的内存占用

Java中数组被先定为对象

Date date = new Date()
  1. .一个引用(机器地址)需要8个字节表示

对象变量date,需要8个字节表示

  1. 每个对象自身需要占用16个字节

除了对象内部存储的数据占用内存,对象的自身占用需要16个字节
new Date()需要16个字节保存对象的头信息

  1. 当内存装不下数据时,会以8字节为单位,进行填充内存

如:现有17字节的数据需要装入16字节内存,装不下,系统将会自动增加8字节内存,也就是24个字节的内存来装着17个字节的数据

  1. 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取代运行时间中的所有加法常数
  2. 在修改后的运行次数中,只保留高阶项
  3. 如果高阶项存在,且常数因子不为1,则 去除 与这个项相乘的常数

4.2.常见的大O阶

  1. 常数阶O(1)
int n = 999;               //执行1次
int m = 0;                 //执行1次
  1. 线性阶O(n)
int n = 999;               //执行1次
int m = 0;                 //执行1次
for (int i=0;i < n;i++)   
    m += i;                //执行n次

  1. 平方阶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次
                   

  1. 立方阶O(n^3)
  2. 对数阶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的主要内容,如果未能解决你的问题,请参考以下文章

数据结构篇--排序算法

数据结构与算法学习之排序算法

算法--相邻两数最大差值

线性差值算法

分类变量差值处理的方法

算法—— 相邻两数的最大差值