每天两道算法题-1(比较版本号、版本号排序以及sort方法详述、第一个唯一字符)

991 阅读5分钟

现在开始每天至少两道算法题,希望能一直不断更!

比较版本号

165. 比较版本号 - 力扣(LeetCode)

题目描述: 给两个版本号 version1 和 version2,请比较它们。

版本号由一个或多个修订号组成,各修订号由一个 '.' 连接。每个修订号由 多位数字 组成,可能包含 前导零。每个版本号至少包含一个字符。修订号从左到右编号,下标从0开始,最左边的修订号下标为0,下一个修订号下标为1,以此类推。例如,2.5.33和 0.1都是有效的版本号。

比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。

返回规则如下:

  • 如果 version1 > version2 返回 1
  • 如果 version1 < version2 返回 -1
  • 除此之外返回0

示例 1:

输入:version1 = "1.01", version2 = "1.001"
输出:0
解释:忽略前导零,"01""001" 都表示相同的整数 "1"

示例 2:

输入:version1 = "1.0", version2 = "1.0.0"
输出:0
解释:version1 没有指定下标为 2 的修订号,即视为 "0"

方法一:字符串分隔

我们可以将版本号按照点号分割成修订号,然后从左到右比较两个版本号的相同下标的修订号。在比较修订号时,需要将字符串转换成整数进行比较。注意根据题目要求,如果版本号不存在某个下标处的修订号,则该修订号视为 0。

    var compareVersion = function(version1, version2) {
        const v1 =version1.split('.')
        const v2 = version2.split('.')
        while(v1.length || v2.length) {
            const dig1 = parseInt(v1.shift()) || 0   //通过parseInt将其转换为数字并且能够忽略前导0
            const dig2 = parseInt(v2.shift()) || 0
            if(dig1 > dig2) {
                return 1
            } else if(dig2 > dig1) {
                return -1
            }
        }
        return 0
    };
  • 时间复杂度:O(n + m)(或 O(max(n,m)),这是等价的),其中 n是字符串 version1的长度,m是字符串 version2的长度
  • 空间复杂度: O(n + m),需要 O(n + m)的空间存储分割后的修订号列表。

方法二:双指针

方法一需要存储分割后的修订号,为了优化空间复杂度,可以在分隔版本号的同时解析出修订号进行比较。

            var compareVersion = function(version1, version2) {
                const n = version1.length, m = version2.length
                let i = 0, j = 0
                while( i < n || j < m) {
                    let x = 0
                    for(; i < n && version1[i] != '.'; ++i) {                   
                        x = x * 10 + (+version1[i])
                    }
                    ++i     //跳过点号
                    let y = 0
                    for(; j < m && version2.charAt(j) != '.'; j++) {
                        y = y * 10 + (+version2[j])
                    }
                    ++j   //跳过点号
                    
                    if( x != y) {
                        return x > y ? 1 : -1
                    }
                }
                return 0
            }
  • 时间复杂度:O(n + m)(或 O(max(n,m)),这是等价的),其中 n是字符串 version1的长度,m是字符串 version2的长度
  • 空间复杂度 : O(1),只需要常数的空间保存若干变量

版本号排序

题目: 输入一组版本号,输出从大到小的排序

示例:

输入: ['2.1.0.1', '0.402.1', '10.2.1', '5.1.2', '1.0.4.5']
输出: ['10.2.1', '5.1.2', '2.1.0.1', '1.0.4.5', '0.402.1']

上面写过比较版本号的,当第一个大于第二个时返回1,第二个大于第一个时返回-1,通过这个可以借助数组的sort方法进行排序。

    var versionSort = function (arr) {
        return arr.sort((a,b) => {
            const n = a.length, m = b.length
            // 采用上题的双指针方法
            let i = 0, j = 0
            while(i < n || j < m) {
                let x = 0
                for(; i < n && a[i] !== '.'; i++) {
                    x = x * 10 + (+a[i])
                }
                i++
                let y = 0
                for(; j < m && b[j] !== '.'; j++) {
                    y = y * 10 + (+b[j])
                }
                j++
                if(x !== y) {
                    return y - x  //因为采用的是降序排列
                }
            }
            return 0
        })
    };

sort方法详述

MDN developer.mozilla.org/zh-CN/docs/…

sort() 方法 就地 对数组的元素进行排序,并返回对相同数组的引用。默认排序是将元素转换为字符串,然后按照它们的 UTF-16 码元值升序排序。

由于它取决于具体实现,因此无法保证排序的时间和空间复杂度。

如果想要不改变原数组的排序方法,可以使用 [toSorted()]。

语法:

sort()
sort(compareFn)

参数:

  • compareFn 可选:定义排序顺序的函数,返回值应该是一个数字,其正负性表示两个元素的相对顺序,该函数使用以下参数调用:

    • a 第一个用于比较的元素,不会是undefined
    • b 第二个用于比较的元素,不会是undefined

如果省略该函数,数组元素会被转换为字符串,然后根据每个字符的Unicode码位置进行排序。

返回值: 经过排序的原始数组的引用。注意数组是就地排序的,不会进行复制。

描述:

如果没有提供 compareFn,所有非 undefined 的数组元素都会被转换为字符串,并按照 UTF-16 码元顺序比较字符串进行排序。例如“banana”会被排列到“cherry”之前。在数值排序中,9 出现在 80 之前,但因为数字会被转换为字符串,在 Unicode 顺序中“80”排在“9”之前。所有的 undefined 元素都会被排序到数组的末尾。

sort() 方法保留空槽。如果源数组是[稀疏的],则空槽会被移动到数组的末尾,并始终排在所有 undefined 元素的后面。

