数据结构与算法 —— 04 串

2017/06/21

1.串(String)

㈠'逻辑结构':
串是由零个或多个字符组成的有限序列,又名字符串
s = "a1a2...an"; (n>=0)

注意:串中字符的位置是从1开始的,

空串:零个字符的串,即值为:null, 其长度为 0
空格串:是只包含空格的串,是有内容长度的。这与空串不同,值为:""(长度为0), " "(长度为1), " "(长度为3),等

(1)串的比较

通过组成串的字符之间的编码(即字符在对应字符集中的序号)来进行的。

计算机常用的字符集:
ASCII:早前用7位二进制数表示,总共表示128个字符, 后来扩展为8位二进制表示,总共表示256个字符(英语语言基本满足,其他语言可能不够)

Unicode:采用16位二进制表示,总共可以表示 2^16 个字符,65536个字符。
注意:为了兼容ASCII,其前256个字符和ASCII完全相同。

(2)串的抽象数据类型

串的逻辑结构和线性表很像,只是串的元素针对的是字符集,它们是一个个字符而已。
况且串更多的是看成一个整体的,如:"123",这是整体

线性表则更关注的是对单个元素的操作,比如查找一个元素,插入一个元素等
串中更多的是针对子串,比如查找 子串的位置,得到某个位置的子串等

基本操作:

① 串复制:将某个串复制给当前串
② 判空:若当前串为空,则返回true,否则返回false
③ 串比较:
④ 求串长:返回当前串的字符个数
⑤ 串连接:将s1和s2连接成一个新串,赋值给串T(即生成新的串)
⑥ 求子串:返回当前串中的第i个字符开始的长度为k的子串
⑦ 子串定位:返回子串在主串中首次出现的位置
⑧ 串替换:用子串x替换当前串中的子串y
⑨ 插入子串:将子串插入到当前串中的某个位置
⑩ 删除子串:从当前串中删除指定的子串
⑾ 大写转小写:
⑿ 小写转大写:
⒀ 串压缩:删除串中首尾处的空格
㈡'物理存储结构'

(1)串的顺序存储
与线性表一样,串的顺序存储结构是采用一组地址连续的存储单元来存储串中的字符序列,一般用数组来实现的

注意:这里采用的数组来实现串的顺序存储时,要注意可能是采用定长数组。这会在进行串连接、新串插入、替换等造成上溢截断,从而使数据不完整。因此,为了解决这个问题,使数组的长度在程序执行过程中被动态分配。

典型的例子,如计算机中的"堆",是自由存储区,可由C语言的动态分配函数malloc(), free() 来管理。

表示串的长度有三种常见的方式:

  1. 用一个变量来表示串的长度
  2. 在串尾存储一个不会在串中出现的特殊字符作为串的终结符。例如'\0'
  3. 用数组的0号单元存放串的长度,串值从1号单元开始存放

'代码实现'

public class MyString {
    private int maxSize = 10; //串中字符数组的初始长度
    private char[] chars; //存储字符的数组
    private int lenght; //记录串的长度的变量,即上面讲的第一种方式
    //1.初始化串: 采用默认长度
    public MyString() {
        this.chars = new String[maxSize];
        this.length = 0;
    }

    public MyString(int n) {
        this.maxSize = n;
        this.chars = new String[maxSize];
        this.length = 0;
    }

    //2.将串t复制给当前串,即调用该方法的字符串
    public void copy(MyString t) {
    
    }
    //3.判空
    public boolean isEmpty() {
        return length == 0;
    }

    //4.串的比较
    public int compare(MyString t) {
    
    }

    //5.求串的长度
    public int getLength() {
        return length;
    }

    //6. 清空当前串
    public boolean clear() {}

    //7.将指定串t连接到当前串的尾部
    public void concat(MyString t) {}

    //8.获得当前串的子串
    public MyString subString(int pos, int len) {}

    //
    public MyString subString(int pos) {}

    //9.返回子串t在当前串中首次出现的位置
    public int index(Mystring t) {}

    //10.返回子串t在当前串中最后一次出现的位置
    public int lastIndext(MyString t) {}

    //11.替换,用v代替所有与t相同的子串
    public int replace(MyString t, MyString v) {}

    //12.插入子串
    public boolean insert(MyString s, int pos) {}

    //13.删除子串
    public boolean delete(int pos, int len) {}

    //删除
    public int remove(MyString s) {}
    //14.转换为大写
    public void toUpperCase() {}

    //15.转换为小写
    public void toLowerCase() {}
}

(2)串的链式存储结构
与线性表的链式结构表示相似,但由于串结构的每个元素数据是一个字符,如果此时采用在链表的每个结点处存储一个串的元素,就会造成大量的浪费(因为,每个结点的数据域对多个存储单元)

        ┌──┬──┬──┬──┬─┐    ┌──┬──┬──┬──┬─┐    ┌──┬──┬──┬──┬─┐
    ——> │ A│ B│ C│ D│-│——> │ E│ F│ G│ H│-│——> │I │J │ #│ #│^│  
        └──┴──┴──┴──┴─┘    └──┴──┴──┴──┴─┘    └──┴──┴──┴──┴─┘

因此,可以考虑在一个结点处存放多个字符(如上图),最后一个结点若是未被占满,就用#,^等非串值字符补全

注意:串的链式存储结构在连接串与串的操作中有一定方便外,其他的一些操作灵活性不如顺序存储结构

㈢ '模式匹配'

在当前串中寻找某个子串的过程称为模式匹配,其中该子串称为模式串。如果成功,则返回子串在当前串中的首次出现的存储位置(或序号),否则匹配失败。

