关于字符串:一些常见的字符串匹配算法

作者:京东批发 李文涛 一、简介1.1 Background字符串匹配在文本处理的宽泛畛域中是一个十分重要的主题。字符串匹配包含在文本中找到一个,或者更一般地说,所有字符串(通常来讲称其为模式)的呈现。该模式示意为p=p[0..m-1];它的长度等于m。文本示意为t=t[0..n-1],它的长度等于n。两个字符串都建设在一个无限的字符集上。 一个比拟常见的字符串匹配办法工作原理如下。在一个大小通常等于m的窗口帮忙下扫描文本。首先将窗口和文本的左端对齐,而后将窗口的字符与文本中的字符进行比拟,这一特定的工作被称为尝试,在齐全匹配或不匹配之后,将窗口移到右侧。持续反复同样的过程,直到窗口的右端超过文本的右端,个别称为滑动窗口机制。 1.2 Brute forceBF算法查看文本中0到n-m之间的所有地位,是否有模式从那里开始呈现。而后,在每次尝试之后,它将模式串向右挪动一个地位。 BF算法不须要预处理阶段,除了模式和文本之外,还须要一个恒定的额定空间。在搜寻阶段,文本字符比拟能够以任何程序进行。该搜寻阶段的工夫复杂度为O(mn)。 public static int strMatch(String s, String p){ int i = 0, j = 0; while(i < s.length() && j < p.length()){ if(s.charAt(i) == p.charAt(j)){ i++; j++; }else{ i = i - j + 1; j = 0; } if (j == p.length()){ return i - j; } } return -1;}二、KMP先回顾下brute force中匹配的状况。咱们在文本串BBC#ABCDAB$ABCDABCDABDE中查找模式串ABCDABD,文本串中第1个字符“B”与模式串中第1个字符“A”不匹配,所以咱们将模式传后移一位。 文本串中的第2个字符“B”和模式串中的第一个字符“A”不匹配,持续后移。 基于这种形式一直比拟并且挪动,咱们发现文本串中的第5个字符“A”和模式串中的第1个字符“A”是匹配的,那么持续比拟文本串和模式串的下一个字符。 一直比拟之后咱们发现,文本串中的字符“$”和模式串中的最初一个字符“D”不匹配。 ...

April 25, 2023 · 3 min · jiezi

关于字符串:KMP算法

在利用中常常会遇到字符串比拟的算法,判断一个字符串pp是否是另外一个字符串ss的子串。注明的算法是KMP算法,当初整顿如下,参考 宫水三叶 的代码实现。 // 作者 宫水三叶// 链接 https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/solution/shua-chuan-lc-shuang-bai-po-su-jie-fa-km-tb86/// KMP 算法// ss: 原串(string) pp: 匹配串(pattern)// 工夫复杂度O(m + n)public int kmp(String ss, String pp) { if (pp.isEmpty()){ return 0; } // 别离读取原串和匹配串的长度 int n = ss.length(), m = pp.length(); // 原串和匹配串后面都加空格,使其下标从 1 开始 ss = " " + ss; pp = " " + pp; char[] s = ss.toCharArray(); char[] p = pp.toCharArray(); // 构建 next 数组,数组长度为匹配串的长度(next 数组是和匹配串相干的) int[] next = new int[m + 1]; // 结构过程 i = 2,j = 0 开始,i 小于等于匹配串长度 【结构 i 从 2 开始】 for (int i = 2, j = 0; i <= m; i++) { // 匹配不胜利的话,j = next(j) while (j > 0 && p[i] != p[j + 1]){ j = next[j]; } // 匹配胜利的话,先让 j++ if (p[i] == p[j + 1]){ j++; } // 更新 next[i],完结本次循环,i++ next[i] = j; } // 匹配过程,i = 1,j = 0 开始,i 小于等于原串长度 【匹配 i 从 1 开始】 for (int i = 1, j = 0; i <= n; i++) { // 匹配不胜利 j = next(j) while (j > 0 && s[i] != p[j + 1]){ j = next[j]; } // 匹配胜利的话,先让 j++,完结本次循环后 i++ if (s[i] == p[j + 1]){ j++; } // 整一段匹配胜利,间接返回下标 if (j == m){ return i - m; } } return -1;}如果是java的开发者,能够应用jdk自带的 ...

September 28, 2022 · 1 min · jiezi

关于字符串:算法字符串

