阿俊带你用Kotlin刷算法(一)

1,713 阅读9分钟

本系列通过JavaKotlin这两种语言来解决力扣上面的算法题,由于本人算法菜鸟一枚,可能部分题目并不是最优题解,希望能和各位大神共同讨论~

阿俊带你用Kotlin刷算法(一)

阿俊带你用Kotlin刷算法(二)

项目的GitHub:Algorithm

两数之和(Two Sum)

难度:简单

链接:Two Sum

代码

Java

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by TanJiaJun on 2021/6/4.
 * 1. 两数之和(Two Sum)
 * 难度:简单
 *
 * @see <a href="https://leetcode-cn.com/problems/two-sum/">Two Sum</a>
 */
class TwoSum {

    public static void main(String[] args) {
        // 示例一
        System.out.println("示例一");

        int[] firstNumbers = {2, 7, 11, 15};
        int firstTarget = 9;
        System.out.println(Arrays.toString(hashTwoSum(firstNumbers, firstTarget)));

        System.out.print("\n");

        // 示例二
        System.out.println("示例二");

        int[] secondNumbers = {3, 2, 4};
        int secondTarget = 6;
        System.out.println(Arrays.toString(hashTwoSum(secondNumbers, secondTarget)));

        System.out.print("\n");

        // 示例三
        System.out.println("示例三");
        int[] thirdNumbers = {3, 3};
        int thirdTarget = 6;
        System.out.println(Arrays.toString(hashTwoSum(thirdNumbers, thirdTarget)));
    }

    /**
     * 方法一:枚举
     * 时间复杂度:O(N^2),其中N是数组的元素数量
     * 空间复杂度:O(1)
     *
     * @param numbers int类型的数组
     * @param target  目标值
     * @return 结果
     */
    private static int[] twoSum(int[] numbers, int target) {
        for (int i = 0, length = numbers.length; i < length; i++) {
            for (int j = i + 1; j < length; j++) {
                if (numbers[i] + numbers[j] == target) {
                    return new int[]{i, j};
                }
            }
        }
        return new int[0];
    }

    /**
     * 方法二:哈希表
     * 时间复杂度:O(N),其中N是数组的元素数量
     * 空间复杂度:O(N),其中N是数组的元素数量
     *
     * @param numbers int类型的数组
     * @param target  目标值
     * @return 结果
     */
    private static int[] hashTwoSum(int[] numbers, int target) {
        Map<Integer, Integer> hashMap = new HashMap<>();
        for (int i = 0, length = numbers.length; i < length; i++) {
            int other = target - numbers[i];
            if (hashMap.containsKey(other)) {
                return new int[]{hashMap.get(other), i};
            }
            hashMap.put(numbers[i], i);
        }
        return new int[0];
    }

}

Kotlin

/**
 * Created by TanJiaJun on 2021/6/5.
 * 1. 两数之和(Two Sum)
 * 难度:简单
 *
 * @see <a href="https://leetcode-cn.com/problems/two-sum/">Two Sum</a>
 */
object TwoSumKotlin {

    @JvmStatic
    fun main(args: Array<String>) {
        // 示例一
        println("示例一")

        val firstNumbers = intArrayOf(2, 7, 11, 15)
        val firstTarget = 9
        println(hashTwoSum(firstNumbers, firstTarget).contentToString())

        print("\n")

        // 示例二
        println("示例二")

        val secondNumbers = intArrayOf(3, 2, 4)
        val secondTarget = 6
        println(hashTwoSum(secondNumbers, secondTarget).contentToString())

        print("\n")

        // 示例三
        println("示例三")
        val thirdNumbers = intArrayOf(3, 3)
        val thirdTarget = 6
        println(hashTwoSum(thirdNumbers, thirdTarget).contentToString())
    }

    /**
     * 方法一:枚举算法
     * 时间复杂度:O(N^2),其中N是数组的元素数量
     * 空间复杂度:O(1)
     *
     * @param numbers int类型的数组
     * @param target  目标值
     * @return 结果
     */
    private fun twoSum(numbers: IntArray, target: Int): IntArray {
        numbers.forEachIndexed { index, number ->
            for (i in index + 1 until numbers.size) {
                if (number + numbers[i] == target) {
                    return intArrayOf(index, i)
                }
            }
        }
        return intArrayOf()
    }

