LeetCode 初级算法之数组(下),看看你都学会了吗?

709 阅读8分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

前言:最近自己也开始系统的刷面试题了,算法是过不去的坎,希望自己能够坚持下去✊,同行的朋友们共勉。

上篇请看👉《LeetCode 初级算法之数组(上),看看你都学会了吗?》

题一:两个数组的交集 II

给你两个整数数组 nums1 和 nums2 ,请你以数组形式返回两数组的交集。返回结果中每个元素出现的次数,应与元素在两个数组中都出现的次数一致(如果出现次数不一致,则考虑取较小值)。可以不考虑输出结果的顺序。

示例一:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]

解题思路:集合法、排序+双指针

第一种思路:在不考虑顺序的情况下,使用字典是比较容易想到,可以通过缓存一个数组的元素,以及个数。key为元素值,value为元素个数。然后在第二个数组中查找是否命中该元素,如果命中,则取出,并将次数-1,当取完后,删除该条缓存。最后被取出的元素即两数组的并集。

第二种思路:先排序,然后判断双指针所指向的大小,如果相等,添加到数组中,如果不相等,哪边值小,哪边移动指针,即index++

解法一: 集合法

遍历num1数组,添加到集合中,key为num, value为出现的次数;
遍历num2数组,去集合中查找是否命中,命中将集合中value-1,当value为0时,则从集合中删除;

代码

func intersect(_ nums1: [Int], _ nums2: [Int]) -> [Int] {
    var tempNums = [Int: Int]() // 缓存的集合
    var newNums = [Int]() // 返回的数组
    for num in nums1 {
        if tempNums[num] != nil {
            tempNums[num]! += 1
        } else {
            tempNums[num] = 1
        }
    }

    for num in nums2 {
        if let value = tempNums[num] {
            newNums.append(num)
            if value > 1 {
                tempNums[num]! -= 1
            } else {
                tempNums.removeValue(forKey: num)
            }
        }
    }
    return newNums
}

解法二: 排序法

先排序,排序后的数组元素更利于后面指针操作;
在使用双指针i,j,指向对应的两个数组,默认i=0,j=0,比较值的大小,如果相等ns1[i]==ns2[j],说明是两数组的合集,则添加到数组中,如果不相等ns1[i]!=ns2[j],值小的一方下标+1,;

代码

func intersect2(_ nums1: [Int], _ nums2: [Int]) -> [Int] {
    // 先排序
    var nums1 = nums1
    var nums2 = nums2
    nums1.sort()
    nums2.sort()

    // 返回的数组
    var newNums = [Int]()
    let num1Count = nums1.count
    let num2Count = nums2.count
    var index1 = 0, index2 = 0

    while index1 < num1Count && index2 < num2Count {
        let num1 = nums1[index1]
        let num2 = nums2[index2]
        if num1 == num2 {
            newNums.append(num1)
            index1 += 1; index2 += 1
        } else if num2 < num1 {
            index2 += 1
        } else if num2 > num1 {
            index1 += 1
        }
    }
    return newNums
}

题二: 加一

给定一个由 整数 组成的 非空 数组所表示的非负整数,在该数的基础上加一。

最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。

你可以假设除了整数 0 之外,这个整数不会以零开头。

示例一:

输入:digits = [1,2,3]
输出:[1,2,4]

解题思路:逢十进一(数学计算)

首先可以判断个位数是不是9,如果是9需要考虑进位,如果其他的数则直接+1即可; 在这里,我是通过做加法判断是否需要进位;

代码

func plusOne(_ digits: [Int]) -> [Int] {
    var newDigits = [Int]()
    var plusDigit = 1
    for i in 0...digits.count-1 {
        let index = digits.count-1 - i
        var num = digits[digits.count-1 - i]
        if num + plusDigit == 10 {
            num = 0
            plusDigit = 1
        } else {
            num += plusDigit
            plusDigit = 0
        }
        newDigits.insert(num, at: 0)
        if index == 0 && plusDigit == 1 {
            newDigits.insert(plusDigit, at: 0)
        }
    }
    return newDigits
}