模式匹配的算法:
1)朴素的模式匹配算法(简单的意思)
基本思想:

第 1 趟:首先将子串的第1个字符和主串的第1个字符比较,若相等,再比
较子串的第2个字符和主串的第2个字符的,
    若相等再比较下一个, ....
    若不相等,则进行下一趟的比较。
        第 2 趟:将子串的第1个字符和主串的第2个字符比较....
        ...
        第 i 趟:将子串的第1个字符和主串的第 i 个字符比较...
        直到匹配成功或失败。

给该类算法取个名称:Brute(野蛮,粗鲁) Force, BF

此类算法低效,最坏情况:O((n-m+1)*m)
平均情况:O(n+m)

'代码实现'

/**
* @param s 主串
* @param t 子串
* @param pos 起始位置
* @return 返回子串在主串中首次出现的位置
* 注意:实际上字符的位置是从1开始,这里为了使用toCharArray()方便,将字符串转换为
*       字符数组,所以,字符的位置便从0开始的
*/
public static int index(String s, String t, int pos) {
    //数据验证
    if(pos < 0) {
        System.out.println("输入的pos参数不合理,应该是大于等于0的数");
        return -1;
    } else if(pos >= (s.length() - t.length() + 1)){
        System.out.println("输入的pos参数不合理");
        return -1;
    }
    char[] chs = s.toCharArray();
    char[] cht = t.toCharArray();
    //完成正常的匹配任务
    int i = pos; //控制大循环,即针对主串的
    int j = 0; //针对子串的
    while (i < (s.length() - t.length() + 1) && j < t.length()) {
        if (chs[i] == cht[j]) {
            //部分匹配成功
            i ++;
            j ++;
        }else {
            //匹配不成功,切换到主串的的下一个地方
            //核心部分,仔细体会
            i = i - j + 1; //返回到主串的这次起始位置的下一个位置
            j = 0; //返回子串的起始位置(即开头处)
        }
    }

    if (j == t.length()) {
        return i-t.length() + 1; //返回子串的位置
    }else {
        return -1;
    }
}

为了避免重复遍历,研究除了下面的算法:KMP算法,克努特-莫里斯-普拉特算法

2) KMP算法

核心思想:就是避免不必要的回溯(即重复遍历),那么什么是不必要的回溯,主要是由模式串来决定的

next数组:
前缀是固定的,后缀是相对的

'代码描述':

/**
 * KMP算法:核心是求取next数组,其大体框架同朴素算法
 * 如果将数组的第一个存储单元拿来存储数组长度,则其变成就会简单一些,不过本质
 * 都是一样的
 * 注意:KMP的优化部分
 * @author Administrator
 */
public class KMP {

    /**
     * 返回子串t在主串s中从pos之后首次出现的位置
     * 
     * @param s
     * @param t
     * @param pos
     * @return
     */
    public int kmpIndex(String s, String t, int pos) {
        
        // 数据验证
        if (pos < 0) {
            System.out.println("输入的pos参数不合理,应该是大于等于0的数");
            return -1;
        } else if (pos >= (s.length() - t.length() + 1)) {
            System.out.println("输入的pos参数不合理");
            return -1;
        }
        
        char[] chrs = s.toCharArray();
        char[] chrt = t.toCharArray();
        int i = pos; //控制主串,从 pos 之后开始检索
        int j = 0; //控制子串,从子串的头部开始检索
        //获取next数组
        int[] next = getNext(t);
        while(i < chrs.length && j < chrt.length) {
            //注意:相对于朴素算法,这里要注意j的调整,有可能j变为-1,说明j指
            // 到子串的外面去了。所以,当 j = -1 时需要修正
            if(-1 == j || chrs[i] == chrt[j]) {
                i++;
                j++;
            } else {
                //i = i - j + 1; //在这里i就不用回溯了
                j = next[j] - 1;                
            }           
        }
        
        if(j == chrt.length) {  
            return i - chrt.length + 1;     
        }else {         
            return -1;
        }       
    }

    /**
     * 计算next数组
     * 字符的前后缀,如果有1个字符相等,则next数组对应的是2,有2个字符相等,就对应是3,即如果有k个字符相等,则对应的数组是(k+1),
     * @param t 为模式字符串
     */
    private int[] getNext(String t) {
        int[] next = new int[t.length()];

        char[] chrs = t.toCharArray(); // 转换为数组,方便进一步操作
        next[0] = 0;
        // next[1] = 1;
        int i = 0; // 控制后缀,只前进,不后退
        int j = -1; // 控制前缀,自动调节后退的位置,等于-1时,是个特殊位置,要注意
        while (i < t.length() - 1) {

            if (j == -1 || chrs[i] == chrs[j]) {
                i++;
                j++;
                /**
                 * 对next数组的优化部分: 
                 * 这里出现了缺陷,如:aaaaax,还是会造成一些不必要的匹配操作,
                 * 因此。要对next数组的进行调整
                 */
                if(chrs[i] == chrs[j]) {
                    next[i] = next[j];      
                }else {
                    next[i] = j + 1;
                }
                
            } else {
                // 不相等时,j得退回到上一次相等的地方
                j = next[j] - 1;
            }
        }
        return next;
    }

    // 打印数组
    public void getPrint(int[] next) {
        System.out.print("[");
        for (int i = 0; i < next.length; i++) {
            if (i == next.length - 1) {
                System.out.println(next[i] + "]");
            } else {
                System.out.print(next[i] + ", ");
            }
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容