    /**
     * 方法二:哈希表
     * 时间复杂度:O(N),其中N是数组的元素数量
     * 空间复杂度:O(N),其中N是数组的元素数量
     *
     * @param numbers int类型的数组
     * @param target  目标值
     * @return 结果
     */
    private fun hashTwoSum(numbers: IntArray, target: Int): IntArray {
        hashMapOf<Int, Int>().apply {
            numbers.forEachIndexed { index, number ->
                val other = target - number
                if (containsKey(other)) {
                    val otherIndex = get(other) ?: 0
                    return intArrayOf(otherIndex, index)
                }
                put(number, index)
            }
        }
        return intArrayOf()
    }

}

题解

枚举

// TwoSumKotlin.kt
/**
 * 方法一:枚举
 * 时间复杂度:O(N^2),其中N是数组的元素数量
 * 空间复杂度:O(1)
 *
 * @param numbers int类型的数组
 * @param target  目标值
 * @return 结果
 */
private fun twoSum(numbers: IntArray, target: Int): IntArray {
    numbers.forEachIndexed { index, number ->
        for (i in index + 1 until numbers.size) {
            if (number + numbers[i] == target) {
                return intArrayOf(index, i)
            }
        }
    }
    return intArrayOf()
}

时间复杂度:O(N^2),其中N是数组的元素数量。

空间复杂度:O(1)。

暴力解法,遍历numbers数组中每一个元素,并且找到任意两个相加等于target的元素,要注意的是,假设当前元素的索引是ii前面的元素其实已经和i匹配过,我们就不需要再次匹配了。

哈希表

// TwoSumKotlin.kt
/**
 * 方法二:哈希表
 * 时间复杂度:O(N),其中N是数组的元素数量
 * 空间复杂度:O(N),其中N是数组的元素数量
 *
 * @param numbers int类型的数组
 * @param target  目标值
 * @return 结果
 */
private fun hashTwoSum(numbers: IntArray, target: Int): IntArray {
    hashMapOf<Int, Int>().apply {
        numbers.forEachIndexed { index, number ->
            val other = target - number
            if (containsKey(other)) {
                val otherIndex = get(other) ?: 0
                return intArrayOf(otherIndex, index)
            }
            put(number, index)
        }
    }
    return intArrayOf()
}

时间复杂度:O(N),其中N是数组的元素数量。

时间复杂度:O(N),其中N是数组的元素数量。

假设当前元素是number,方法一在查找另外一个元素的时候,也就是查找target - number的消耗太多时间了,我们可以使用HashMap存储,以元素作为key,以索引作为value,这样就可以通过containsKey方法快速地查找到这个元素对应的索引,至于为什么不用containsValue方法而使用containsKey方法,这是因为HashMap使用链地址法来解决哈希冲突的,简单来说,就是数组和链表的结合,每个数组元素都是一个链表结构,首先调用keyhashCode方法得到哈希值(该方法适用于每个Java对象),然后再通过哈希算法后两步运算(高位运算、取模运算)来定位该键值对对应的存储位置,所以通过key去查找元素的效率会比通过value的效率高,Oracle官方说,用这两个方法遍历HashMap中的所有元素,containsValue方法所需的时间比containsKey方法多出几个数量级,所以我们使用containsKey方法来再提升效率。

要注意的是,我们不要使用mutableMapOf方法,而是使用hashMapOf方法,因为mutableMapOf方法使用的是LinkedHashMap,而hashMapOf方法使用的是HashMapLinkedHashMap通过维护一个额外的双向链表来保证迭代顺序,但是在该题目其实是不需要的,不需要为此增加时间空间上的开销,源码如下所示:

// Maps.kt
/**
 * Returns an empty new [MutableMap].
 *
 * The returned map preserves the entry iteration order.
 * @sample samples.collections.Maps.Instantiation.emptyMutableMap
 */
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <K, V> mutableMapOf(): MutableMap<K, V> = LinkedHashMap()

两数相加(Add Two Numbers)

难度:中等

链接:Add Two Numbers

代码

Java

/**
 * Created by TanJiaJun on 2021/6/5.
 * 2. 两数相加(Add Two Numbers)
 * 难度:中等
 *
 * @see <a href="https://leetcode-cn.com/problems/add-two-numbers/">Add Two Numbers</a>
 */
class AddTwoNumbers {

