算法总结——【技巧,ACM模式输入】

十七 技巧

题目1------只出现一次的数字【36】

给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。

你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。

示例 1 :

**输入:**nums = [2,2,1]

**输出:**1

示例 2 :

**输入:**nums = [4,1,2,1,2]

**输出:**4

示例 3 :

**输入:**nums = [1]

**输出:**1

提示:

  • 1 <= nums.length <= 3 * 104
  • -3 * 104 <= nums[i] <= 3 * 104
  • 除了某个元素只出现一次以外,其余每个元素均出现两次。

异或运算,a^a=0,a^0=a

java 复制代码
public int singleNumber(int[] nums) {
    int ans = 0;
    for (int num : nums) {
        ans ^= num;
    }
    return ans;
}

题目2------多数元素【67】

给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

复制代码
输入:nums = [3,2,3]
输出:3

示例 2:

复制代码
输入:nums = [2,2,1,1,1,2,2]
输出:2

提示:

  • n == nums.length
  • 1 <= n <= 5 * 104
  • -109 <= nums[i] <= 109
  • 输入保证数组中一定有一个多数元素。

**进阶:**尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

思路:哈希,排序都不符合要求,摩尔投票法

摩尔投票:

java 复制代码
public int majorityElement(int[] nums) {
    int c = 0;
    int vote = 0;
    for (int num : nums) {
        if (vote == 0) {
            c = num;
        }
        if (c == num) {
            vote++;
        } else {
            vote--;
        }
    }
    return c;
}

题目3------颜色分类

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 012 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

复制代码
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

示例 2:

复制代码
输入:nums = [2,0,1]
输出:[0,1,2]

提示:

  • n == nums.length
  • 1 <= n <= 300
  • nums[i]012

思路:

排序算法最少都是O(nlogn),技术排序

一次遍历区统计2的个数并且计算数组总和,这样可以计算出1和0的个数

然后再遍历一次,

java 复制代码
 public void sortColors(int[] nums) {
     int count2 = 0;
     int sum = 0;
     for (int num : nums) {
         if (num==2) count2++;
         sum += num;
     }
     int count1 = sum - count2*2;
     int count0 = nums.length - count1 - count2;
     for (int i = 0; i < nums.length; i++) {
         if (count0 != 0) {
             nums[i] = 0;
             count0--;
         } else if (count1 != 0) {
             nums[i] = 1;
             count1--;
         } else {
             nums[i] = 2;
             count2--;
         }
     }
 }

题目4------下一个排列【133】

高频背

整数数组的一个 排列 就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3][1,3,2][3,1,2][2,3,1]

整数数组的 下一个排列 是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的 下一个排列 就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2]
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2]
  • arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须**原地**修改,只允许使用额外常数空间。

示例 1:

复制代码
输入:nums = [1,2,3]
输出:[1,3,2]

示例 2:

复制代码
输入:nums = [3,2,1]
输出:[1,2,3]

示例 3:

复制代码
输入:nums = [1,1,5]
输出:[1,5,1]

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 100

思路:背背背

算法推导

如何得到这样的排列顺序?这是本文的重点。我们可以这样来分析:

  1. 我们希望下一个数 比当前数大,这样才满足 "下一个排列" 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。

  2. 我们还希望下一个数 增加的幅度尽可能的小,这样才满足"下一个排列与当前排列紧邻"的要求。为了满足这个要求,我们需要:

  3. 在 尽可能靠右的低位 进行交换,需要 从后向前 查找

  4. 将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换

  5. 将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。显然 123546 比 123564 更小,123546 就是 123465 的下一个排列

以上就是求 "下一个排列" 的分析过程。

算法过程

标准的 "下一个排列" 算法可以描述为:

  1. 从后向前 查找第一个 相邻升序 的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序

  2. 在 [j,end) 从后向前 查找第一个满足 A[i] < A[k] 的 k。A[i]、A[k] 分别就是上文所说的「小数」、「大数」

  3. 将 A[i] 与 A[k] 交换

  4. 可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序

  5. 如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4

java 复制代码
public void nextPermutation(int[] nums) {
    if (nums.length < 2) {
        return;
    }
    // 1.从后往前找第一个升序峰
    int i = nums.length-2, j = nums.length-1;
    // 一定要严格大于
    while (i >= 0 && nums[i] >= nums[j]) {
        i--;
        j--;
    }
    // 2.找第一个nums[i]<nums[k]
    int k = nums.length - 1;
    if (i>=0){
        while (k>=0 && nums[i ]>= nums[k]) k--;
        swap(nums, i, k);
    }


    reverse(nums, j, nums.length - 1);
}

public void swap(int[] nums, int i, int j) {
    int temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

public void reverse(int[] nums, int left, int right) {
    while (left < right) {
        swap(nums, left, right);
        left++;
        right--;
    }
}


public static void main(String[] args) {
    Main main = new Main();
    int[] nums = new int[]{1,2,3};
    main.nextPermutation(nums);
    for (int num : nums) {
        System.out.println(num);
    }
}

题目5------寻找重复数【35】

【鸽笼定理】可能考察口述

给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1n),可知至少存在一个重复的整数。

假设 nums 只有 一个重复的整数 ,返回 这个重复的数

你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。

示例 1:

