数据结构:字符串(个体转换整体的字符容器)

1、定义

由n个字符(n >= 0)组成的有序整体。字符串使用两个双引号""表示,双引号之间为字符串的值。字符串本身可以看作是字符个体转换成整体的字符容器。

1.1、空字符串

不含任何字符的字符串。例如"";

1.2、空格串

只包含空格的字符串。例如" ";

1.3、主字符串与子字符串。

字符串(主字符串)中任意连续字符组成的字符串(子字符串)。

1.4、字符串相等

即使两个字符串包含的字符完全一致,这两个字符串也不一定相等;只有两个字符串的值完全相同,这两个字符串才相等。

2、存储方式

2.1、顺序存储

通过字符数组实现。适合存储固定长度的字符串,利用数组索引搜索字符的效率高。

2.2、链式存储

通过线性表实现。适合存储不定长度的字符串,字符串增删效率高;如果字符串的数据量很大,那么线性表中每个结点有必要存放多个字符,否则1个字符加1个指针的结点模式很浪费内存,但是1个结点存放多个字符的模式会产生另一个问题,如果字符串频繁进行增删操作,则需要判断、合并、或拆分每个结点中的多个字符,非常麻烦。所以链式存储的字符串很不灵活,性能也差,不建议使用。

3、基本操作(顺序存储的字符串)

3.1、新增

3.1.1、在字符串开头或中间新增字符串
相当于在字符数组开头或中间若干添加字符,需要向后移动添加处之后字符的位置,时间复杂度是O(n)。
3.1.2、在字符串末尾新增字符串(字符串的连接)
相当于在字符数组末尾之后添加若干字符,不需要移动其它字符的位置,时间复杂度是O(1)。

3.2、删除

3.2.1、在字符串开头或中间删除字符串
相当于在字符数组开头或中间删除若干字符,需要向前移动删除处之后字符的位置,时间复杂度是O(n)。
3.2.2、在字符串末尾删除字符串
相当于在字符数组末尾删除若干字符,不需要移动其它字符的位置,时间复杂度是O(1)。

3.3、查找(匹配)

假设原字符串str1(以下简称str1)有n个字符,指定字符串str2(以下简称str2)有m个字符,现要在str1中查找str2,查找的步骤为:
3.3.1、比较n与m的大小。
如果n>=m,则继续下一步;否则str1中未匹配到str2。时间复杂度是O(1)。
3.3.2、判断str2的首字符是否在str1中出现。
如果str2的首字符在str1中出现,则记录第一次出现的位置(索引),继续下一步;否则str1中未匹配到str2。遍历str1查找,时间复杂度是O(n)。
3.3.3、从上一步记录的索引开始,逐个读取str1与str2中对应索引的字符并进行比较,如果每次比较的两个字符都相同,并且一共比较m次,则str1中查找到str2;否则本轮比较str1中未匹配到str2,重新进行上一步,再查找str2首字符在str1中下一次出现的位置,继续之后的步骤。遍历str1的同时,遍历str2,时间复杂度是O(mn)。

4、Java代码描述顺序存储字符串及相关例题。

package com.zy.demo.util;

import java.util.Arrays;

/**
 * 字符串
 * @author zy
 */
public class StringUtil {

    //字符数组
    private char[] chars;

    //字符串长度
    private int size;

    //字符数组初始化默认值
    private static final char[] DEFAULT_CHARS = {};

    //字符数组默认长度
    private static final int DEFAULT_SIZE = 10;

    /**
     * 无参构造方法
     */
    public StringUtil(){
        this.chars = DEFAULT_CHARS;
    }

    /**
     * 指定初始容量的构造方法
     * @param capacity 初始容量
     */
    public StringUtil(int capacity){
        if(capacity == 0){
            this.chars = DEFAULT_CHARS;
        }else if(capacity > 0){
            this.chars = new char[capacity];
        }else{
            throw new IllegalArgumentException("Illegal capacity!");
        }
    }

    /**
     * 指定初始字符串的构造方法
     * @param chars 初始字符串
     */
    public StringUtil(char[] chars){
        this.chars = DEFAULT_CHARS;
        this.addLast(chars);
    }

    /**
     * 获取字符串的长度
     * @return 字符数组的元素个数
     */
    public int size(){
        return this.size;
    }