如果提供了 compareFn,所有非 undefined 的数组元素都会按照比较函数的返回值进行排序(所有的 undefined 元素都会被排序到数组的末尾,并且不调用 compareFn)。

compareFn(a, b) 返回值排序顺序
> 0ab 后,如 [b, a]
< 0ab 前,如 [a, b]
=== 0保持 ab 原来的顺序

所以,比较函数形式如下:

function compareFn(a, b) {
  if (根据排序标准,a 小于 b) {
    return -1;
  }
  if (根据排序标准,a 大于 b) {
    return 1;
  }
  // a 一定等于 b
  return 0;
}

更正式地说,为了确保正确的排序行为,比较函数应具有以下属性:

  • 纯函数:比较函数不会改变被比较的对象或任何外部状态。(这很重要,因为无法保证比较函数将在何时以及如何调用,因此任何特定的调用都不应对外部产生可见的效果。)
  • 稳定性:比较函数对于相同的输入对应始终返回相同的结果。
  • 自反性compareFn(a, a) === 0
  • 反对称性compareFn(a, b)compareFn(b, a) 必须都是 0 或者具有相反的符号。
  • 传递性:如果 compareFn(a, b)compareFn(b, c) 都是正数、零或负数,则 compareFn(a, c) 的符号与前面两个相同。

符合上述限制的比较函数将始终能够返回 10-1 中的任意一个,或者始终返回 0。例如,如果比较函数只返回 10,或者只返回 0-1,它将无法可靠地排序,因为反对称性被破坏了。一个总是返回 0 的比较函数将不会改变数组,但仍然是可靠的。

默认的字典比较函数符合上述所有限制。

要比较数字而非字符串,比较函数可以简单的用 ab,如下的函数将会将数组升序排列(如果它不包含 InfinityNaN):

function compareNumbers(a, b) {
  return a - b;
}

sort() 方法是[通用的],它只期望 this 值具有 length 属性和整数键属性。虽然字符串也类似于数组,但此方法不适用于字符串,因为字符串是不可变的。

对象数组的排序

const items = [
  { name: "Edward", value: 21 },
  { name: "Sharpe", value: 37 },
  { name: "And", value: 45 },
  { name: "The", value: -12 },
  { name: "Magnetic", value: 13 },
  { name: "Zeros", value: 37 },
];

// 根据 value 排序
items.sort((a, b) => a.value - b.value);

// 根据 name 排序
items.sort((a, b) => {
  const nameA = a.name.toUpperCase(); // 忽略大小写
  const nameB = b.name.toUpperCase(); // 忽略大小写
  if (nameA < nameB) {
    return -1;
  }
  if (nameA > nameB) {
    return 1;
  }

  // name 必须相等
  return 0;
});

第一个唯一字符

387. 字符串中的第一个唯一字符 - 力扣(LeetCode)

题目:给定一个字符串s,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。

示例 1:

输入: s = "loveleetcode"
输出: 2

示例 2:

输入: s = "aabb"
输出: -1

方法一:使用哈希表存储频数

对字符串进行两次遍历,第一次遍历时,使用哈希映射出字符串中每个字符出现的次数。在第二次遍历时,只要遍历到了一个只出现一次的字符,就返回它的索引,否则在遍历结束后返回-1。

function findOneStr(str) {
    if( !str) return -1
    //使用 map存储字符串出现的次数
    let map = new Map()
    for(let i = 0; i < str.length; i++) {
        if(map.has(str[i])) {
            map.set(str[i], map.get(str[i]) + 1)
        } else {
            map.set(str[i], 1)
        }
    }
​
    // 再遍历一遍str
    for(let i=0; i< str.length; i++) {
        if(map.get(str[i]) ===1) {
            return i
        }
    }
    return -1
}
  • 时间复杂度:O(n)其中 n是字符串 的长度,需要进行两次遍历
  • 空间复杂度: O(|E|),其中 E是字符集,在本题中 s只包含小写字母,因此 |E|<= 26。需要 O(|E|)的空间存储哈希映射。

方法二:使用哈希表存储索引

对方法一进行修改,使第二次遍历的对象从字符串变为哈希映射。

哈希映射中的每一个键值对,键表示字符,值表示它首次出现的索引(如果该字符只出现一次)或者 -1(该字符出现多次)。第一次遍历字符串时,如果字符不在哈希映射中,就将c 与它的索引作为一个键值对加入哈希映射中,否则将c 在哈希映射中对应的值修改为 -1.

第一次遍历结束后,再遍历一次哈希映射中的所有值,找出其中不为 -1的键值返回即可

注意Map实例会维护键值对的插入顺序,可以根据插入顺序进行迭代操作,同时keys()、values()、entries()会返回以插入顺序生成的相应迭代器

var firstUniqChar = function(s) {
        let map = new Map()
        for (let i = 0; i < s.length; i++) {
            if (map.has(s[i])) {
                map.set(s[i], -1)
                
            } else {
                map.set(s[i], i)
            }
        }
        for(let pos of map.values()) {
            if(pos !== -1) {
                return pos
            }
        }
        return -1
};
  • 时间复杂度:O(n)其中 n是字符串 的长度。第一次遍历字符串的时间复杂度为 O(n),第二次遍历映射的时间复杂度为 O(|E|),由于s 包含的字符种类数一定小于 s的长度,因此 O(|E|)在渐进意义下小于 O(n),可以忽略。
  • 空间复杂度: O(|E|),其中 E是字符集,在本题中 s只包含小写字母,因此 |E|<= 26。需要 O(|E|)的空间存储哈希映射。

今天超额完成了!!!开心!!加油,坚持!!!