1. 做算法题的⽅法

  • 充分阅读题⽬,了解题⽬背后的关键意思

  • 分析题⽬,涉及到哪些数据结构,对问题进⾏分类。到底属于链表问题、栈思想问题、字符串问题、⼆叉树问题、图相关问题、排序问题,与你之前所接触过的算法题有没有类似,找到问题的解题思路

  • 实现算法:在算法的实现的过程,并不是⼀蹴⽽就,肯定是需要不断的调试、修改

  • 验证算法正确性

  • 找到题源,看其他的开发者提供的解决思路

  • 找到题解建议之后,对于其他优秀思路,分析它的优势,学习它的思路,并且写成其他解法的代码(借⼒, 不要单纯的闭⻔造⻋)

  • 算法题的解题能⼒来⾃于两点:对于数据结构与算法核⼼问题是否夯实 + 是否有⾜够多且⾜够耐⼼的积累

例如:如果你连基本的链表操作都不知道,如何去解决链表翻转问题?如果你连图的存储、遍历等都不清楚,如何去解决图的最⼩路径问题?如果你都不了解⼆叉树,如果去计算⼆叉树的深度问题?

2. 算法练习

2.1 括号匹配检验

假设表达式中允许包含两种括号:圆括号与⽅括号,其嵌套顺序随意,即([]())或者[([][])]都是正确的,⽽这[(]或者(()])或者([())都是不正确的格式。检验括号是否匹配的⽅法,可⽤“期待的急迫程度”这个概念来描述。例如:考虑以下括号的判断[([][])]

代码实现:

  1. let str = "(()])";
  2. let dic = ["[" : "]", "(" : ")"];
  3. let stack = LinkedStack<String>();
  4. for c in str {
  5. let s = "\(c)";
  6. if(stack.isEmpty()){
  7. stack.push(element: s);
  8. continue;
  9. }
  10. let key = stack.getTop()!;
  11. let val = dic[key];
  12. if(val == nil || val != s){
  13. stack.push(element: s);
  14. continue;
  15. }
  16. stack.pop();
  17. }
  18. print(stack.isEmpty() ? "匹配" : "不匹配");
  • 在字典中定义括号的匹配关系,然后利用栈的先进先出原则进行匹配

  • 遍历字符串,如果当前栈空,压栈

  • 获取栈顶元素作为字典的key,如果取不到valval与当前字符不等,压栈

  • 如果val和当前字符相等,出栈

  • 遍历后如果栈为空,表示匹配成功,否则匹配失败

2.2 每⽇⽓温

根据每⽇⽓温列表,请重新⽣成⼀个列表,对应位置的输⼊是你需要再等待多久温度才会升⾼超过该⽇的天数。如果之后都不会升⾼,请在该位置0来代替

示例:

  1. //给定气温列表
  2. temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
  3. //输出结果
  4. [1, 1, 4, 2, 1, 1, 0, 0]

提示:⽓温列表⻓度的范围是[1, 30000]。每个⽓温的值的均为华⽒度,都是在[30, 100]范围内的整数

2.2.1 暴力破解法

  1. let temperatures = [73, 74, 75, 71, 69, 72, 76, 73];
  2. var arrResult : [Int] = Array();
  3. for i in 0 ..< temperatures.count {
  4. let cur = temperatures[i];
  5. var jump = 0;
  6. if(i > 0 && cur == temperatures[i - 1]){
  7. let preResult = arrResult[i - 1];
  8. jump = preResult == 0 ? 0 : preResult - 1;
  9. arrResult.append(jump);
  10. continue;
  11. }
  12. for j in (i + 1) ..< temperatures.count {
  13. let next = temperatures[j];
  14. if(cur < next){
  15. jump = j - i;
  16. break;
  17. }
  18. }
  19. arrResult.append(jump);
  20. }
  21. print("\(arrResult)");

2.2.3 跳跃对比法

  1. let temperatures = [73, 74, 75, 71, 69, 72, 76, 73];
  2. var arrResult : [Int] = Array.init(repeating: 0, count: temperatures.count);
  3. for i in (0 ..< temperatures.count - 1).reversed() {
  4. let cur = temperatures[i];
  5. var jump = 1;
  6. var next = temperatures[i + jump];
  7. if(cur < next){
  8. arrResult[i] = jump;
  9. continue;
  10. }
  11. var nextResult = arrResult[i + jump];
  12. if(nextResult == 0){
  13. continue;
  14. }
  15. while(nextResult > 0 && cur >= next){
  16. jump += nextResult;
  17. next = temperatures[i + jump];
  18. nextResult = arrResult[i + jump];
  19. }
  20. arrResult[i] = jump;
  21. }
  22. print("\(arrResult)");

2.2.4 栈思想解决

  1. let temperatures = [73, 74, 75, 71, 69, 72, 76, 73];
  2. var arrResult : [Int] = Array.init(repeating: 0, count: temperatures.count);
  3. var stack = LinkedStack<[Int]>();
  4. for i in 0 ..< temperatures.count {
  5. let cur = temperatures[i];
  6. while(!stack.isEmpty()){
  7. let top = stack.getTop()!;
  8. if(top[1] > cur){
  9. break;
  10. }
  11. let index = top[0];
  12. arrResult[index] = i - index;
  13. stack.pop();
  14. }
  15. stack.push(element: [i, cur]);
  16. }
  17. stack.clear();
  18. print("\(arrResult)");

2.3 爬楼梯问题

假设你正在爬楼梯。需要n阶你才能到达楼顶。每次你可以爬12个台阶。你有多少种不同的⽅法可以爬到楼顶呢?注意:给定n是⼀个正整数å

示例1:

  1. 输⼊:2
  2. 输出:2
  3. 解释:有2种⽅法可以爬到楼顶
  4. 1. 1 + 1
  5. 2. 2

示例2:

  1. 输⼊:3
  2. 输出:3
  3. 解释:有3种⽅法可以爬到楼顶
  4. 1. 1 + 1 + 1
  5. 2. 1 + 2
  6. 3. 2 + 1

2.3.1 递归法

  1. func algorithm(n : Int) -> Int {
  2. if(n <= 2){
  3. return n;
  4. }
  5. return algorithm(n: n - 1) + algorithm(n: n - 2);
  6. }
  • 公式可拆解为:f(n) = f(n - 1) + f(n - 2)

  • f(1) = 1

  • f(2) = 2

2.3.2 动态规划法

动态规划(Dynamic programming,简称DP)是⼀种在数学、管理科学、计算机科学、经济学和⽣物信息学中使⽤的,通过把原问题分解为相对简单的⼦问题的⽅式求解复杂问题的⽅法

动态规划常常适⽤于有重叠⼦问题和最优⼦结构性质的问题,动态规划⽅法所耗时间往往远少于朴素解法

动态规划背后的基本思想⾮常简单。⼤致上,若要解⼀个给定问题,我们需要解其不同部分(即⼦问题),再根据⼦问题的解以得出原问题的解。动态规划往往⽤于优化递归问题,例如斐波那契数列,如果运⽤递归的⽅式来求解会重复计算很多相同的⼦问题,利⽤动态规划的思想可以减少计算量

通常许多⼦问题⾮常相似,为此动态规划法试图仅仅解决每个⼦问题⼀次,具有天然剪枝的功能,从⽽减少计算量

⼀旦某个给定⼦问题的解已经算出,则将其记忆化存储,以便下次需要同⼀个⼦问题解之时直接查表。这种做法在重复⼦问题的数⽬关于输⼊的规模呈指数增⻓时特别有⽤

动态规划法的三个核心要素:

  • 找到最优子结构(可以将问题拆解,找到最优的拆解方式)

  • 找到算法的边界(结束条件)

  • 状态转移方程式

案例中f(n) = f(n - 1) + f(n - 2)公式,属于从上至下的拆解方式。这个问题也可以被分解为⼀些包含最优⼦结构的⼦问题,即它的最优解可以从其⼦问题的最优解来有效地构建,从而使⽤动态规划来解决这⼀问题
image.png

假设爬 n个台阶有f(n)个可能:

  • 假设先爬1阶, 剩下n-1阶有f(n-1)种可能

  • 假设先爬2阶,剩下n-2阶有f(n-2)种可能

因此爬n阶可以转化成两种,爬n-1问题和n-2问题
image.png

代码实现:

  1. func algorithm(n : Int) -> Int {
  2. var n1 = 1;
  3. var n2 = 2;
  4. if(n == 1){
  5. return n1;
  6. }
  7. for _ in 3 ... n {
  8. let tmp = n2;
  9. n2 += n1;
  10. n1 = tmp;
  11. }
  12. return n2;
  13. }

动态规划法中的n1n2为已知结果,通过已知结果计算n阶结果,时间复杂度为O(n)。而递归法,相当于在下楼过程中层层拆解,时间复杂度为O(n ^ 2)

2.4 进制转换

将给定数字转为二进制

示例:

  1. 输⼊:17
  2. 输出:10001

代码实现:

  1. var n = 17;
  2. var stack = LinkedStack<Int>();
  3. while(n > 0){
  4. stack.push(element: n % 2);
  5. n = n / 2;
  6. }
  7. var result = "";
  8. while(!stack.isEmpty()){
  9. result += "\(stack.getTop()!)";
  10. stack.pop();
  11. }
  12. print(result);

2.5 杨辉三角

image.png

代码实现:

  1. var n = 5;
  2. var arr = [[Int]](repeating: [Int](repeating: 0, count: n), count: n);
  3. for i in 0 ..< n {
  4. arr[i][0] = 1;
  5. if(i == 0){
  6. continue;
  7. }
  8. arr[i][i] = 1;
  9. for j in 1 ..< i {
  10. arr[i][j] = arr[i - 1][j - 1] + arr[i - 1][j];
  11. }
  12. }

2.6 字符串编码

给定⼀个经过编码的字符串,返回它解码后的字符串

编码规则为k[encoded_string],表示其中⽅括号内部的encoded_string正好重复k次。 注意:k保证为正整数

你可以认为输⼊字符串总是有效的,输⼊字符串中没有额外的空格, 且输⼊的⽅括号总是符合格式要求的。此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数k,不会出现像3a2[4]的输⼊

示例:

  1. s = "3[a]2[bc]"
  2. 返回 "aaabcbc"
  3. s = "3[a2[c]]"
  4. 返回 "accaccacc"
  5. s = "2[abc]3[cd]ef"
  6. 返回 "abcabccdcdcdef"
  7. s = "a3[b]2[c1[d1[e]]f]10[g]x2[y]z"
  8. 返回 "abbbcdefcdefggggggggggxyyz"

代码实现:

  1. let str = "a3[b]2[c1[d1[e]]f]10[g]x2[y]z";
  2. var stack = LinkedStack<String>();
  3. var result = "";
  4. for c in str {
  5. if(c == "["){
  6. stack.push(element: result);
  7. result = "";
  8. continue;
  9. }
  10. if(c != "]"){
  11. result += "\(c)";
  12. continue;
  13. }
  14. let letter = result;
  15. result = "";
  16. let top = stack.pop()!;
  17. var number = "";
  18. for t in top.reversed() {
  19. if(t >= "0" && t <= "9"){
  20. number = "\(t)" + number;
  21. continue;
  22. }
  23. break;
  24. }
  25. if(top.count > number.count){
  26. result += top.prefix(top.count - number.count);
  27. }
  28. for _ in 0 ..< Int.init(number)! {
  29. result += letter;
  30. }
  31. }
  32. print(result);

2.7 去除重复字⺟

给你⼀个仅包含⼩写字⺟的字符串,请你去除字符串中重复的字⺟,使得每个字⺟只出现⼀次。需保证返回结果的字典序最⼩,要求不能打乱其他字符的相对位置

示例:

输⼊:"bcabc"
输出:"abc"

输⼊:"cbacdcbc"
输出:"acdb"

代码实现:

let str = "bcacdcbc";

var dic = [String : Int]();

for c in str {

    if(dic["\(c)"] == nil){
        dic["\(c)"] = 1;
        continue;
    }

    let count = dic["\(c)"]!;
    dic["\(c)"] = count + 1;
}

var result = "";

for i in 0 ..< str.count {

    let c = str[i];

    let count = dic[c]!;

    if(count == 1){
        dic.removeValue(forKey: c);
    }
    else{
        dic[c] = count - 1;
    }

    if(result.isEmpty){
        result += c;
        continue;
    }

    var isExist = false;

    for j in (0 ..< result.count).reversed() {
        if(result[j] == c){
            isExist = true;
            break;
        }
    }

    if(isExist){
        continue;
    }

    var j = result.count - 1;

    while(!result.isEmpty && result[j] > c && dic[result[j]] != nil){
        result = String(result.prefix(j));
        j = result.count - 1;
    }

    result += c;
}

print(result);

实现字符串扩展,支持使用下标截取字符串

extension String {

    subscript (i:Int)->String{
        let startIndex = self.index(self.startIndex, offsetBy: i)
        let endIndex = self.index(startIndex, offsetBy: 1)
        return String(self[startIndex..<endIndex])
    }

    subscript (r: Range<Int>) -> String {
        get {
            let startIndex = self.index(self.startIndex, offsetBy: r.lowerBound)
            let endIndex = self.index(self.startIndex, offsetBy: r.upperBound)
            return String(self[startIndex..<endIndex])
        }
    }

    subscript (index:Int , length:Int) -> String {
        get {
            let startIndex = self.index(self.startIndex, offsetBy: index)
            let endIndex = self.index(startIndex, offsetBy: length)
            return String(self[startIndex..<endIndex])
        }
    }

    func substring(to:Int) -> String{
        return self[0..<to]
    }

    func substring(from:Int) -> String{
        return self[from..<self.count]
    }
}

2.8 字符串匹配

给你⼀个仅包含⼩写字⺟的字符串主串S = "abcacabdc",模式串T = "abd",请查找出模式串在主串第⼀次出现的位置

提示:主串和模式串均为⼩写字⺟且都是合法输⼊

示例:

S = "abcacabdc"
T = "abc"
返回 1

S = "abcacabdc"
T = "abd"
返回 6

2.8.1 BF算法

BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法

BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和T的第二个字符。若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果。BF算法是一种蛮力算法

代码实现:

let str = "abcacabdc";
let t = "abd";

var result = 0;
var i = 0;
var j = 0;

while(i < str.count && j < t.count){

    if(str[i] == t[j]){
        i += 1;
        j += 1;
        continue;
    }

    i = i - j + 1;
    j = 0;
}

if(j == t.count){
    result = i - j + 1;
}

print("\(result)");

2.8.2 RK算法

HASH:如果两个字符串HASH后的值不相同,则它们肯定不相同。如果它们HASH后的值相同,它们不一定相同

RK算法的基本思想就是:将模式串THASH值跟主串S中的每一个长度为T的子串的HASH值比较。如果不同,则它们肯定不相等。如果相同,则将原文逐位比较

不考虑溢出情况的简单实现:

let str = "abcacabdc";
let t = "abd";

let basics = Int(Character.init("a").asciiValue!);
let radix = 26;
let max = Int(pow(Double(radix), Double(t.count - 1)));

var hash = 0;
var result = 0;

var tmp = 0;

for i in 0 ..< t.count {

    let o = Double(t.count - i - 1);

    var c = Int(Character(str[i]).asciiValue!) - basics;
    tmp += c * Int(pow(Double(radix), o));

    c = Int(Character(t[i]).asciiValue!) - basics;
    hash += c * Int(pow(Double(radix), o));
}

for i in 0 ... str.count - t.count {

    if(i > 0){

        let x = Int(Character(str[i - 1]).asciiValue!) - basics;
        let c = Int(Character(str[i + t.count - 1]).asciiValue!) - basics;

        tmp = (tmp - x * max) * radix + c;
    }

    if(tmp == hash){
        result = i + 1;
        break;
    }
}

print("\(result)");
  • 将主串按照模式串的长度进行拆分

  • 使用以下公式得到HASH

    • 例如:主串为abcd,模式串为bcd

      • 将当前字母的ASCII码减去字母aASCII码,得到0 ~ 25的值

      • 将数字分别乘以26n次方,n拆分后的字符串长度 - 当前字母所在位置 - 1

      • 计算abcHASH值:hash = 0 * (26 ^ 2) + 1 * (26 ^ 1) + 2 * (26 ^ 0)

      • 结果不匹配,进行bcdHASH值:hash = (hash - 0 * (26 ^ 2)) * 26 + 3

这种方式的好处是,以模式串的长度为基础拆分出的主串计算的HASH值不会出现冲突,缺陷是字符串过长计算出的HASH值会溢出

如果考虑溢出问题,我们需要缩短HASH计算后的数字大小,但会增加出现HASH冲突的情况

解决溢出情况的代码实现:

let str = "abcacabdc";
let t = "abd";

let basics = Int(Character.init("a").asciiValue!);
var hash = 0;
var result = 0;

var tmp = 0;

for i in 0 ..< t.count {

    var c = Int(Character(str[i]).asciiValue!) - basics;
    tmp += c;

    c = Int(Character(t[i]).asciiValue!) - basics;
    hash += c;
}

for i in 0 ... str.count - t.count {

    if(i > 0){

        let x = Int(Character(str[i - 1]).asciiValue!) - basics;
        let c = Int(Character(str[i + t.count - 1]).asciiValue!) - basics;

        tmp = (tmp - x) + c;
    }

    if(tmp != hash){
        continue;
    }

    var isSucceed = true;

    for j in 0 ..< t.count {

        if(str[i + j] != t[j]){
            isSucceed = false;
            break;
        }
    }

    if(isSucceed){
        result = i + 1;
        break;
    }
}

print("\(result)");
  • 将主串按照模式串的长度进行拆分

  • 将当前字母的ASCII码减去字母aASCII码,得到0 ~ 25的值

  • 将拆分后的字符串,直接按照0 ~ 25的值进行累加得到HASH

  • 当拆分后的主串和模式串的HASH值相等,还要匹配两个字符串中的字符是否一致,避免HASH冲突的情况