    public static void main(String[] args) {
        // 示例一
        System.out.print("示例一:");

        Node firstNode =
                new Node(2,
                        new Node(4,
                                new Node(3)));
        Node secondNode =
                new Node(5,
                        new Node(6,
                                new Node(4)));
        printNode(addTwoNumbers(firstNode, secondNode));

        System.out.print("\n");

        // 示例二
        System.out.print("示例二:");

        Node thirdNode = new Node(0);
        Node fourthNode = new Node(0);
        printNode(addTwoNumbers(thirdNode, fourthNode));

        System.out.print("\n");

        // 示例三
        System.out.print("示例三:");

        Node fifthNode =
                new Node(9,
                        new Node(9,
                                new Node(9,
                                        new Node(9,
                                                new Node(9,
                                                        new Node(9,
                                                                new Node(9)))))));
        Node sixthNode =
                new Node(9,
                        new Node(9,
                                new Node(9,
                                        new Node(9))));
        printNode(addTwoNumbers(fifthNode, sixthNode));

        System.out.print("\n");

        // 示例四
        System.out.print("示例四:");

        Node seventhNode = new Node(2);
        Node eightNode = new Node(8);
        printNode(addTwoNumbers(seventhNode, eightNode));
    }

    /**
     * 时间复杂度:O(Max(m, n)),其中m是第一个结点的长度,n是第二个结点的长度
     * 空间复杂度:O(1)
     *
     * @param firstNode  第一个结点
     * @param secondNode 第二个结点
     * @return 结果
     */
    private static Node addTwoNumbers(Node firstNode, Node secondNode) {
        Node dummyNode = new Node(-1);
        Node node = dummyNode;
        int carry = 0;
        // 遍历两个链表
        while (firstNode != null || secondNode != null) {
            // 如果两个链表的长度不相同的话,就在短的链表的后面通过添加若干个0
            int firstValue = firstNode != null ? firstNode.item : 0;
            int secondValue = secondNode != null ? secondNode.item : 0;
            int value = firstValue + secondValue + carry;
            int newItem = value % 10;
            // 相加后的进位值通过carry来存储
            carry = value / 10;
            Node newNode = new Node(newItem);
            if (firstNode != null) {
                firstNode = firstNode.next;
            }
            if (secondNode != null) {
                secondNode = secondNode.next;
            }
            // 如果在遍历完这两个链表后,carry的值大于0,那么就应该在链表的后面增加一个新的结点来存储这个值
            if (firstNode == null && secondNode == null && carry > 0) {
                newNode.next = new Node(carry);
            }
            node.next = newNode;
            node = node.next;
        }
        return dummyNode.next;
    }

    /**
     * 打印结点
     *
     * @param node 结点
     */
    private static void printNode(Node node) {
        System.out.print("[");
        while (node != null) {
            System.out.print(node.item);
            node = node.next;
            if (node != null) {
                System.out.print(",");
            }
        }
        System.out.print("]");
    }

    private static class Node {

        int item;
        Node next;

        Node(int item) {
            this.item = item;
        }

        Node(int item, Node next) {
            this.item = item;
            this.next = next;
        }

    }

}

Kotlin

/**
 * Created by TanJiaJun on 2021/6/6.
 * 2. 两数相加(Add Two Numbers)
 * 难度:中等
 *
 * @see <a href="https://leetcode-cn.com/problems/add-two-numbers/">Add Two Numbers</a>
 */
object AddTwoNumbersKotlin {

    @JvmStatic
    fun main(args: Array<String>) {
        // 示例一
        print("示例一:")
        val firstNode =
                Node(
                        2,
                        Node(
                                4,
                                Node(3)
                        )
                )
        val secondNode =
                Node(
                        5,
                        Node(
                                6,
                                Node(4)
                        )
                )
        printNode(addTwoNumbers(firstNode, secondNode))

        print("\n")

        // 示例二
        print("示例二:")
        val thirdNode = Node(0)
        val fourthNode = Node(0)
        printNode(addTwoNumbers(thirdNode, fourthNode))

        print("\n")

        // 示例三
        print("示例三:")
        val fifthNode =
                Node(
                        9,
                        Node(
                                9,
                                Node(
                                        9,
                                        Node(
                                                9,
                                                Node(
                                                        9,
                                                        Node(
                                                                9,
                                                                Node(9)
                                                        )
                                                )
                                        )
                                )
                        )
                )
        val sixthNode =
                Node(
                        9,
                        Node(
                                9,
                                Node(
                                        9,
                                        Node(9)
                                )
                        )
                )
        printNode(addTwoNumbers(fifthNode, sixthNode))

        print("\n")

        // 示例四
        print("示例四:")
        val seventhNode = Node(2)
        val eightNode = Node(8)
        printNode(addTwoNumbers(seventhNode, eightNode))
    }