题三:移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例一:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例二:

输入: nums = [0]
输出: [0]

解题思路: 双指针

双指针i,j,每次递增,先移动i指针; 核心思想就是每次循环保证j的左边都不为0。
如果i指针下的值num[i] == 0j则保持不变,
如果i指针下的值num[i] != 0,并满足i > j条件,则交换i,j值的,将0往右侧移动;

代码

func moveZeroes(_ nums: inout [Int]) {
    var j = 0
    for i in 0..<nums.count {
        if i > j {
            nums[j] = nums[i]
            nums[i] = 0
        }
        j+=1
    }
}

题四:两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例一:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1]

示例二:

输入:nums = [3,2,4], target = 6
输出:[1,2]

进阶:你可以想出一个时间复杂度小于 O(n^2^) 的算法吗?

解题思路: 双循环、 哈希表(字典缓存)

第一种思路:常规的双层遍历,i=0,i++,j=i+1,j++; 依次交替相加,判断是否与目标值相等。

时间复杂度:O(n^2^)
空间复杂度:O(1)

第二种思路:使用哈希表缓存当前值和下标,每次遍历时,取出当前位置下的值和目标值的差,在缓存中是否能命中,如果能命中,说明找到了;

时间复杂度:O(n)
空间复杂度:O(n)

解法一:双循环

双层遍历,i=0,i++,j=i+1,j++; 判断nums[i] + nums[j] == target则说明找到这两个数了;

代码

func twoSum(_ nums: [Int], _ target: Int) -> [Int] {
    var newNums = [Int]()
    for i in 0..<nums.count {
        for j in i+1..<nums.count {
            if nums[i] + nums[j] == target {
                newNums.append(i)
                newNums.append(j)
                break
            }
        }
    }
    return newNums
}

解法二:哈希表

在每次遍历,先去字典中查找是否存在另一个值target - nums[i],如果存在,则返回这两个数的index,如果不存在,则缓存当前值,使用字典缓存key:nums[i], value:index

代码

func twoSum2(_ nums: [Int], _ target: Int) -> [Int] {
    var newNums = [Int]()
    var dict = [Int: Int]()
    for index in 0..<nums.count {
        let num = nums[index]
        let nextNum = target - num
        if let nextIndex = dict[nextNum] {
            newNums.append(nextIndex)
            newNums.append(index)
        }
        dict[num] = index
    }
    return newNums
}

题五:有效的数独

请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。

数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫格内只能出现一次。(请参考示例图)

250px-sudoku-by-l2g-20050714svg.png