    /**
     * 打印字符串
     */
    public String toString() {
        if(this.size == 0){
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for(int i = 0 ; i < this.size ; i++){
            sb.append(chars[i]);
        }
        return sb.toString();
    }

    /**
     * 字符数组扩容
     * 时间复杂度:可能是O(n)  --底层是JVM本地方法实现的。
     * 空间复杂度:O(n)
     */
    private void expand(int newLen){
        if(newLen <= DEFAULT_SIZE){
            this.chars = Arrays.copyOf(this.chars,DEFAULT_SIZE);
        }else{
            newLen = newLen * 3/2;
            this.chars = Arrays.copyOf(this.chars, newLen);
        }
    }

    /**
     * 字符串连接
     * 时间复杂度:如果不考虑扩容的情况,则是O(n)。
     * 空间复杂度:如果不需要扩容,则是O(1);否则是O(n)。
     * @param chars 要连接的字符数组
     * @return 连接后的新字符串
     */
    public StringUtil addLast(char[] chars){
        //校验入参字符串
        if(chars == null || chars.length == 0){
            return this;
        }
        //连接后新字符串长度
        int newLen = chars.length + this.size;
        //如果连接后的字符串长度大于字符数组长度,则扩容
        if(newLen > this.chars.length){
            this.expand(newLen);
        }
        //连接字符串
        for(int i = 0 ; i < chars.length ; i++){
            this.chars[this.size++] = chars[i];
        }
        //添加完成后重新初始化数组
        this.chars = Arrays.copyOf(this.chars,this.size);
        return this;
    }

    /**
     * 在指定索引处添加指定字符串
     * 时间复杂度:如不考虑数组扩容则是O(n)  --数组中间添加元素需要移动其它元素的位置。
     * 空间复杂度:如不考虑扩容则是O(1)
     * @param index 索引。规定起始索引为0。
     * @param chars 要添加的字符数组
     * @return 添加成功则返回true;否则返回false。
     */
    public StringUtil add(int index,char[] chars){
        //校验入参索引
        if(index < 0 || index >= this.size){
            return this;
        }
        //校验入参字符串
        if(chars == null){
            return this;
        }
        //空字符串处理
        if(chars.length == 0){
            return this;
        }
        //获取添加的子字符串长度
        int addLen = chars.length;
        //获取添加后的主字符串长度
        int newLen = this.size + addLen;
        //判断是否扩容
        if(newLen > this.chars.length){
            this.expand(newLen);
        }
        //提前把原数组指定索引处之后的元素向后移动,为添加的字符串腾出空间。
        for(int i = this.size-1 ; i >= index ; i--){
            this.chars[i+addLen] = this.chars[i];
        }
        //添加指定字符串
        for(int i = 0 ; i < addLen ; i++){
            this.chars[index+i] = chars[i];
            this.size++;
        }
        //添加完成后重新初始化数组
        this.chars = Arrays.copyOf(this.chars,this.size);
        return this;
    }

    /**
     * 判断原字符串中是否包含指定字符串(字符串查找)
     * 时间复杂度:O(mn) --两层嵌套for循环,其中第二层for循环遍历指定字符串的长度m。
     * 空间复杂度:O(n)  --创建Integer数组存储结果,如果原字符串中的字符不是完全一样的,则此复杂度很小,可以看作O(1)。
     * @param chars 指定字符串
     * @return 如果存在返回true;否则返回false。
     */
    public boolean contains(char[] chars){
        if(chars == null || chars.length == 0){
            return false;
        }
        if(chars.length > this.size){
            return false;
        }
        int[] ints = this.getIndexes(chars);
        if(ints != null && ints.length > 0){
            return true;
        }
        return false;
    }

    /**
     * 判断原字符串中是否包含指定字符串,并返回出现的索引列表。
     * 时间复杂度:O(mn) --两层嵌套for循环,其中第二层for循环遍历指定字符串的长度m。
     * 空间复杂度:O(n)  --创建Integer数组存储结果,如果原字符串中的字符不是完全一样的,则此复杂度很小,可以看作O(1)。
     * @param chars 指定字符串
     * @return 如果存在返回指定字符串首字符在原字符串出现的位置;否则返回空数组。
     */
    private int[] getIndexes(char[] chars){
        //初始化数组记录结果
        int[] ints = {};
        //结果数组索引
        int index = 0;
        //结果数组长度
        int len = 0 ;
        //遍历原字符串
        for(int i = 0 ; i < this.size ; i++){
            //如果在原字符串中查找到指定字符串的首字符
            if(this.chars[i] == chars[0]){
                //首字符后的元素个数不足时,直接退出。
                if(this.size - i < chars.length){
                    break;
                }
                //计数器
                int count = 0;
                //遍历指定字符串
                for(int j = 0 ; j < chars.length ; j++){
                    //逐个比较两个字符串对应位置的字符
                    if(this.chars[i+j] == chars[j]){
                        count++;
                    }
                }
                //如果字符相等个数等于指定字符串的长度,则证明原字符串包含指定字符串。
                if(count == chars.length){
                    //索引大于数组长度,则扩容
                    if(index >= ints.length){
                        int newLen = ints.length == 0 ? 10 : ints.length * 3/2;
                        ints = Arrays.copyOf(ints,newLen);
                    }
                    ints[index++] = i;
                    len++;
                }
            }
        }
        //重新初始化数组
        ints = Arrays.copyOf(ints,len);
        return ints;
    }

    /**
     * 将原字符串中指定字符串替换为新的字符串(字符串删除)
     * 时间复杂度:如果不考虑扩容是O(mn)
     * 空间复杂度:O(n)  --创建数组存储结果
     * @param newChars 要替换的新字符串
     * @param oldChars 要替换的旧字符串
     * @return 替换成功返回替换后的新字符串;否则返回原字符串。
     */
    public StringUtil replace(char[] oldChars,char[] newChars){
        //原字符串为空不处理
        if(this.size == 0){
            return this;
        }
        //要替换的旧字符串为空或者长度大于原字符串不处理
        if(oldChars == null || oldChars.length == 0 || oldChars.length > this.size){
            return this;
        }
        //要替换的新字符串为空不处理
        if(newChars == null){
            return this;
        }
        //查询要替换的旧字符串是否存在,并获取首字符索引列表。
        int[] ints = this.getIndexes(oldChars);
        //未匹配到指定字符串直接返回原字符串。
        if(ints == null || ints.length == 0){
            return this;
        }
        //如果要替换的新字符串是空数组,则为删除操作
        if(newChars.length == 0){
            //遍历存储首字符索引的列表
            for(int i = 0 ; i < ints.length ; i++){
                //删除处后元素索引
                int afterIndex = 0;
                //先把删除处后的元素向数组头移动
                for(int j = ints[i] ; j < this.size ; j++){
                    //数组越界校验
                    afterIndex = j + oldChars.length;
                    if(afterIndex >= this.size){
                        break;
                    }
                    //删除操作索引向前移动
                    this.chars[j] = this.chars[afterIndex];
                }
                //数组长度变更(匹配1次删除oldChars个元素)
                this.size = this.size - oldChars.length;
                //重新初始化数组
                this.chars = Arrays.copyOf(this.chars,this.size);
                //删除成功后同时移动索引列表剩余索引
                for(int k = i+1 ; k < ints.length ; k++){
                    ints[k] = ints[k] - oldChars.length;
                }
            }
        }else{
            //否则是字符串替换操作,有三种情况:
            //1、替换+删除
            if(oldChars.length > newChars.length){
                //删除元素个数
                int delNum = oldChars.length - newChars.length;
                //遍历首字符索引列表
                for(int i = 0 ; i < ints.length ; i++){
                    //起始索引
                    int index = ints[i];
                    //遍历要替换的字符串
                    for(int j = 0 ; j < newChars.length ; j++){
                        //替换
                        this.chars[index++] = newChars[j];
                    }
                    //删除元素前先移动其它元素索引
                    for(int k = index ; k < this.size ; k++){
                        //数组越界校验
                        if(k+delNum >= this.size){
                            break;
                        }
                        this.chars[k] = this.chars[k+delNum];
                    }
                    //数组长度变更
                    this.size = this.size - delNum;
                    //删除后重新初始化数组
                    this.chars = Arrays.copyOf(this.chars,this.size);
                    //删除成功后同时移动索引列表剩余索引
                    for(int m = i+1 ; m < ints.length ; m++){
                        ints[m] = ints[m] - delNum;
                    }
                }
            }else if(oldChars.length < newChars.length){
                //2、替换+添加
                //添加元素个数
                int addNum = newChars.length - oldChars.length;
                //遍历要替换的索引列表
                for(int i = 0 ; i < ints.length ; i++){
                    //起始索引
                    int index = ints[i];
                    //添加的起始索引
                    int jIndex = 0;
                    //替换
                    for(int j = 0 ; j < oldChars.length ; j++){
                        this.chars[index++] = newChars[j];
                        jIndex = j;
                    }
                    //替换完成后的下一个索引为添加的起始索引
                    jIndex++;
                    //判断是否需要扩容
                    if(this.size + addNum > this.chars.length){
                        this.expand(this.size+addNum);
                    }
                    //添加前移动其它元素位置
                    for(int k = this.size-1 ; k >= index ; k--){
                        this.chars[k+addNum] = this.chars[k];
                    }
                    //添加
                    for(int j = jIndex ; j < newChars.length ; j++){
                        this.chars[index++] = newChars[j];
                    }
                    //数组长度变更
                    this.size = this.size + addNum;
                    //添加成功后重新初始化数组
                    this.chars = Arrays.copyOf(this.chars,this.size);
                    //索引列表位置移动
                    for(int m = i+1 ; m < ints.length ; m++){
                        ints[m] = ints[m] + addNum;
                    }
                }
            }else{
                //3、完全替换
                //遍历存储首字符索引的列表
                for(int i = 0 ; i < ints.length ; i++){
                    //起始索引
                    int index = ints[i];
                    //逐个替换字符
                    for(int j = 0 ; j < newChars.length ; j++){
                        this.chars[index++] = newChars[j];
                    }
                }
            }
        }
        return this;
    }

    /**
     * 指定两个字符串,查找它们的最大公共字符串。
     * 时间复杂度:O(m²n)  --三层嵌套for循环,原字符串的长度是n,指定字符串的长度是m
     * 空间复杂度:O(n)  --新建数组对象存储结果
     * @return 最大公共字符串
     */
    public StringUtil findMaxCommon(char[] charsA,char[] charsB){
        if(charsA == null || charsA.length == 0 || charsB == null || charsB.length == 0){
            return null;
        }
        //最大公共字符串
        this.chars = null;
        //最大公共字符串元素个数
        int maxNum = 0;
        //遍历字符串A
        for(int i = 0 ; i < charsA.length ; i++){
            //遍历字符串B
            for(int j = 0 ; j < charsB.length ; j++){
                //如果字符串A当前字符等于字符串B的首个字符
                if(charsA[i] == charsB[j]){
                    //获取字符串A的当前索引
                    int index = i;
                    //计数器
                    int count = 0;
                    //缓存
                    char[] tmp = new char[Math.min(charsA.length,charsB.length)];
                    //遍历字符串B
                    for(int k = j ; k < charsB.length ; k++){
                        if(index >= charsA.length){
                            break;
                        }
                        //逐个比较两个字符串对应位置的字符
                        if(charsA[index++] == charsB[k]){
                            //记录结果
                            tmp[count++] = charsB[k];
                        }else{
                            break;
                        }
                    }
                    //赋值
                    if(count > maxNum){
                        maxNum = count;
                        this.chars = tmp;
                    }
                }
            }
        }
        //重新初始化结果数组
        if(maxNum > 0){
            this.size = maxNum;
            this.chars = Arrays.copyOf(this.chars,this.size);
        }
        return this;
    }

    /**
     * 字符串翻转
     * 时间复杂度:O(n/2)=O(n)  --遍历字符串
     * 空间复杂度:O(1)  --使用有限的内存资源
     * @return 翻转后的字符串
     */
    public StringUtil reverse(){
        if(this.size == 0){
            return this;
        }
        //遍历字符串
        for(int i = 0 ; i < this.size/2 ; i++){
            //缓存当前字符
            char tmp = this.chars[i];
            //首尾交换字符
            this.chars[i] = this.chars[this.size-i-1];
            this.chars[this.size-i-1] = tmp;
        }
        return this;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355