    /**
     * 时间复杂度:O(Max(m, n)),其中m是第一个结点的长度,n是第二个结点的长度
     * 空间复杂度:O(1)
     *
     * @param first 第一个结点
     * @param second 第二个结点
     * @return 结果
     */
    private fun addTwoNumbers(first: Node?, second: Node?): Node {
        var firstNode = first
        var secondNode = second
        val dummy = Node(-1)
        var node: Node = dummy
        var carry = 0
        // 遍历两个链表
        while (firstNode != null || secondNode != null) {
            // 如果两个链表的长度不相同的话,就在短的链表的后面通过添加若干个0
            val firstValue = firstNode?.item ?: 0
            val secondValue = secondNode?.item ?: 0
            val value = firstValue + secondValue + carry
            val newItem = value.rem(10)
            // 相加后的进位值通过carry来存储
            carry = value.div(10)
            val newNode = Node(newItem)
            firstNode?.let { firstNode = it.next }
            secondNode?.let { secondNode = it.next }
            // 如果在遍历完这两个链表后,carry的值大于0,那么就应该在链表的后面增加一个新的结点来存储这个值
            if (firstNode == null && secondNode == null && carry > 0) {
                newNode.next = Node(carry)
            }
            node.next = newNode
            node = node.next ?: dummy
        }
        return dummy.next ?: dummy
    }

    /**
     * 打印结点
     *
     * @param node 结点
     */
    private fun printNode(node: Node?) {
        print("[")
        var curNode: Node? = node
        while (curNode != null) {
            print(curNode.item)
            curNode = curNode.next
            if (curNode != null) {
                print(",")
            }
        }
        print("]")
    }

    private data class Node(
            var item: Int,
            var next: Node? = null
    )

}

时间复杂度:O(Max(m, n)),其中m是第一个结点的长度,n是第二个结点的长度。

空间复杂度:O(1)。

题解

这道题要注意的地方是,我们发现要输入的链表都是逆序存储相关的数字,然后两个链表在同一个位置的时候可以直接相加相加后的进位值我们通过carry来存储,如果两个链表的长度不相同的话,就在的链表的后面通过添加若干个0,使得长度和的链表相等,要注意的是,如果在遍历完这两个链表后,carry的值大于0,那么就应该在链表的后面增加一个新的结点来存储这个值。

无重复字符的最长子串(Longest Substring Without Repeating Characters)

难度:中等

链接:Longest Substring Without Repeating Characters

代码

Java

import java.util.HashSet;
import java.util.Set;

/**
 * Created by TanJiaJun on 2021/6/6.
 * 3. 无重复字符的最长子串(Longest Substring Without Repeating Characters)
 * 难度:中等
 *
 * @see <a href="https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/">Longest Substring Without Repeating Characters</a>
 */
class LongestSubstringWithoutRepeatingCharacters {

    public static void main(String[] args) {
        // 示例一
        System.out.print("示例一:");

        String firstStr = "abcabcbb";
        System.out.println(lengthOfLongestSubstring(firstStr));

        System.out.print("\n");

        // 示例二
        System.out.print("示例二:");

        String secondStr = "bbbbb";
        System.out.println(lengthOfLongestSubstring(secondStr));

        System.out.print("\n");

        // 示例三
        System.out.print("示例三:");

        String thirdStr = "pwwkew";
        System.out.println(lengthOfLongestSubstring(thirdStr));

        System.out.print("\n");
    }

    /**
     * 方法:滑动窗口
     * 时间复杂度:O(N),其中N是字符串的长度
     * 空间复杂度:O(∣Σ∣),其中Σ表示字符串中出现的字符集合,∣Σ∣表示该字符集合的大小,默认为所有ASCII码为[0,128)内的字符,也就是大小是128
     *
     * @param str 字符串
     * @return 结果
     */
    private static int lengthOfLongestSubstring(String str) {
        // 结果
        int result = 0;
        // 右指针
        int right = 0;
        // 哈希集合,记录出现的字符
        Set<Character> chars = new HashSet<>();
        // 遍历字符串中的字符
        for (int i = 0, length = str.length(); i < length; i++) {
            if (i > 0) {
                // 移除一个字符,左指针向右移动一格
                chars.remove(str.charAt(i - 1));
            }
            while (right < length && !chars.contains(str.charAt(right))) {
                // 如果索引小于字符串的长度,同时该字符没出现过,就将该字符放到哈希集合中,然后右指针向右移动一格
                chars.add(str.charAt(right));
                right++;
            }
            // 计算长度的最大值
            result = Math.max(result, right - i);
        }
        return result;
    }

}