复制代码
输入:nums = [1,3,4,2,2]
输出:2

示例 2:

复制代码
输入:nums = [3,1,3,4,2]
输出:3

示例 3 :

复制代码
输入:nums = [3,3,3,3,3]
输出:3

提示:

  • 1 <= n <= 105
  • nums.length == n + 1
  • 1 <= nums[i] <= n
  • nums只有一个整数 出现 两次或多次 ,其余整数均只出现 一次

进阶:

  • 如何证明 nums 中至少存在一个重复的数字?
  • 你可以设计一个线性级时间复杂度 O(n) 的解决方案吗?

鸽笼定理,可以使用

快慢指针、将数组和索引的转化成链表就变成了,判断链表的环

java 复制代码
    public int findDuplicate(int[] nums) {
        int slow = 0, fast = 0;
        do {
            slow = nums[slow];
            fast = nums[nums[fast]];
        } while (slow != fast);
        slow = 0;
        while (slow != fast) {
            slow = nums[slow];
            fast = nums[fast];
        }
        return slow;
    }

十八 ACM数据读入

1 从控制台读取输入

两数之和为例,它的输入是这样的:

csharp 复制代码
[2,7,11,15]
9

我们需要读入第一行的字符串和第二行的整数,然后将字符串转换成数组。首先,使用 Scanner 进行逐行读取。

Java 复制代码
import java.util.Scanner;
   
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String line = s.nextLine();         //读取字符串
        System.out.println("字符串:"line);
        int a = s.nextInt();                //读取整数
        System.out.println("整数:"+a);
    }
}

2 手动构建树结构

从控制台读取字符串之后,需要手动将这些字符串转换成数组、链表和二叉树,转化的代码都是很固定的,可以作为模板使用。

数组

数组的构建非常简单,将字符串根据分隔符分割,然后转换成整型即可。代码如下:

Java 复制代码
    private static int[] StringToIntArray(String str) {
        String[] parts = str                //根据","分割成字符串数组
            .substring(1, str.length() - 2)
            .split(",");
        int[] nums = new int[parts.length];
        for (int i = 0; i < parts.length; i ++) 
            nums[i] = Integer.parseInt(parts[i]);    //转为整型数组
        
        return nums;
    }
链表

链表的构建也不难。

Java 复制代码
    private static ListNode StringToListNode(String str) {
        String[] parts = str    //分割字符串
            .substring(1, str.length() - 2)
            .split(",");

        ListNode dummy = new ListNode(-1), cur = dummy;        //虚拟头结点

        for (int s: parts) {
            cur.next = new ListNode(Integer.parseInt(s));      //逐个添加链表的结点
            cur = cur.next;
        }

        return dummy.next;    //返回真正的头结点
    }
二叉树

构建二叉树的代码就很长了,个人感觉手动构建二叉树相当于一道中等题了。

Java 复制代码
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;

public class Main {

    private static class TreeNode {    //二叉树类
        int val;
        TreeNode left, right;
        TreeNode() {}
        TreeNode(int val) { this.val = val; }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String str = scanner.nextLine();

        TreeNode root = StringToTreeNode(str);
    }

    private static TreeNode StringToTreeNode(String str) {
        String[] parts = str
            .substring(1, str.length() - 1)
            .split(",");

        String item = parts[0];
        TreeNode root = new TreeNode(Integer.parseInt(item));    //二叉树的根节点

        Queue<TreeNode> q = new LinkedList<>();    //使用队列添加结点
        q.offer(root);

        int index = 1;
        while (!q.isEmpty()) {
            TreeNode node = q.poll();

            if (index == parts.length) break;

            item = parts[index ++];
            if (!item.equals("null")) {
                node.left = new TreeNode(Integer.parseInt(item));
                q.offer(node.left);
            }

            if (index == parts.length) break;

            item = parts[index ++];
            if (!item.equals("null")) {
                node.right = new TreeNode(Integer.parseInt(item));
                q.offer(node.right);
            }
        }

        return root;
    }
}
相关推荐
智者知已应修善业2 小时前
【无序数组指针交换2则】2024-10-28
c语言·数据结构·c++·经验分享·笔记·算法
一叶落4382 小时前
LeetCode 136. 只出现一次的数字(C语言详解 | 哈希表 + 排序 + 位运算)
c语言·数据结构·算法·leetcode·哈希算法·散列表
古译汉书2 小时前
【数据结构算法】二分查找
c语言·开发语言·数据结构·c++·算法
逆境不可逃2 小时前
【从零入门23种设计模式19】行为型之观察者模式
java·开发语言·算法·观察者模式·leetcode·设计模式·动态规划
小龙报2 小时前
【算法通关指南:算法基础篇】二分答案专题:1.木材加工 2.砍树
c语言·数据结构·c++·算法·启发式算法
cici158742 小时前
经典的基于策略迭代和值迭代法的动态规划MATLAB实现
算法·matlab·动态规划
月明长歌2 小时前
【码道初阶-Hot100】 LeetCode 49. 字母异位词分组:从排序哈希到分组映射,彻底讲透为什么排序后可以作为同一组的标识
算法·leetcode·哈希算法
Q一件事2 小时前
结构方程相关
python·算法·机器学习
yugi9878382 小时前
兰伯特问题求解的MATLAB实现
开发语言·算法·matlab