输入:board =
[["5","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出:true

解题思路:二维数组

解题思路也比较明确,我们要确定三个条件,行、列、块(九格宫)都不允许有相同数字出现。

通过上述条件,初始化三个二维数组,第一层数组index表示的是具体在哪一行、列、块,第二层数组index表示数字1-9的排列位置。
每次遍历,判断行、列、块index对应下的数字1-9index下的值是否>0,if rows[row][num] > 0 || columns[column][num] > 0 || blocks[index][num] > 0,若满足此条件,则不符合规则,跳出循环;若不满足条件,则填充该区域。

以上面输入为例
rows :

Row\Num123456789
row1001010100
row2100011001
...

代码

func isValidSudoku(_ board: [[Character]]) -> Bool {
    var subList = Array<Int>(repeating: 0, count: 9)
    var list = Array<[Int]>(repeating: subList, count: 9)
    var rows = list
    var columns = list
    var blocks = list
    var index = 0
    for row in 0...board.count-1 {
        for column in 0...board[row].count-1 {
            index = column/3 + row/3 * 3
            if board[row][column] == "." { continue }
            let num = (board[row][row].hexDigitValue ?? 1) - 1
            if rows[row][num] > 0 || columns[column][num] > 0 || blocks[index][num] > 0 {
                return false
            }
            rows[row][num] = 1
            columns[column][num] = 1
            blocks[index][num] = 1
        }
    }
    return true
}

题六:旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例一:

输入:matrix = [[1,2,3],
               [4,5,6],
               [7,8,9]]

输出:[[7,4,1],
      [8,5,2],
      [9,6,3]]

解题思路:直接交换、两次翻转

第一种思路:直接交换,通过数学推导,我们可以得出一个规律:即matrix[row][col] => matrix[col][n-row-1]; 所以在每次的循环中所做的事情都是围绕这个规律来交换值;

第二种思路:顺时针旋转90,还可以翻转两次数组即可达到一样的效果。
首先上下翻转一次,然后再根据对角线翻转一次;

Notice:数组旋转的题可以通过翻转来做:

一维数组旋转:三次翻转:整体翻转,0...s-1翻转, s...n-1翻转,
向右:s = k%n, 向左: s = n - k%n,(n:长度 k:偏移量)

二位数组90度旋转;两次翻转:上下翻转,对角线翻转;顺时针旋转,对角线是左上到右下,逆时针旋转,对角线是右上到

解法一:直接交换

根据结论:matrix[row][col] => matrix[col][n-row-1]
如果是顺时针:matrix[row][col] = matrix[n-col-1][row]
如果是逆时针:matrix[row][col] = matrix[col][n-row-1]

这种替换值的方式就是通过一圈一圈的替换; 外圈替换完成后,内圈接着替换,由外层向内层逐一替换; 双层循环控制整个替换过程,外层循环的次数为length/2,内层循环次数为length-i-1

代码

func rotate(_ matrix: inout [[Int]]) {
    let length = matrix.count
    for i in 0..<length/2 {
        for j in i..<length-i-1 {
            let temp = matrix[i][j]
            let m = length - j - 1
            let n = length - i - 1
            /// 顺时针
            matrix[i][j] = matrix[m][i]
            matrix[m][i] = matrix[n][m]
            matrix[n][m] = matrix[j][n]
            matrix[j][n] = temp

            /**
             逆时针
             matrix[i][j] = matrix[j][n]
             matrix[j][n] = matrix[n][m]
             matrix[n][m] = matrix[m][i]
             matrix[m][i] = temp
             */
        }
    }
}

解法二:两次翻转

第一次:上下翻转
matrix[i][j] < = > matrix[n-i-1][j]
第二次:对角线翻转
matrix[i][j] < = > matrix[j][i]
(顺时针: ↘   逆时针:↙)

代码

func rotate(_ matrix: inout [[Int]]) {
    upReverse(&matrix)
    diagonalReverse(&matrix)
}
/// 上下翻转
func upReverse(_ matrix: inout [[Int]]) {
    let n = matrix.count
    for i in 0..<n/2 {
        for j in 0..<n {
            let temp = matrix[i][j]
            matrix[i][j] = matrix[n-i-1][j]
            matrix[n-i-1][j] = temp
        }
    }
}
/// 对角线翻转
func diagonalReverse(_ matrix: inout [[Int]]) {
    let n = matrix.count
    for i in 0..<n {
        for j in 0..<i {
            let temp = matrix[i][j]
            matrix[i][j] = matrix[j][i]
            matrix[j][i] = temp
        }
    }
}

小结

初级算法中的数组篇已经告一段落,主要是围绕着双指针的操作以哈希表的运用。比较意外的是可以用位运算去判断重复元素,这也算是为自己在以后的一些算法题中提供一种不同的思路,很多时候,自己也不会往位运算上去想。

总的来说,大部分的算法题都是让我们考虑如何合理的运用数据结构来达到更快的计算。如何利用数组的下标取值,如何利用哈希表的键值,如何利用快排的思想等等。

数组:双指针、哈希表、集合、位运算、快排、多次翻转