Kotlin

import kotlin.math.max

/**
 * Created by TanJiaJun on 2021/6/6.
 * 3. 无重复字符的最长子串(Longest Substring Without Repeating Characters)
 * 难度:中等
 *
 * @see <a href="https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/">Longest Substring Without Repeating Characters</a>
 */
object LongestSubstringWithoutRepeatingCharactersKotlin {

    @JvmStatic
    fun main(args: Array<String>) {
        // 示例一
        print("示例一:")

        val firstStr = "abcabcbb"
        println(lengthOfLongestSubstring(firstStr))

        print("\n")

        // 示例二
        print("示例二:")

        val secondStr = "bbbbb"
        println(lengthOfLongestSubstring(secondStr))

        print("\n")

        // 示例三
        print("示例三:")

        val thirdStr = "pwwkew"
        println(lengthOfLongestSubstring(thirdStr))
    }

    /**
     * 方法:滑动窗口
     * 时间复杂度:O(N),其中N是字符串的长度
     * 空间复杂度:O(∣Σ∣),其中Σ表示字符串中出现的字符集合,∣Σ∣表示该字符集合的大小,默认为所有ASCII码为[0,128)内的字符,也就是大小是128
     *
     * @param str 字符串
     * @return 结果
     */
    private fun lengthOfLongestSubstring(str: String): Int {
        // 结果
        var result = 0
        // 右指针
        var right = 0
        // 哈希集合,记录出现的字符
        val chars = hashSetOf<Char>()
        // 遍历字符串中的字符
        str.forEachIndexed { index, _ ->
            // 当索引大于0的时候,执行以下逻辑
            if (index > 0) {
                // 移除一个字符,左指针向右移动一格
                chars.remove(str[index - 1])
            }
            while (right < str.length && !chars.contains(str[right])) {
                // 如果索引小于字符串的长度,同时该字符没出现过,就将该字符放到哈希集合中,然后右指针向右移动一格
                chars.add(str[right])
                right++
            }
            // 计算长度的最大值
            result = max(result, right - index)
        }
        return result
    }
}

时间复杂度:O(N),其中N是字符串的长度。

空间复杂度:O(∣Σ∣),其中Σ表示字符串中出现的字符集合,∣Σ∣表示该字符集合的大小,默认为所有ASCII码为[0,128)内的字符,也就是大小是128。

题解

我们可以使用滑动窗口完成这道题,滑动窗口是一种通过调整左右两个指针来计算指针之间相关的逻辑的方法,我们通过示例一来作为例子,当字符串为abcabcbb的时候,我们先把左指针右指针都放在字符a,这个时候左指针右指针对应的字符都是相同的,然后我们把右指针移动一格,这个时候右指针放在字符b的位置,此时像刚才那样判断下两个指针对应的字符是否相同,如果相同的话,左指针就往移动一格,右指针保持不变,如果不相同左指针保持不变,右指针移动一格,如此类推,就可以得到对应的区间,从而得到没有重复字符的最长子串

要注意的是,和两数之和那道题一样,我们这里使用Set集合来记录出现的字符,但是我们并不需要保证迭代顺序,所以这里使用hashSetOf方法来创建HashSet就可以了,而不需要使用mutableSetOf方法来创建,因为mutableSetOf方法创建的一个LinkedHashSet,它也是通过额外的双向链表来保证迭代顺序,源码如下所示:

// Sets.kt
/**
 * Returns an empty new [MutableSet].
 *
 * The returned set preserves the element iteration order.
 * @sample samples.collections.Collections.Sets.emptyMutableSet
 */
@SinceKotlin("1.1")
@kotlin.internal.InlineOnly
public inline fun <T> mutableSetOf(): MutableSet<T> = LinkedHashSet()

我的GitHub:TanJiaJunBeyond

Android通用框架:Android通用框架

我的掘金:谭嘉俊

我的简书:谭嘉俊

我的CSDN:谭嘉俊