字符串双指针用两个指针来定位一个子数组,其中一个指针指向数组的第1个数字,另一个指针指向数组的最初一个数字,那么两个指针之间所蕴含的就是一个子数组。 能够在挪动这两个指针的同时,统计两个指针之间的字符串中字符呈现的次数,这样能够解决很多常见的面试题,如在一个字符串中定位另一个字符串的变位词等。 因为这种类型的面试题都与统计字母呈现的次数无关,咱们常常应用哈希表来存储每个元素呈现的次数,因而解决这种类型的面试题通常须要同时应用双指针和哈希表。 字符串中的变位词题目:输出字符串s1和s2,如何判断字符串s2中是否蕴含字符串s1的某个变位词?如果字符串s2中蕴含字符串s1的某个变位词,则字符串s1至多有一个变位词是字符串s2的子字符串。假如两个字符串中只蕴含英文小写字母。例如,字符串s1为"ac",字符串s2为"dgcaf",因为字符串s2中蕴含字符串s1的变位词"ca",因而输入为true。如果字符串s1为"ab",字符串s2为"dgcaf",则输入为false。 由变位词的定义可知,变位词具备以下几个特点。首先,一组变位词的长度肯定雷同;其次,组成变位词的字母汇合肯定雷同,并且每个字母呈现的次数也雷同。 /** * @param {string} s1 * @param {string} s2 * @return {boolean} */var checkInclusion = function(s1, s2) { if(s2.length < s1.length) { return false; } var areAllZero = function(counts) { for(let i of counts) { if(i !== 0) { return false } } return true; } const counts = new Array(26).fill(0) for(let i = 0; i < s1.length; i++) { counts[s1.charCodeAt(i) - 97]++; counts[s2.charCodeAt(i) - 97]--; } if(areAllZero(counts)) { return true; } for (let i = s1.length; i < s2.length; ++i) { counts[s2.charCodeAt(i) - 97]-- counts[s2.charCodeAt(i - s1.length) - 97]++ if(areAllZero(counts)) { return true } } return false;};字符串中的所有变位词题目:输出字符串s1和s2,如何找出字符串s2的所有变位词在字符串s1中的起始下标?假如两个字符串中只蕴含英文小写字母。例如,字符串s1为"cbadabacg",字符串s2为"abc",字符串s2的两个变位词"cba"和"bac"是字符串s1中的子字符串,输入它们在字符串s1中的起始下标0和5。/** * @param {string} s * @param {string} p * @return {number[]} */var findAnagrams = function(s1, s2) { if(s2.length > s1.length) { return []; } let result = []; const counts = new Array(26).fill(0); var areAllZero = function(counts) { for(let i of counts) { if(i !== 0) { return false } } return true; } for(let i = 0; i < s2.length; i++){ counts[s2.charCodeAt(i)-97]++ counts[s1.charCodeAt(i)-97]-- } if(areAllZero(counts)) { result.push(0) } for(let i = s2.length; i < s1.length; i++) { counts[s1.charCodeAt(i)-97]-- counts[s1.charCodeAt(i-s2.length)-97]++ if(areAllZero(counts)) { result.push(i-s2.length+1) } } return result;};不含反复字符的最长子字符串题目:输出一个字符串,求该字符串中不含反复字符的最长子字符串的长度。例如,输出字符串"babcca",其最长的不含反复字符的子字符串是"abc",长度为3。/** * @param {string} s * @return {number} */var lengthOfLongestSubstring = function(s) { if(s.length == 0) return 0 if(s.length == 1) return 1 const map = new Map(); let max = 0 // 双指针 let left = 0 let right = 0 var areAllZero = function(counts) { for(let i of counts) { if(i[1] === 2) { return false } } return true; } while(right < s.length) { if(map.get(s.charAt(right)) !== 1) { map.set(s.charAt(right), (map.get(s.charAt(right)) || 0) + 1) right++ if(map.get(s.charAt(right)) === 1) { max = Math.max(right - left, max) } } else { map.set(s.charAt(left), (map.get(s.charAt(left)) || 0) - 1) left++ } } max = Math.max(right - left, max) return max;};蕴含所有字符的最短字符串输出两个字符串s和t,请找出字符串s中蕴含字符串t的所有字符的最短子字符串。例如,输出的字符串s为"ADDBANCAD",字符串t为"ABC",则字符串s中蕴含字符'A'、'B'和'C'的最短子字符串是"BANC"。如果不存在符合条件的子字符串,则返回空字符串""。如果存在多个符合条件的子字符串,则返回任意一个。var minWindow = (s, t) => { const charToCount = new Map(); for(let ch of t) { charToCount.set(ch, (charToCount.get(ch) || 0) + 1) } let count = charToCount.size; let start = 0, end = 0, minStart = 0, minEnd = 0; let minLength = Infinity; while(end < s.length || (count == 0) && end == s.length) { if(count > 0) { let endCh = s.charAt(end); if(charToCount.get(endCh)!== undefined) { charToCount.set(endCh, charToCount.get(endCh)-1) if (charToCount.get(endCh) === 0) { count-- } } } else { if (end - start < minLength) { minLength = end - start minStart = start minEnd = end } let startCh = s.charAt(start); if(charToCount.get(startCh)!==undefined) { charToCount.set(startCh, charToCount.get(startCh) + 1) if(charToCount.get(startCh) == 1) { count++ } } start++ } } return minLength < Infinity ? s.substring(minStrat, minEnd) : "";}上述代码中只有一个while循环,用来把两个变量从0减少到字符串s的长度。如果字符串的长度是n,那么工夫复杂度就是O(n)。能够应用一个哈希表来统计每个字符呈现的次数。哈希表的键为字符,假如字符串中只有英文字母,那么哈希表的大小不会超过256,辅助空间的大小不会随着字符串长度的减少而减少,因而空间复杂度是O(1)。 ...

March 28, 2022 · 4 min · jiezi

关于字符串:golangleetcode中级二叉树的序列化与反序列化常数时间插入删除和获取随机元素

第一题 二叉树的序列化与反序列化题目 解题思路本题有两个子问题 第一个为将二叉树序列化为一个字符串,即为二叉树的遍历对于树的遍历,有深搜和广搜两种深搜又按根节点的地位分为先序遍历,中序遍历,后序遍历三种状况这里咱们规定 应用先序遍历将其转化为字符串序列 这里咱们应用包strconv实现对根本数据类型的字符串示意的转换Atoi(string to int)和 Itoa(int to string) func (Codec) serialize(root *TreeNode) string { sb := &strings.Builder{} var dfs func(*TreeNode) dfs = func(node *TreeNode) { if node == nil { sb.WriteString("null,") return } sb.WriteString(strconv.Itoa(node.Val)) sb.WriteByte(',') dfs(node.Left) dfs(node.Right) } dfs(root) return sb.String()}第二个为将序列化的字符串从新转化为树依据先序遍历的规定字符的第一个元素为根节点 这里应用函数strings.Split函数按指定的分隔符切割字符串,并返回切割后的字符串切片。不便咱们对字符串进行操作 func (Codec) deserialize(data string) *TreeNode { sp := strings.Split(data, ",") var build func() *TreeNode build = func() *TreeNode { if sp[0] == "null" { sp = sp[1:] return nil } val, _ := strconv.Atoi(sp[0]) sp = sp[1:] return &TreeNode{val, build(), build()} } return build()}具体代码为package mainimport ( "strconv" "strings")type TreeNode struct { Val int Left *TreeNode Right *TreeNode}type Codec struct{}func Constructor() (_ Codec) { return}func (Codec) serialize(root *TreeNode) string { sb := &strings.Builder{} var dfs func(*TreeNode) dfs = func(node *TreeNode) { if node == nil { sb.WriteString("null,") return } sb.WriteString(strconv.Itoa(node.Val)) sb.WriteByte(',') dfs(node.Left) dfs(node.Right) } dfs(root) return sb.String()}func (Codec) deserialize(data string) *TreeNode { sp := strings.Split(data, ",") var build func() *TreeNode build = func() *TreeNode { if sp[0] == "null" { sp = sp[1:] return nil } val, _ := strconv.Atoi(sp[0]) sp = sp[1:] return &TreeNode{val, build(), build()} } return build()}复杂度剖析工夫复杂度:O(n);咱们只拜访每个节点一次,因而工夫复杂度为 O(n),其中 n 是节点数,即树的大小。空间复杂度:O(n);在序列化和反序列化函数中,咱们递归会应用栈空间,故渐进空间复杂度为 O(n)。 ...

February 23, 2022 · 2 min · jiezi

关于字符串:Java-字符串常见的操作

在 Java 当中,为字符串类提供了丰盛的操作方法,对于字符串,咱们常见的操作就是:字符串的比拟、查找、替换、拆分、截取以及其余的一些操作。 在 Java 中,有 String,StringBuffer 和 StringBuilder 字符串类,他们的区别是 String 类是不可批改的,而 StringBuffer 和 StringBuilder 类是能够批改的。要留神的是,这里的批改不是字面意思上的批改。简略来说,比方,要实现两个字符串的拼接,对于前者来说,假如有 str1 = "hello" , 要给他拼接一个"world",那么是这样的,在这个过程中,"hello"自身没有变,它还在池中。然而对于后两者来说,假如有 str2 = "世界",要拼接''你好'',拼接之后,池中就不存在"世界"了。StringBuffer 和 StringBuilder 的区别就是一个是线程平安的,一个不是线程平安的。 上面,咱们围绕字符串的一些操作来进行阐明。 一,字符串的比拟 1,equal()办法 官网文档形容: public boolean equals(Object anObject) 将此字符串与指定对象进行比拟。 其后果是 true 当且仅当该参数不是 null 并且是 String 对象,示意雷同的字符序列作为该对象。 参数 anObject - 比照这个 String 的对象 后果 true 如果给定的对象代表一个 String 等效于这个字符串, 否则 false String 类当中的 equal()办法用来比拟两个字符串是否相等。这一种比拟是辨别大小写的。当有一个是字符串常量的时候,倡议的写法是将字符串常量放在里面,这样的理由是:如果里面的变量是 null 的时候,就会抛出空指针异样。 String str1 = new String("Hello"); String str2 = new String("Hello"); ...

February 8, 2022 · 4 min · jiezi

关于字符串:不使用strcat函数进行字符串拼接

include"stdio.h"void function(char p,char q){ int n=0;int m=0;for(;p[n]!='\0';) n++;for(;q[m]!='\0';) p[++n]=q[m++];p[n]='\0';printf("%s",p);}int main(void){ char n[100]="asdfgh";char m[100]="zxcvbnm";function(n,m);return 0;}

October 18, 2021 · 1 min · jiezi

ES6-字符串与正则的扩展

1、字符串的扩展Unicode - \u0000 ~ \uFFFF '\{u0061}' // a '\uD842\uDfB7' "\u{20BB7}" // "????"字符串遍历器 - 识别大于0xFFFF let text2 = String.fromCodePoint(0x20BB7); for(let i = 0;i < text2.length;i++){ console.log(text2[i]); } // � // � for (let i of text){ console.log(i); } // "????"模板字符串 - 保留所有空格与换行 const getName = () => 'Iven'; const myName = 'Eric'; `\`Hello\` ${getName()},I am ${myName}` //"`Hello` Iven,I am Eric"标签模板 alert`111`; // alert(111); func`This ${ a + b} is ${ a * b}`; // func(['This', 'is', ''],a + b, a * b); <div> <span>1111</span> </div>新增方法 ...

August 8, 2019 · 2 min · jiezi

字符串的四则运算表达式

public static void main(String[] args) { // 支持括号 小数 负数 String statement = "-10/(4.5+5.5)*(-4-6+20)/-2"; // 10/(-2) 也行 System.out.println(calculate(statement)); } @SuppressWarnings("unchecked") private static double calculate(String statement){ Object[] result = filter(statement); // 运算数字 List<Double> numList = (List) result[0]; // 运算符 List<Character> symbolList = (List) result[1]; while (!symbolList.isEmpty()) { int index = symbolList.indexOf('('); if (index == -1) { // 没有括号正常运算 realCalculate(numList, symbolList); } else { int right = symbolList.indexOf(')'); if (right == index + 1) { // 括号内的值都取完了,删除括号 symbolList.remove(index); symbolList.remove(index); continue; } // 左右括号齐全 先算括号内的 if (right != -1) { List<Double> doubles = numList.subList(index, right); List<Character> subChars = symbolList.subList(index + 1, right); realCalculate(doubles, subChars); } } } return numList.get(0); } /** * @return 一个包含数字的列表和一个包含运算符的列表 */ private static Object[] filter(String statement) { // 形式 123,456,789 可能会有空字符串 StringBuilder nums = new StringBuilder(); // 符号列表 List<Character> symbolList = new LinkedList<>(); for (int i = 0; i < statement.length(); i++) { char c = statement.charAt(i); if (c == '-' && (i == 0 || statement.charAt(i - 1) == '(' || statement.charAt(i - 1) == '*' || statement.charAt(i - 1) == '/')) { nums.append(c).append(statement.charAt(i + 1)); i++; } else if (Character.isDigit(c) || c == '.') { nums.append(c); } else { symbolList.add(c); nums.append(','); } } String[] ss = nums.toString().split(","); List<Double> numList = new ArrayList<>(); for (String num : ss) { if (!num.isEmpty()) { numList.add(Double.parseDouble(num)); } } return new Object[]{numList, symbolList}; } private static void realCalculate(List<Double> numList, List<Character> symbolList) { while (!symbolList.isEmpty()) { int index = symbolList.indexOf('*'), tmp; double value = 0.0D; if (index != -1 && (tmp = symbolList.indexOf('/')) != -1) { // 同时出现 * / 从左至右运算 if (index < tmp) { value = numList.remove(index) * numList.remove(index); } else { index = tmp; value = numList.remove(index) / numList.remove(index); } } else if (index != -1) { value = numList.remove(index) * numList.remove(index); } else if ((index = symbolList.indexOf('/')) != -1) { value = numList.remove(index) / numList.remove(index); } else if ((index = symbolList.indexOf('+')) != -1 && (tmp = symbolList.indexOf('-')) != -1) { // 同时出现 + - 从左至右运算 if (index < tmp) { value = numList.remove(index) + numList.remove(index); } else { index = tmp; value = numList.remove(index) - numList.remove(index); } } else if (index != -1) { value = numList.remove(index) + numList.remove(index); } else if ((index = symbolList.indexOf('-')) != -1) { value = numList.remove(index) - numList.remove(index); } // 删除运算符 symbolList.remove(index); // 将计算结果放回列表,待下次计算 numList.add(index, value); } }总结一下我的方法是先从括号的算起,根据运算符索引查找运算数索引,从而进行计算,算完后删除运算符和运算数,并将运算结果放回待运算的列表 ...

June 29, 2019 · 2 min · jiezi

Go-字符串编码Unicode-和UTF8

1.字符串字符串在Go语言中以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、 float32、foat64等)一样。 字符串的值为双引号中的内容,可以在Go语言的源码中直接添加非ASCⅡ码字符 Go语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下所示 转移符 含义 \r 回车符(返回行首)\n 换行符(直接跳到下一行的同列位置)\t 制表符\' 单引号\" 双引号\\ 反斜杠2.字符串实现基于UTF-8编码 go 语言里的字符串的内部实现使用UTF8编码. 通过rune类型,可以方便地对每个UTF-8字符进行访问。 当然,Go语言也支持按传统的ASCII码方式进行逐字符访问。 3.字符 字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符非元素时可以获得字符。 Go语言的字符有以下两种: 一种是uint8类型,或者叫byte型,代表了ASCII码的一个字符。另一种是rune类型,代表一个UTF-8字符。当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。使用 fmt.Printf中的“%T”动词可以输出变量的实际类型,使用这个方法可以查看byte和rune的本来类型,代码如下: var a byte = 'a'fmt.Printf("%d %T\n", a, a)var b rune='你'fmt.Printf("%d %T\n", b, b)输出如下97 uint820320 int324.UTF-8和 Unicode有何区别? Unicode是字符集。ASCⅡ也是一种字符集。 字符集为每个字符分配一个唯一的ID,我们使用到的所有字符在 Unicode字符集中都有唯一的一个ID对应,例如上面例子中的a在 Unicode与ASCII中的编码都是97。 “你“在 Unicode中的编码为20320,但是在不同国家的字符集中,“你”的ID会不同。而无论任何情况下, Unicode中的字符的ID都是不会变化的。 UTF-8是编码规则,将 Unicode中字符的ID以某种方式进行编码。UTF-8的是一种变长编码规则,从1到4个字节不等。 5.计算字符串长度 tip := "genji is a ninja"fmt.Println(len(tip))tip2 := "认真"fmt.Println(len(tip2))结果:166len 表示字符串的ASCII 字符个数或字节长度 所以:ASCII 字符串长度使用len() 长度Unicode 字符串长度使用utf8.RuneCountInString() 5.字符串遍历1.遍历每一个 ASCII 字符直接使用for 2.按Unicode 字符遍历字符串使用 range ...

June 1, 2019 · 1 min · jiezi

3分钟干货之字符串匹配类问题的解题技巧

首先要认真审题,避免答偏。可以先确定是单模式匹配问题还是多模式匹配问题,命中条件是否有多个。然后确定对算法时间复杂度或者内存占用是否有额外要求。最后要明确期望的返回值是什么,比如存在有多个命中结果时,是返回第一个命中的,还是全部返回。 关于解题思路,如果是单模式匹配问题,可以考虑使用BM或者KMP算法,如果是多模匹配,可以考虑使用tire树来解决。在实现匹配算法时,可以考虑用前缀或者后缀匹配的方式来进行。最后可以考虑是否能够通过栈、二叉树或者多叉树等数据结构来辅助解决。

May 29, 2019 · 1 min · jiezi

ES6字符串模板引擎4

字符串模板引擎 ES5中的字符串缺乏多行字符串、字符串格式化、HTML转义等特性。 而ES6通过模板字面量的方式进行了填补,模板字面量试着跳出JS已有的字符串体系,通过一些全新的方法来 解决问题。 1.基本用法 ES5字符串写法: let message = "我的宠物狗叫黑子,今年16岁了"将其转化成ES6写法,其实非常简单: 只需把最外围的双引号(")或者单引号(') 转化成反引号(`)即可。 let message = `我的宠物狗叫黑子,今年16岁了`如果想在字符串内部使用反引号,只需使用反斜杠( )转义即可 let message = `我的宠物狗叫\`黑子\`,今年16岁了`;console.log(message); // "我的宠物狗叫`黑子`,今年16岁了" 2.多行字符串 传统的JavaScript语言,输出模板通常是这样写的: var name = '黑子';var age = 8;$('#result').append( '我的宠物狗叫 <b>' + name + '</b>\n' + '今年\n' + '<em>' + age+ '</em>岁,\n'+ '十分可爱!');但是在ES6中,要获得同样效果的多行字符串,只需使用如下代码: let name = '黑子';let age = 8;$('#result').append( `我的宠物狗叫 <b>${name}</b> 今年 <em>${age}</em>岁, 十分可爱!`);对比两段拼接的代码,模板字符串使得我们不再需要反复使用双引号(或者单引号)了;而是改用反引号标识符 (`),插入变量的时候也不需要再使用加号(+)了,而是把变量放入${ }即可。 也不用再通过写 n 进行换行了,ES6 的模板字面量使多行字符串更易创建,因为它不需要特殊的语法,只需在想 要的位置直接换行即可,此处的换行会同步出现在结果中。简单、清晰、明了。 注意:如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。因此需要特别留意缩进。 ...

May 5, 2019 · 2 min · jiezi

javascript实现ArrayBuffer转字符串微信小程序蓝牙数据转换

/** * ArrayBuffer转字符串 * @param {ArrayBuffer} e 需要转换的ArrayBuffer类型数值 * @param {function} t 转换成功后的回调 */ getUint8Value(e, t) { for (var a = e, i = new DataView(a), n = "", s = 0; s < i.byteLength; s++) n += String.fromCharCode(i.getUint8(s)); t(n); }效果图显示: 微信小程序蓝牙通讯wx.onBLECharacteristicValueChange返回的value值是 ArrayBuffer类型,需要转换前端才能进行naf

April 26, 2019 · 1 min · jiezi

动态字符串SDS的实现 | 自己实现Redis源代码(1)

通过对《Redis设计与实现》一书的学习,我打算动手自己实现一份“Redis源代码”作为自己的学习记录。对Redis感兴趣的同学可以查看我的另一篇文章 造个轮子 | 自己动手写一个Redis。本章介绍的是Redis源代码中的动态字符串SDS的实现。动态字符串SDS的实现SDS的API(1)创建一个包含给定c字符串的sdssds sdsnew(char *);(2)为sds(也就是buf数组)分配指定空间sds sdsnewlen(sds,int);(3)创建一个不包含任何内容的空字符串sds sdsempty(void);(4)释放给定的sdsvoid sdsfree(sds);(5)创建一个给定sds的副本sds sdsdup(sds);(6)清空sds保存的字符串内容sds sdsclear(sds);(7)将给定c字符串拼接到另一个sds字符串的末尾sds sdscat(sds,char *);(8)将给定sds字符串拼接到另一个sds字符串的末尾sds sdscatsds(sds,sds);(9)将给定的c字符串复制到sds里面,覆盖原有的字符串sds sdscpy(sds,char *);(10)保留sds给定区间内的数据sds sdsrange(sds,int,int);(11)从sds中移除所有在c字符串中出现过的字符sds sdstrim(sds,const char );(12)对比两个sds字符串是否相同bool sdscmp(sds,sds);头文件#ifndef SDS_H#define SDS_H//实现Redis中的动态字符串//SDS:simple dynamic stringtypedef struct sdshdr{ //记录buf数组中已使用字节的数量 //等于SDS所保存字符串的长度,不 //包括最后的’\0’; int len; //记录buf数组中未使用字节的数量 int free; //字节数组,用于保存字符串,以 //’\0’结束 char buf;}*sds;//返回sds已使用空间的字节数:lenstatic inline int sdslen(const sds sh){ return sh->len;}//返回sds未使用空间的字节数:freestatic inline int sdsavail(const sds sh){ return sh->free;}//创建一个包含给定c字符串的sdssds sdsnew(char *);//为sds(也就是buf数组)分配指定空间/lensds sdsnewlen(sds,int);//创建一个不包含任何内容的空字符串sds sdsempty(void);//释放给定的sdsvoid sdsfree(sds);//创建一个给定sds的副本sds sdsdup(sds);//清空sds保存的字符串内容sds sdsclear(sds);//将给定c字符串拼接到另一个sds字符串的末尾sds sdscat(sds,char );//将给定sds字符串拼接到另一个sds字符串的末尾sds sdscatsds(sds,sds);//将给定的c字符串复制到sds里面,覆盖原有的字符串sds sdscpy(sds,char );//保留sds给定区间内的数据,不在区间内的数据会被覆盖或清除//s = sdsnew(“Hello World”);//sdsrange(s,1,-1); => “ello World"sds sdsrange(sds,int,int);//接受一个sds和一个c字符串作为参数,从sds中移除所有在c字符串中出现过的字符//s = sdsnew(“AA…AA.a.aa.aHelloWorld :::”);//s = sdstrim(s,“A. :”);//printf("%s\n”, s);//Output will be just “Hello World”.//大小写不敏感sds sdstrim(sds,const char );//对比两个sds字符串是否相同bool sdscmp(sds,sds);#endifSDS API的实现#include <stdio.h>#include <stdlib.h>#include <string.h>#include “sds.h”//创建一个包含给定c字符串的sdssds sdsnew(char init){ sds sh=(sds)malloc(sizeof(struct sdshdr)); sh->len=strlen(init); sh->free=0; sh->buf=(char)malloc(sizeof(char)(sh->len+1)); //将字符串内容进行复制 int i; for(i=0;i<sh->len;i++){ (sh->buf)[i]=init[i]; } (sh->buf)[i]=’\0’; return sh;}//为sds(也就是buf数组)分配指定空间/lensds sdsnewlen(sds sh,int len){ int i; sh->free=len-1-sh->len; //保存之前的buf内容 char str=(char )malloc(sizeof(char)(sh->len+1)); for(i=0; i<(sh->len); i++){ str[i]=sh->buf[i]; } str[i]=’\0’; //sh->buf=(char)realloc(sh->buf,len); sh->buf=(char)malloc(sizeof(char)len); for(i=0; i<(sh->len); i++){ sh->buf[i]=str[i]; } sh->buf[i]=’\0’; free(str); return sh;}//创建一个不包含任何内容的空字符串sds sdsempty(void){ sds sh=(sds)malloc(sizeof(struct sdshdr)); sh->len=0; sh->free=0; sh->buf=(char)malloc(sizeof(char)); sh->buf[0]=’\0’; return sh;}//释放给定的sdsvoid sdsfree(sds sh){ (sh)->free=0; (sh)->len=0; free((sh)->buf); free(sh);}//创建一个给定sds的副本sds sdsdup(sds sh01){ sds sh02=(sds)malloc(sizeof(struct sdshdr)); sh02->free=sh01->free; sh02->len=sh01->len; sh02->buf=(char)malloc(sizeof(char)(sh02->free+sh02->len+1)); int i; for(i=0;i<sh01->len;i++){ sh02->buf[i]=sh01->buf[i]; } sh02->buf[i]=’\0’; return sh02;}//清空sds保存的字符串内容sds sdsclear(sds sh){ int total=sh->len+sh->free+1; sh->len=0; sh->free=total-1; sh->buf[0]=’\0’; return sh;}//将给定c字符串拼接到另一个sds字符串的末尾//先检查sds的空间是否满足修改所需的要求,如//果不满足则自动将sds空间扩展至执行修改所需//要的大小,然后在执行实际的修改操作——防止//缓冲区溢出//扩展空间的原则:拼接后的字符串是n个字节,则//再给其分配n个字节的未使用空间,buf数组的实际长度为n+n+1//当n超过1MB的时候,则为其分配1MB的未使用空间//两个字符串cat,中间使用空格隔开sds sdscat(sds sh,char str){ int newlen=strlen(str); int newfree; //剩余的空间不够cat操作 if(sh->free<=newlen){ //超出部分的空间 newfree=newlen-sh->free; if(newfree<1024){ newfree=newfree+newfree+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); }else{ newfree=newfree+1024+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); } } int i; //执行cat操作 sh->buf[sh->len]=’ ‘; for(i=0;i<newlen;i++){ sh->buf[sh->len+i+1]=str[i]; } sh->buf[sh->len+i+1]=’\0’; sh->len+=(newlen+1); sh->free-=newlen; return sh;}//将给定sds字符串拼接到另一个sds字符串的末尾sds sdscatsds(sds sh,sds str){ int newlen=str->len; int newfree; //剩余的空间不够cat操作 if(sh->free<=newlen){ //超出部分的空间 newfree=newlen-sh->free; if(newfree<1024){ newfree=newfree+newfree+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); }else{ newfree=newfree+1024+1+sh->len+sh->free; sh=sdsnewlen(sh,newfree); } } int i; //执行cat操作 sh->buf[sh->len]=’ ‘; for(i=0;i<newlen;i++){ sh->buf[sh->len+i+1]=str->buf[i]; } sh->buf[sh->len+i+1]=’\0’; sh->len+=(newlen+1); sh->free-=newlen; return sh;}//将给定的c字符串复制到sds里面,覆盖原有的字符串//需要先检查sds sdscpy(sds sh,char str){ //新来的长度 int len=strlen(str); //需要使用到的新空间长度 int newlen=len-sh->len; int total; //剩余的空间不够了需要重新分配,在copy if(newlen>=sh->free){ //新空间长度大于1M,就只多分配newlen+1M+1 //总的空间是len+newlen+1M+1 if(newlen>=1024){ total=len+newlen+1024+1; //copy后使用到的len,就是新字符串的长度 sh->len=len; //空闲的空间长度 //sh->free=total-len-1; //sh->buf=(char)realloc(sh->buf,total); sh=sdsnewlen(sh,total); //分配newlen+newlen+1 }else{ total=len+newlen+newlen+1; sh->len=len; //sh->free=total-len-1; //sh->buf=(char)realloc(sh->buf,total); sh=sdsnewlen(sh,total); } if(sh->buf==NULL){ printf(“PIG Redis ERROR : Realloc failed.\n”); } }else{ //剩余的空间够,不需要分配 //原来拥有的总空间 total=sh->len+sh->free; sh->len=len; sh->free=total-sh->len; } //开始copy int i; for(i=0;i<len;i++){ (sh->buf)[i]=str[i]; } sh->buf[i]=’\0’; return sh;}//保留sds给定区间内的数据,不在区间内的数据会被覆盖或清除//s = sdsnew(“Hello World”);//sdsrange(s,1,-1); => “ello World"sds sdsrange(sds sh,int start,int end){ int newlen=end-start+1; char str=(char)malloc(sizeof(char)(sh->len+1)); //sh1->free=sh->len-sh1->len; int i,j; for(i=start,j=0;i<=end;i++,j++){ str[j]=sh->buf[i]; } str[j]=’\0’; sh->buf=(char)malloc(sizeof(char)(sh->len+1)); sh->free=sh->len-newlen; sh->len=newlen; for(i=0;i<strlen(str);i++){ sh->buf[i]=str[i]; } sh->buf[i]=’\0’; free(str); return sh;}//接受一个sds和一个c字符串作为参数,从sds中移除所有在c字符串中出现过的字符//s = sdsnew(“AA…AA.a.aa.aHelloWorld :::”);//s = sdstrim(s,“A. :”);//printf("%s\n”, s);//Output will be just “Hello World”.//截断操作需要通过内存重分配来释放字符串中不再使用的空间,否则会造成内存泄漏//大小写不敏感//使用惰性空间释放优化字符串的缩短操作,执行缩短操作的时候,不立即使用内存重分//配来回收缩短后多出来的字节,而是使用free属性记录这些字节,等待将来使用sds sdstrim(sds s,const char chstr);//对比两个sds字符串是否相同bool sdscmp(sds sh1,sds sh2){ if(sh1->len!=sh2->len){ return false; } for(int i=0;i<sh1->len;i++){ if(sh1->buf[i]!=sh2->buf[i]){ return false; } } return true;}int main(){ printf(“sdsnew(‘sss’)\n”); sds sh=sdsnew(“sss”); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); printf(“sdscat(sh,‘www’)\n”); sh=sdscat(sh,“www”); printf("%s\n",sh->buf); /for(int i=0;i<sh->len;i++){ printf("%c",sh->buf[i]); }/ printf("%d\n",sh->len); printf("%d\n",sh->free); sds sh1=sdsnew(“qqqq”); sh=sdscatsds(sh,sh1); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); sh=sdsrange(sh,1,5); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); sds sh3=sdsnew(“qqqq”); sds sh4=sdsnew(“qqqq”); if(sdscmp(sh3,sh4)){ printf(“same\n”); }else{ printf(“no same\n”); }/ printf(“sdscpy(sh,‘wwww’)\n”); sh=sdscpy(sh,“wwww”); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); printf(“sdsnewlen(sh,12)\n”); sh=sdsnewlen(sh,12); printf("%s\n",sh->buf); printf("%d\n",sh->len); printf("%d\n",sh->free); printf(“sdsdup(sh)\n”); sds sh1=sdsdup(sh); printf("%s\n",sh1->buf); printf("%d\n",sh1->len); printf("%d\n",sh1->free); printf(“sdsclear(sh1)\n”); sh1=sdsclear(sh1); printf("%s\n",sh1->buf); printf("%d\n",sh1->len); printf("%d\n",sh1->free);/ sdsfree(&sh); sdsfree(&sh1); //sdsfree(&sh2); sdsfree(&sh3); sdsfree(&sh4); system(“pause”); return 0;} ...

March 5, 2019 · 2 min · jiezi

【现代C++】性能控的工具箱之string_view

本篇文章从string_view引入的背景出发,依次介绍了其相关的知识点及使用方式,然后对常见的使用陷阱进行了说明,最后对该类型做总结。一、背景在日常C/C++编程中,我们常进行数据的传递操作,比如,将数据传给函数。当数据占用的内存较大时,减少数据的拷贝可以有效提高程序的性能。在C中指针是完成这一目的的标准数据结构,而C++引入了安全性更高的引用类型。所以在C++中若传递的数据仅仅只读,const string&成了C++的天然的方式。但这并非完美,从实践来看,它至少有以下几方面问题:字符串字面值、字符数组、字符串指针的传递仍要数据拷贝这三类低级数据类型与string类型不同,传入时,编译器需要做隐式转换,即需要拷贝这些数据生成string临时对象。const string&指向的实际上是这个临时对象。通常字符串字面值较小,性能损耗可以忽略不计;但字符串指针和字符数组某些情况下可能会比较大(比如读取文件的内容),此时会引起频繁的内存分配和数据拷贝,会严重影响程序的性能。substr O(n)复杂度这是一个特别常用的函数,好在std::string提供了这个函数,美中不足的是其每次都返回一个新生成的子串,很容易引起性能热点。实际上我们本意并不是要改变原字符串,为什么不在原字符串基础上返回呢?在C++17中引入了string_view,能很好的解决以上两个问题。二、std::string_view从名字出发,我们可以类比数据库视图,view表示该类型不会为数据分配存储空间,而且该数据类型只能用来读。该数据类型可通过{数据的起始指针,数据的长度}两个元素表示,实际上该数据类型的实例不会具体存储原数据,仅仅存储指向的数据的起始指针和长度,所以这个开销是非常小的。要使用字符串视图,需要引入<string_view>,下面介绍该数据类型主要的API。这些API基本上都有constexpr修饰,所以能在编译时很好地处理字符串字面值,从而提高程序效率。2.1 构造函数constexpr string_view() noexcept;constexpr string_view(const string_view& other) noexcept = default;constexpr string_view(const CharT* s, size_type count);constexpr string_view(const CharT* s);基本上都是自解释的,唯一需要说明的是:为什么我们代码string_view foo(string(“abc”))可以编译通过,但为什么没有对应的构造函数?实际上这是因为string类重载了string到string_view的转换操作符:operator std::basic_string_view<CharT, Traits>() const noexcept;所以,string_view foo(string(“abc”))实际执行了两步操作:string(“abc”)转换为string_view对象astring_view使用对象本篇文章从string_view引入的背景,2.2 自定义字面量自定义字面量也是C++17新增的特性,提高了常量的易读。下面的代码取值cppreference,能很好地说明自定义字面值和字符串语义的差异。#include <string_view>#include <iostream> int main(){ using namespace std::literals; std::string_view s1 = “abc\0\0def”; std::string_view s2 = “abc\0\0def"sv; std::cout << “s1: " << s1.size() << " "” << s1 << “"\n”; std::cout << “s2: " << s2.size() << " "” << s2 << “"\n”;}输出:s1: 3 “abc"s2: 8 “abc^@^@def"以上例子能很好看清二者的语义区别,\0对于字符串而言,有其特殊的意义,即表示字符串的结束,字符串视图根本不care,它关心实际的字符个数。2.3 成员函数下面列举其成员函数:忽略了函数的返回值,若函数有重载,括号内用…填充。这样可以对其有个整体轮廓。// 迭代器begin()end()cbegin()cend()rbegin()rend()crbegin()crend() // 容量size()length()max_size()empty() // 元素访问operator[](size_type pos)at(size_type pos)front()back()data() // 修改器remove_prefix(size_type n)remove_suffix(size_type n)swap(basic_string_view& s) copy(charT* s, size_type n, size_type pos = 0)string_view substr(size_type pos = 0, size_type n = npos)compare(…)starts_with(…)ends_with(…)find(…)rfind(…)find_first_of(…)find_last_of(…) find_first_not_of(…)find_last_not_of(…)从函数列表来看,几乎跟string的只读函数一致,使用string_view的方式跟string基本一致。有几个地方需要特别说明:string_view的substr函数的时间复杂度是O(1),解决了背景部分的第二个问题。修改器中的三个函数仅会修改string_view的数据指向,不会修改指向的数据。除此之外,函数名基本是自解释的。2.4 示例Haskell中有一个常用函数lines,会将字符串切割成行存储在容器里。下面我们用C++来实现string-版本#include <string>#include <iostream>#include <vector>#include <algorithm>#include <sstream>void lines(std::vector<std::string> &lines, const std::string &str) { auto sep{"\n”}; size_t start{str.find_first_not_of(sep)}; size_t end{}; while (start != std::string::npos) { end = str.find_first_of(sep, start + 1); if (end == std::string::npos) end = str.length(); lines.push_back(str.substr(start, end - start)); start = str.find_first_not_of(sep, end + 1); }}上面我们用const std::string &类型接收待分割的字符串,若我们传入指向较大内存的字符指针时,会影响程序效率。使用std::string_view可以避免这种情况:string_view-版本#include <string>#include <iostream>#include <vector>#include <algorithm>#include <sstream>#include <string_view>void lines(std::vector<std::string> &lines, std::string_view str) { auto sep{"\n”}; size_t start{str.find_first_not_of(sep)}; size_t end{}; while (start != std::string_view::npos) { end = str.find_first_of(sep, start + 1); if (end == std::string_view::npos) end = str.length(); lines.push_back(std::string{str.substr(start, end - start)}); start = str.find_first_not_of(sep, end + 1); }}上面的例子仅仅是把string类型修改成了string_view就获得了性能上的提升。一般情况下,将程序中的string换成string_view的过程是比较直观的,这得益于两者的成员函数的相似性。但并不是所有的“翻译”过程都是这样的,比如:void lines(std::vector<std::string> &lines, const std::string& str) { std::stringstream ss(str); std::string line; while (std::getline(ss, line, ‘\n’)) { lines.push_back(line); }}这个版本使用stringstream实现lines函数。由于stringstream没有相应的构造函数接收string_view类型参数,所以没法采用直接替换的方式,所以翻译过程要复杂点。三、使用陷阱世上没有免费的午餐。不恰当的使用string_view也会带来一系列的问题。string_view范围内的字符可能不包含\0如#include <iostream>#include <string_view>int main() { std::string_view str{“abc”, 1}; std::cout << str.data() << std::endl; return 0;}本来是要打印a,但输出了abc。这是因为字符串相关的函数都有一条兼容C的约定:\0代表字符串的结尾。上面的程序打印从开始到字符串结束的所有字符,虽然str包含的有效字符是a,但cout认\0。好在这块内存空间有合法的字符串结尾符,如果str指向的是一个没有\0的字符数组,程序很有可能会出现内存问题,所以我们在将string_view类型的数据传入接收字符串的函数时要非常小心。2.从[const] char构造string_view对象时间复杂度O(n) 这是因为获取字符串的长度需要从头开始遍历。如果对[const] char类型仅仅是一些O(1)的操作,相比直接使用[const] char*,转为string_view是没有性能优势的。只不过是相比const string&,string_view少了拷贝的损耗。实际上我们完全可以用[const] char*接收所有的字符串,但这个类型太底层了,不便使用。在某些情况下,我们转为string_view可能仅仅是想用其中的一些函数,比如substr。3.string_view指向的内容的生命周期可能比其本身短string_view并不拥有其指向内容的所有权,用Rust的术语来说,它仅仅是暂时borrow(借用)了它。如果拥有者提前释放了,你还在使用这些内容,那会出现内存问题,这跟悬挂指针(dangling pointer)或悬挂引用(dangling references)很像。Rust专门有套机制在编译时分析变量的生命期,保证borrow的资源在使用期间不会被释放,但C++没有这样的检查,需要人工保证。下面列出一些典型的问题情况:std::string_view sv = std::string{“hello world”}; string_view foo() { std::string s{“hello world”}; return string_view{s};}auto id(std::string_view sv) { return sv; }int main() { std::string s = “hello”; auto sv = id(s + " world”); }四、总结string_view解决了一些痛点,但同时也引入了指针和引用的一些老问题。C++标准并没有对这个类型做太多的约束,这引来的问题是我们可以像平常的变量一样以多种方式使用它,如,可以传参,可以作为函数返回值,可以做普遍变量,甚至我们可以放到容器里。随着使用场景的复杂,人工是很难保证指向的内容的生命周期足够长。所以,推荐的使用方式:仅仅作为函数参数,因为如果该参数仅仅在函数体内使用而不传递出去,这样使用是安全的。 请关注我的公众号哦。 ...

March 5, 2019 · 2 min · jiezi

30秒的PHP代码片段(3)字符串-String & 函数-Function

本文来自GitHub开源项目点我跳转30秒的PHP代码片段精选的有用PHP片段集合,您可以在30秒或更短的时间内理解这些片段。字符串endsWith判断字符串是否以指定后缀结尾,如果以指定后缀结尾返回true,否则返回false。function endsWith($haystack, $needle){ return strrpos($haystack, $needle) === (strlen($haystack) - strlen($needle));}ExamplesendsWith(‘Hi, this is me’, ‘me’); // truefirstStringBetween返回参数start和end中字符串之间的第一个字符串。function firstStringBetween($haystack, $start, $end){ return trim(strstr(strstr($haystack, $start), $end, true), $start . $end);}ExamplesfirstStringBetween(‘This is a [custom] string’, ‘[’, ‘]’); // customisAnagram检查一个字符串是否是另一个字符串的变位元(不区分大小写,忽略空格、标点符号和特殊字符)。就是所谓的字谜function isAnagram($string1, $string2){ return count_chars($string1, 1) === count_chars($string2, 1);}ExamplesisAnagram(‘fuck’, ‘fcuk’); // trueisAnagram(‘fuckme’, ‘fuckyou’); // falseisLowerCase如果给定字符串是小写的,则返回true,否则返回false。function isLowerCase($string){ return $string === strtolower($string);}ExamplesisLowerCase(‘Morning shows the day!’); // falseisLowerCase(‘hello’); // trueisLowerCase如果给定字符串为大写,则返回true,否则返回false。function isUpperCase($string){ return $string === strtoupper($string);}ExamplesisUpperCase(‘MORNING SHOWS THE DAY!’); // trueisUpperCase(‘qUick Fox’); // falsepalindrome如果给定字符串是回文,则返回true,否则返回false。回文,顾名思义,即从前往后读和从后往前读是相等的function palindrome($string){ return strrev($string) === (string) $string;}Examplespalindrome(‘racecar’); // truepalindrome(2221222); // truestartsWith检查字符串是否是以指定子字符串开头,如果是则返回true,否则返回false。function startsWith($haystack, $needle){ return strpos($haystack, $needle) === 0;}ExamplesstartsWith(‘Hi, this is me’, ‘Hi’); // truecountVowels返回给定字符串中的元音数。使用正则表达式来计算字符串中元音(A, E, I, O, U)的数量。function countVowels($string){ preg_match_all(’/[aeiou]/i’, $string, $matches); return count($matches[0]);}ExamplescountVowels(‘sampleInput’); // 4decapitalize使字符串的第一个字母去大写。对字符串的第一个字母进行无头化,然后将其与字符串的其他部分相加。省略upperRest参数以保持字符串的其余部分完整,或将其设置为true以转换为大写。function decapitalize($string, $upperRest = false){ return lcfirst($upperRest ? strtoupper($string) : $string);}Examplesdecapitalize(‘FooBar’); // ‘fooBar’isContains检查给定字符串输入中是否存在单词或者子字符串。使用strpos查找字符串中第一个出现的子字符串的位置。返回true或false。function isContains($string, $needle){ return strpos($string, $needle);}ExamplesisContains(‘This is an example string’, ’example’); // trueisContains(‘This is an example string’, ‘hello’); // false函数compose返回一个将多个函数组合成单个可调用函数的新函数。function compose(…$functions){ return array_reduce( $functions, function ($carry, $function) { return function ($x) use ($carry, $function) { return $function($carry($x)); }; }, function ($x) { return $x; } );}…为可变数量的参数,http://php.net/manual/zh/func…Examples$compose = compose( // add 2 function ($x) { return $x + 2; }, // multiply 4 function ($x) { return $x * 4; });$compose(3); // 20memoize创建一个会缓存func结果的函数,可以看做是全局函数。function memoize($func){ return function () use ($func) { static $cache = []; $args = func_get_args(); $key = serialize($args); $cached = true; if (!isset($cache[$key])) { $cache[$key] = $func(…$args); $cached = false; } return [‘result’ => $cache[$key], ‘cached’ => $cached]; };}Examples$memoizedAdd = memoize( function ($num) { return $num + 10; });var_dump($memoizedAdd(5)); // [‘result’ => 15, ‘cached’ => false]var_dump($memoizedAdd(6)); // [‘result’ => 16, ‘cached’ => false]var_dump($memoizedAdd(5)); // [‘result’ => 15, ‘cached’ => true]curry(柯里化)把函数与传递给他的参数相结合,产生一个新的函数。function curry($function){ $accumulator = function ($arguments) use ($function, &$accumulator) { return function (…$args) use ($function, $arguments, $accumulator) { $arguments = array_merge($arguments, $args); $reflection = new ReflectionFunction($function); $totalArguments = $reflection->getNumberOfRequiredParameters(); if ($totalArguments <= count($arguments)) { return $function(…$arguments); } return $accumulator($arguments); }; }; return $accumulator([]);}Examples$curriedAdd = curry( function ($a, $b) { return $a + $b; });$add10 = $curriedAdd(10);var_dump($add10(15)); // 25once只能调用一个函数一次。function once($function){ return function (…$args) use ($function) { static $called = false; if ($called) { return; } $called = true; return $function(…$args); };}Examples$add = function ($a, $b) { return $a + $b;};$once = once($add);var_dump($once(10, 5)); // 15var_dump($once(20, 10)); // nullvariadicFunction(变长参数函数)变长参数函数允许使用者捕获一个函数的可变数量的参数。函数接受任意数量的变量来执行代码。它使用for循环遍历参数。function variadicFunction($operands){ $sum = 0; foreach($operands as $singleOperand) { $sum += $singleOperand; } return $sum;}ExamplesvariadicFunction([1, 2]); // 3variadicFunction([1, 2, 3, 4]); // 10相关文章:30秒的PHP代码片段(1)数组 - Array30秒的PHP代码片段(2)数学 - Math ...

February 19, 2019 · 2 min · jiezi

在Java虚拟机中,字符串常量到底存放在哪

前言前阵子和朋友讨论一个问题:字符串常量归常量池管理,那比如 String str = “abc”; “abc"这个对象是放在内存中的哪个位置,是字符串常量池中还是堆?”这句代码的abc当然在常量池中,只有new String(“abc”)这个对象才在堆中创建“,他们大概是这么回答。“abc”这个东西,是放在常量池中,这个答案是错误的。字符串“abc"的本体、实例,应该是存在于Java堆中。可能还真的有部分同学对这个知识点不熟悉,今天和大家聊聊字符串这个问题 ~初学Java时,学到字符串这一部分,有一段代码String str1 = “hello”;String str2 = new String(“hello”); 书上的解释是:执行第一行的时候,已经把"hello"字符串放到了常量池中,执行第二行代码时,会将常量池中已经存在的"hello"复制一份到堆内存中,创建一个的新的String对象。虽然值一样,但他们是不同的对象。当时看完这个解释,我产生了很多疑惑。因为在此之前已经知道字符串的底层是char数组实现的。我很疑惑:他copy一份过去,是copy了char数组呢?还是copy整个String对象?“hello” 这个对象实例真的存放在常量池中吗?当时在网上搜了一些文章和答案,各有说辞,大部分回答都是 “str” 这个对象在常量池中,但也有认为字符串常量实例(或叫对象)是在堆中创建,只是将其引用放到字符串常量池中,交给常量池管理。JAVA内存区域 — 运行时数据区理清这个问题前,需要梳理一下前置知识。从一个经典的示意图讲起,以hotspot虚拟机为例,此内存模型需建立在JDK1.7之前的版本来讨论,JDK1.7之后有所改变,但是原理还是一样的。Java虚拟机管理的内存是运行时数据区那一部分,简单概括一下其中各个区域的区别:虚拟机栈:线程私有,生命周期与线程相同,即每条线程都一个独立的栈(VM Stack)。每个方法执行时都会创建一个栈帧,也就是说,当有一条线程执行了多个方法时,就会有一个栈,栈中有多个栈帧。本地方法栈:线程私有程序计数器:线程私有堆Heap:线程共享,是Java虚拟机所管理的内存中最大的一块,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。 (原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated) 但有特殊情况,随着JIT编译器的发展,逃逸分析和标量替换技术的逐渐成熟,对象也可以在栈上分配。另外,虽说堆是线程共享,但其中也可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。方法区:线程共享,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。JAVA的三种常量池此外,Java有三种常量池,即字符串常量池(又叫全局字符串池)、class文件常量池、运行时常量池。 (图一)1. 字符串常量池(也叫全局字符串池、string pool、string literal pool)字符串常量池在每个VM中只有一份,他在内存中的位置如图,红色箭头所指向的区域 Interned Strings2. 运行时常量池(runtime constant pool)当程序运行到某个类时,class文件中的信息就会被解析到内存的方法区里的运行时常量池中。看图可清晰感知到每一个类被加载进来都会产生一个运行时常量池,由此可知,每个类都有一个运行时常量池。它在内存中的位置如图,蓝色箭头所指向的区域,方法区中的Class Date中的运行时常量池(Run-Time Constant Pool) (图二)3. class文件常量池(class constant pool)class常量池是在编译后每个class文件都有的,class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是 常量池(constant pool table),用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic References)。字面量就是我们所说的常量概念,如文本字符串、被声明为final的常量值等。他在class文件中的位置如上图所示,Constant Pool 中。个人理解public static void main(String[] args) { String str = “hello”;}回到一开始说到的这句代码,可以来总结一下它的执行过程了。首先,字面量 “hello” 在编译期,就会被记录在 class文件的 class常量池中。而当 class文件被加载到内存中后,JVM就会将 class常量池中的大部分内容存放到运行时常量池中,但是字符串 “hello” 的本体(对象)和其他所有对象一样,是会在堆中创建,再将引用放到字符串常量池,也就是图一的 Interned Strings的位置。(RednaxelaFX 的文章里,测试结果是在新生代的Eden区。但因为一直有一个引用驻留在字符串常量池,所以不会被GC清理掉)而到了String str = “hello” 这步,JVM会去字符串常量池中找,如果找到了,JVM会在栈中的局部变量表里创建str变量,然后把字符串常量池中的(hello 对象的)引用复制给 str 变量。在《深入理解Java虚拟机》这本书中也有字符串相关的解释,举其中几个例子:例子1(原文)运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。最后一句描述不太准确,编译期生成的各种字面量并不是全部进入方法区的运行时常量池中。字符串字面量就不进入运行时常量池,而是在堆中创建了对象,再将引用驻留到字符串常量池中。例子2代码清单2-7 String.intern()返回引用的测试public class RuntimeConstantPoolOOM{ public static void main(String[]args){ String str1=new StringBuilder(“计算机”).append(“软件”).toString(); System.out.println(str1.intern()==str1); String str2=new StringBuilder(“ja”).append(“va”).toString(); System.out.println(str2.intern()==str2); }}(原文)这段代码在JDK 1.6中运行,会得到两个false,而在JDK 1.7中运行,会得到一个true和一个false。产生差异的原因是:在JDK 1.6中,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。而JDK 1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串实例是同一个。对str2比较返回false是因为 “java” 这个字符串在执行StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。原文解释也不太准确,我觉得在 JDK 1.6中,intern()并不会把首次遇到的字符串实例复制到永久代中,而是会将实例再复制一份到堆(heap)中,然后将其引用放入字符串常量池中进行管理,所以此代码返回false。而JDK1.7中的intern()不会再复制实例,直接将首次遇到的此字符串实例的引用,放入字符串常量池,于是返回true。关于此观点,还没看到大神文章实锤,欢迎讨论。最后再延伸一点,大家都知道,字符串的value是final修饰的char数组,那么以下这段代码:// private final char value[];String str1 = “hello world”;String str2 = new String(“hello world”);String str3 = new String(“hello world”);str1、str2、str3 三个变量所指向的都是不同的对象。(str1 != str2 != str3)那么,这三个对象里的char数组是否是同一个数组?相信大家都有答案了。此文所讨论的Java内存模型是建立在JDK1.7之前。JDK 7开始 Hotspot 虚拟机把字符串常量池(Interned String位置)从永久代(PermGen)挪到Heap堆,JDK 8又彻底取消 PermGen,把诸如klass之类的元数据都挪到GC堆之外管理。但不管怎样,基本原理还是不变的,字面量 ”hello“ 等依旧不是放在 Interned String 中。推荐文章:请别再拿 “String s = new String(“xyz”); 创建了多少个String实例” 来面试了吧 借HSDB来探索HotSpot VM的运行时数据 作者:RednaxelaFX,曾为《深入理解Java虚拟机》提推荐语java用这样的方式生成字符串:String str = “Hello”,到底有没有在堆中创建对象? - 胖君的回答 - 知乎隆鹏广州芦苇科技Java开发团队芦苇科技-广州专业互联网软件服务公司抓住每一处细节 ,创造每一个美好关注我们的公众号,了解更多想和我们一起奋斗吗?lagou搜索“ 芦苇科技 ”或者投放简历到 server@talkmoney.cn 加入我们吧关注我们,你的评论和点赞对我们最大的支持 ...

January 19, 2019 · 1 min · jiezi

leetcode讲解--937. Reorder Log Files

题目You have an array of logs. Each log is a space delimited string of words.For each log, the first word in each log is an alphanumeric identifier. Then, either:Each word after the identifier will consist only of lowercase letters, or;Each word after the identifier will consist only of digits.We will call these two varieties of logs letter-logs and digit-logs. It is guaranteed that each log has at least one word after its identifier.Reorder the logs so that all of the letter-logs come before any digit-log. The letter-logs are ordered lexicographically ignoring identifier, with the identifier used in case of ties. The digit-logs should be put in their original order.Return the final order of the logs.Example 1:Input: [“a1 9 2 3 1”,“g1 act car”,“zo4 4 7”,“ab1 off key dog”,“a8 act zoo”]Output: [“g1 act car”,“a8 act zoo”,“ab1 off key dog”,“a1 9 2 3 1”,“zo4 4 7”]Note:0 <= logs.length <= 1003 <= logs[i].length <= 100logs[i] is guaranteed to have an identifier, and a word after the identifier.题目地址讲解这道题我看过后立马联想到了另一个题目:953. Verifying an Alien Dictionary。那个题目是判断是否有序,而现在这个题目是要排序。感觉这一题更难。实际做下来确实还是比较难的,主要是字符串的比较,要自己写比较规则。然后在调用系统的sort函数。如果要自己写快排那又要多花不少时间了。有个小窍门,题目要求数字的logs保持原样,而且要放在数组尾部,所以我这里是从后往前遍历数组。Java代码class Solution { public String[] reorderLogFiles(String[] logs) { String[] result = new String[logs.length]; int indexBegin = 0; int indexEnd = logs.length-1; for(int i=logs.length-1;i>=0;i–){ int firstSpaceIndex = logs[i].indexOf(’ ‘); if(logs[i].charAt(firstSpaceIndex+1)>=‘0’ && logs[i].charAt(firstSpaceIndex+1)<=‘9’){ result[indexEnd–] = logs[i]; }else{ result[indexBegin++] = logs[i]; } } // for(int i=0;i<logs.length;i++){ // System.out.println(result[i]); // } StringComparator comparator = new StringComparator(); Arrays.sort(result, 0, indexBegin, comparator); return result; } class StringComparator implements Comparator<String>{ @Override public int compare(String o1, String o2) { int o1IndexOfFirstSpace = o1.indexOf(’ ‘); o1 = o1.substring(o1IndexOfFirstSpace); int o2IndexOfFirstSpace = o2.indexOf(’ ‘); o2 = o2.substring(o2IndexOfFirstSpace); int minLength = o1.length(); boolean o1ShortThano2 = true; if(o1.length()>o2.length()){ minLength = o2.length(); o1ShortThano2 = false; } boolean o1LittleThano2 = true; boolean o1Equalso2 = true; for(int i=0;i<minLength;i++){ if(o1.charAt(i)<o2.charAt(i)) { o1Equalso2 = false; break; }else if(o1.charAt(i)>o2.charAt(i)){ o1LittleThano2 = false; o1Equalso2 = false; break; } } if(o1Equalso2){ if(o1ShortThano2){ return -1; }else{ return 1; } }else{ if(o1LittleThano2){ return -1; }else{ return 1; } } } }} ...

January 4, 2019 · 2 min · jiezi

leetcode讲解--953. Verifying an Alien Dictionary

题目In an alien language, surprisingly they also use english lowercase letters, but possibly in a different order. The order of the alphabet is some permutation of lowercase letters.Given a sequence of words written in the alien language, and the order of the alphabet, return true if and only if the given words are sorted lexicographicaly in this alien language.Example 1:Input: words = [“hello”,“leetcode”], order = “hlabcdefgijkmnopqrstuvwxyz"Output: trueExplanation: As ‘h’ comes before ’l’ in this language, then the sequence is sorted.Example 2:Input: words = [“word”,“world”,“row”], order = “worldabcefghijkmnpqstuvxyz"Output: falseExplanation: As ’d’ comes after ’l’ in this language, then words[0] > words[1], hence the sequence is unsorted.Example 3:Input: words = [“apple”,“app”], order = “abcdefghijklmnopqrstuvwxyz"Output: falseExplanation: The first three characters “app” match, and the second string is shorter (in size.) According to lexicographical rules “apple” > “app”, because ’l’ > ‘∅’, where ‘∅’ is defined as the blank character which is less than any other character (More info).Note:1 <= words.length <= 1001 <= words[i].length <= 20order.length == 26All characters in words[i] and order are english lowercase letters.题目地址讲解这道题讲道理,我觉得有点难。可能递归的解法比较简单吧。刚开始我思路一直是错的,正确的思路应该是先深入比较两个字符串,没有问题再比较下一对字符串(这是一种深度优先,我刚开始一直是广度优先的思路)。主要的麻烦来自于对数组越界的判断。java代码class Solution { public boolean isAlienSorted(String[] words, String order) { if(words.length==1){ return true; } Map<Character, Integer> map = new HashMap<>(); for(int i=0;i<order.length();i++){ map.put(order.charAt(i), i); } for(int i=1;i<words.length;i++){ int index = 0; boolean indexOut = false; if(index>words[i-1].length()-1){ continue; } if(index>words[i].length()-1){ return false; } while(map.get(words[i].charAt(index))==map.get(words[i-1].charAt(index))){ index++; System.out.println(index); if(index>words[i-1].length()-1){ indexOut=true; break; } if(index>words[i].length()-1){ return false; } } if(indexOut){ continue; } if(map.get(words[i].charAt(index))<map.get(words[i-1].charAt(index))){ return false; } } return true; }} ...

January 2, 2019 · 2 min · jiezi

leetcode讲解--884. Uncommon Words from Two Sentences

题目We are given two sentences A and B. (A sentence is a string of space separated words. Each word consists only of lowercase letters.)A word is uncommon if it appears exactly once in one of the sentences, and does not appear in the other sentence.Return a list of all uncommon words. You may return the list in any order.Example 1:Input: A = “this apple is sweet”, B = “this apple is sour"Output: [“sweet”,“sour”]Example 2:Input: A = “apple apple”, B = “banana"Output: [“banana”]Note:0 <= A.length <= 2000 <= B.length <= 200A and B both contain only spaces and lowercase letters.题目地址讲解用hashmap记录每一个单词Java代码class Solution { private Map<String, Integer> map = new HashMap<>(); public String[] uncommonFromSentences(String A, String B) { List<String> result = new ArrayList<>(); countString(A); countString(B); for(String key:map.keySet()){ if(map.get(key)==1){ result.add(key); } } String[] str = new String[result.size()]; return result.toArray(str); } private void countString(String A){ int index=0; for(int i=0;i<A.length();i++){ if(A.charAt(i)==’ ‘){ Integer count = map.get(A.substring(index, i)); if(count==null){ map.put(A.substring(index, i), 1); }else{ map.put(A.substring(index, i), count+1); } if(i+1<A.length()){ index = i+1; } }else if(i==A.length()-1){ Integer count = map.get(A.substring(index)); if(count==null){ map.put(A.substring(index), 1); }else{ map.put(A.substring(index), count+1); } } } }} ...

January 1, 2019 · 1 min · jiezi

字符串匹配算法之KMP模式

这篇文章主要是介绍KMP模式匹配算法,在正式介绍KMP之前我们先看一下普通模式匹配,由普通模式匹配在进一步的推导KMP模式会更容易理解。字符串的普通模式匹配普通模式匹配的原理不进行说明了,简单来说就是两个字符串的每个字符依次进行匹配。public int match(String S,String T){ int i = 0; int j = 0; while(i < S.length() && j < T.length()){ if(S.charAt(i) == T.charAt(j)){ i++; j++; }else{ i = i - j + 1; j = 0; } } if(j >= T.length()){ return i - T.length(); }else{ return 0; }}普通模式匹配的时间复杂度最坏的情况(即T在S的末尾)为O((m-n+1)*n)。这种算法的优点是实现简单,缺点也显而易见那就是效率较低。jdk String类内的静态方法indexOf底层使用的就是类似该种算法。KMP模式匹配算法推导回溯推导为了方便理解KMP模式,我们先看一下普通模式模式的流程:栗子1:主串:S = “abcdefgab"子串:T = “abcdex"观察下普通模式匹配算法,对于要匹配的子串T = “abcdex”来说首字母"a"与后面的串"bcdex"中的任意一个字符都不相等;串"bcde"与主串S的第2-5位相等,那么首字母"a"就不可能与主串S的第2-5位相等。所以,第2 3 4 5 的判断都是不需要的,这是个理解KMP模式的关键。那么问题来了,如果T串后面还含有首字母"a"的字符会怎么样呢?我们在看一个例子:粟子2:主串:S = “abcabcabc"子串:T = “abcabx”对于要比配的子串T = “abcabx"来说,T的首字母"a"与T的第2个字符"b”、第3个字符"c"均不等,且T的第1-3位字符与主串S的第1-3相等,所以,步骤2和步骤3可以省略;T的前2位串"ab"与T的第4-5串"ab"相等,且T的第4-5串"ab"与主串S的第4-5串"ab"相等,得出T的前2位串与S的第4-5也相等,可以省略。最后,第6步的前两次比较也是不需要的,直接比较 主串S的第6位的"a"和子串T的第3位"c"即可,如下图:对比这两个例子,可以发现普通模式主串S的游标每次都是回溯到i++的位置。而经过上面的分析后发现,这种回溯方式其实是不需要的,KMP模式就是解决了这些没有必要的回溯。既然要避免i的回溯,那么我们就要在子串T的游标j上下工夫了。通过观察也可发现,如果T的首字母与自身后面的字符有相等的情况,那j值的变化就会不相同。例子1内j的变化,由于首字母"a"与后面的字符都不相等,则j由6变回了1。例子2内j的变化,由于前缀"ab"与后面的"ab"相等,因此j从6变回了3。有没有看出什么规律?提示一下:例子1内与前缀相同的字符个数为0,j变成了1。例子2内与前缀相同的字符个数为2,j变成了3。j的变化取决也当前字符之前的串的前后缀的相似度。回溯公式根据上一节的推导,我们可以将T串各个位置的j的变化定义为一个数组next,next的长度就是T串的长度,我们可以得到下面的函数定义(为了后面读程序方便理解,所以该函数是遵循数组下标从0开始的规范):我们来手工验证一下该函数。T = “abcdex"j123456模式串Tabcdexnext[j]000000if j = 0, next[0] = 0;if j = 1, ‘p0 … pk’ = ‘pj-k … pj’ –> ‘a’ <> ‘b’ ,next[1] = 0;if j = 2, 子串"abc”,也没有重复的字符子串,next[2] = 0;以后同理,所以最终T串的ntxt[j]为000000。例子就列举到这里,现在放下KMP的Java实现,其它实例大家可以使用程序进行验证。KMP模式匹配算法实现public void getNext(String T,Integer[] next){ next[0] = 0; //当j = 0时,next[0] = 0 int i = 1; int j = 0; int k = 0; while (i < T.length() && j < i){ if(T.substring(0,j).equals(T.substring(i-j,i))){ k = j; //若有相同子串,则记录下对应在T串内的位置,最终得出最长匹配成功的位置 } j++; if(j >= i){ next[i] = k; //若一直未匹配成功,将k = 0赋值给next[i] j = 1; k = 0; i++; } }}我们来测试 几个字符串:input: abcdexoutput:000000input: abcabxoutput:000012input: aaaaaaaaboutput:001234567下面是完整的KMP模式匹配算法的代码:package string;public class KMPMatch { public void getNext(String T,Integer[] next){ next[0] = 0; //当j = 0时,next[0] = 0 int i = 1; int j = 0; int k = 0; while (i < T.length() && j < i){ if(T.substring(0,j).equals(T.substring(i-j,i))){ k = j; //若有相同子串,则记录下对应在T串内的位置,最终得出最长匹配成功的位置 } j++; if(j >= i){ next[i] = k; //若一直未匹配成功,将k = 0赋值给next[i] j = 1; k = 0; i++; } } } public int match(String S,String T){ int i = 0; int j = 0; Integer[] next = new Integer[T.length()]; getNext(T,next); while(i < S.length() && j < T.length()){ if(j == 0 || S.charAt(i) == T.charAt(j)){ i++; j++; }else{ j = next[j]; } } if(j >= T.length()){ return i - T.length(); }else{ return 0; } } public static void main(String[] args) { String S = “aaaabcde”; String T = “abcd”; System.out.println(new KMPMatch().match(S,T)); }}对于方法getNext来说,若T的长度为m,因只涉及简单的单循环,其时间复杂度为O(m),由于i不进行回溯,while循环的时间复杂度为O(n)。因此,整个算法的时间复杂度为O(m+n)。对于KMP模式来说,仅当子串与主串之前存在许多“部分匹配”的时候才体现出它的优势,否则两者差异并不明显。KMP模式匹配算法的优化看下面这个例子:S = “aaaabcde"T = “aaaaax"T的next分别为001234,两个串的匹配流程如下:从流程中可发现,当中的步骤2 3 4 5都是多余的判断。由于T串的第2 3 4 5位置的字符都与首字符相等,那么就可以用首位next[0]值去取代与它相等的字符的next[j],下面就是对getNext方法进行的改良,代码如下:public void getNextVal(String T,Integer[] nextVal){ nextVal[0] = 0; //当j = 0时,next[0] = 0 int i = 1; int j = 0; int k = 0; while (i < T.length() && j < i){ if(T.substring(0,j).equals(T.substring(i-j,i))){ k = j; //若有相同子串,则记录下对应在T串内的位置,最终得出最长匹配成功的位置 } j++; if(j >= i){ //当前字符与前缀字符相同中,则当前字符j为nextVal[i] if(T.charAt(i) == T.charAt(k)){ nextVal[i] = nextVal[k]; }else { nextVal[i] = k; } j = 1; k = 0; i++; } }}学习是一个过程,对学习到的知识进行理解、吸收、整理,并将其按自己的理解进行输出。参考材料:《大话数据结构》 ...

December 20, 2018 · 2 min · jiezi

leetcode讲解--942. DI String Match

DI String MatchGiven a string S that only contains “I” (increase) or “D” (decrease), let N = S.length.Return any permutation A of [0, 1, …, N] such that for all i = 0, …, N-1:If S[i] == “I”, then A[i] < A[i+1]If S[i] == “D”, then A[i] > A[i+1]Example 1:Input: “IDID"Output: [0,4,1,3,2]Example 2:Input: “III"Output: [0,1,2,3]Example 3:Input: “DDI"Output: [3,2,0,1]Note:1 <= S.length <= 10000S only contains characters “I” or “D”.题目地址算法:默认是递增的,然后根据D修改顺序,每遇到一串D,就reverse一段A。java代码class Solution { public int[] diStringMatch(String S) { int[] A = new int[S.length()+1]; for(int i=0;i<=S.length();i++){ A[i] = i; } char[] c = S.toCharArray(); int count=0; for(int i=0;i<c.length;i++){ if(c[i]==‘I’){ continue; } while(i<c.length && c[i]==‘D’){ i++; count++; } reverse(A, i-count, i); count=0; } return A; } private void reverse(int[] A, int begin, int end){ for(int i=0;i<(end-begin+1)/2;i++){ int temp=A[begin+i]; A[begin+i] = A[end-i]; A[end-i] = temp; } }}

December 18, 2018 · 1 min · jiezi