数据结构 - 字符串

[TOC]

简介

串(string)是由零个或多个字符组成的有限序列,又名字符串。

上述描述是对字符串的定义,将其使用数学方法进行描述,一般记为:,其中:

  • 是字符串的名称,用双引号括起来的是字符串的内容(值),注意双引号本身不属于字符串内容。
  • 表示字符串中第 个位置的字符。
  • 表示字符串的长度,当 时,表示字符串内容为空,即 “空串”,可以直接使用两个双引号""表示。

从上述字符串的定义中,还可以看到以下几方面内容:

  • 零个或多个字符组成:说明字符串的内部元素类型为字符。
    :关于字符的更加详细释义,请参考 附录
  • 有限:说明字符串的内容长度是有一定限制的,小于一个最大范围,但是在该范围内,实际的长度是不确定的。
  • 序列:说明字符串中的相邻字符存在前驱和后继的关系。

一些名词释义

  • 空串:表示字符串没有内容,可用两个双引号""进行表示。

  • 空格串:表示只包含一个或多个空格的字符串。注意其与 空串 的区别,空格串 是有内容的,其内容为一个或多个空格。空串 没有内容,且其长度为0

  • 主串:具备全部内容的字符串。

  • 子串:字符串中任意个数的连续字符组成的子串称为该串的子串。

  • 串的比较:两个串的大小比较方法是依次比较相同位置上的字符码值,首次不同时则比较完成。

字符串的存储结构

字符串的逻辑结构和线性表很相似,因此这里可以借助线性表的典型结构,即数组和链表来实现字符串结构:

  • 串的顺序存储结构:即基于数组实现字符串结构。首先分配一个足够大的数组作为字符串的底层数据结构,然后依次将字符存储到数组对应位置上,完成存储。

  • 串的链式存储结构:即基于链表实现字符串结构。由于字符串的数据元素为字符,如果采用链表作为字符串的底层数据结构,则需要为每一个字符创建一个链表结点,链表结点最少还应当具备一个后继指针域,如果要求具备往前回溯功能,则还应当创建一个前驱指针域,相对来说,基于链表的字符串在空间上存在资源使用过大问题。

当然,可以通过其他方式提高串的链式存储结构效率,但是总体来说,使用数组作为字符串的底层结构更加简单直接,性能上也更加优越。
因此,本文主要介绍基于数组的方式实现字符串数据结构。

字符串的基本操作

字符串的基本操作包含如下内容:

// 头文件
#ifndef __STRING_H__
#define __STRING_H__

#include 

#define DEFAULT_LENGTH 100                 // 底层数组默认长度

class String {
private:
    char* pstr = nullptr;                  // 字符串指针
    std::size_t capacity = DEFAULT_LENGTH; // 底层数组最大容量
    std::size_t length = 0;                // 字符串长度
private:
    void init();
    void checkIfNeedEnlarge();             // 长度超过默认长度,则动态扩展
public:
    String();                              // 空串
    String(char c);                        // 一个字符串
    String(const char* str);               // 字符串指针
    String(const String& str);             // 字符串引用
    ~String(); // 析构函数

    // 获取字符串长度
    size_t size() const;
    // 清空
    void clear();
    // 判空
    bool isEmpty() const;

    // 子串开头
    bool startsWith(const char* str) const;
    bool startsWith(const String& str) const;
    // 子串结尾
    bool endsWith(const char* str) const;
    bool endsWith(const String& str) const;

    // 删除字符串前后空白符
    String trim();

    // 获取索引 i 的字符
    char& operator[](std::size_t i);

    // 比较
    bool operator==(const char* str) const;
    bool operator==(const String& str) const;

    bool operator!=(const String& str) const;
    bool operator!=(const char* str) const;

    // 字符串拼接 concat
    String& operator+=(const String& str);
    String& operator+=(const char* str);
    String& operator+=(const char c);
    String operator+(const String& str) const;
    String operator+(const char* str) const;
    String operator+(const char c) const;

    // 支持输出操作
public:
    friend std::ostream& operator<<(std::ostream& os, const String& str);
};

#endif

各个操作对应的具体实现如下所示:

#include "String.h"

void String::init() {
    // 动态开辟一块内存,并初始化为 0
    this->pstr = new char[DEFAULT_LENGTH] {0};
    if (this->pstr == nullptr) {
        throw "No extra memory to create String object!";
    }
}

void String::checkIfNeedEnlarge() {
    if (this->length >= this->capacity) {
        // 开启一块更大的内存
        char* p = new char[this->capacity += DEFAULT_LENGTH];
        // 复制源字符串数组
        std::memcpy(p, this->pstr, this->length);
        // 释放源数组内存
        free(this->pstr);
        // 指向新字符串数组内存空间
        this->pstr = p;
    }
}
 
String::String() {
    init();
}

String::String(char c) {
    init();
    (*this) += c;
}

String::String(const char* str) {
    init();
    (*this) += str;
}

String::String(const String& str) {
    init();
    (*this) += str;
}

String::~String() {
    if (this->pstr) {
        delete[] this->pstr;
    }
}

size_t String::size() const {
    return this->length;
}

void String::clear() {
    std::memset(this->pstr, 0, this->length);
    this->length = 0;
}

bool String::isEmpty() const {
    return this->length == 0;
}

bool String::startsWith(const char* str) const {
    bool ret = (str != nullptr);
    do {
        // str 为空
        if (!ret) {
            ret = false;
            break;
        }
        // 子串长度大于主串,直接返回 false
        size_t len = strlen(str);
        if (len > this->length) {
            ret = false;
            break;
        }
        ret = true;
        for (size_t i = 0; i < len; ++i) {
            if (str[i] != this->pstr[i]) {
                ret = false;
                break;
            }
        }
    } while (false);
    return ret;
}

bool String::startsWith(const String& str) const {
    return this->startsWith(str.pstr);
}

bool String::endsWith(const char* str) const {
    bool ret = (str != nullptr);
    do {
        if (!ret) {
            ret = false;
            break;
        }
        size_t len = strlen(str);
        if (len > this->length) {
            ret = false;
            break;
        }
        ret = true;
        for (size_t i = 0; i < len; ++i) {
            if (str[len - 1 - i] != this->pstr[this->length - 1 - i]) {
                ret = false;
                break;
            }
        }
    } while (false);
    return ret;
}

bool String::endsWith(const String& str) const {
    return this->endsWith(str.pstr);
}

String String::trim() {
    size_t begin = 0;
    size_t end = this->length - 1;

    while (this->pstr[begin] == ' ') {
        ++begin;
    }
    while (this->pstr[end] == ' ') {
        --end;
    }

    String trimStr;
    while (begin <= end) {
        trimStr += this->pstr[begin++];
    }
    return trimStr;
}


char& String::operator[](std::size_t i) {
    return this->pstr[i];
}

bool String::operator==(const char* str) const {
    size_t len = strlen(str);
    if (this->length != len) {
        return false;
    }
    for (size_t i = 0; i < len; ++i) {
        if (str[i] != this->pstr[i]) {
            return false;
        }
    }
    return true;
}

bool String::operator==(const String& str) const {
    if (this->length != str.size()) {
        return false;
    }
    for (size_t i = 0; i < this->length; ++i) {
        if (str.pstr[i] != this->pstr[i]) {
            return false;
        }
    }
    return true;
}

bool String::operator!=(const String& str) const {
    return !(*this == str);
}
bool String::operator!=(const char* str) const {
    return !(*this == str);
}

String& String::operator+=(const String& str) {
    size_t len = str.size();
    // 先增加长度
    this->length += len;
    // 在检查
    this->checkIfNeedEnlarge();
    // 最后复制
    for (size_t i = 0; i < len; ++i) {
        this->pstr[this->length - len + i] = str.pstr[i];
    }
    return *this;
}
String& String::operator+=(const char* str) {
    size_t len = strlen(str);
    for (size_t i = 0, len = strlen(str); i < len; ++i) {
        (*this) += str[i];
    }
    return *this;
}

String& String::operator+=(const char c) {
    // 先检查边界
    this->checkIfNeedEnlarge();
    this->pstr[this->length++] = c;
    return *this;
}

String String::operator+(const String& str) const {
    String newStr(*this);
    return newStr += str;
}

String String::operator+(const char* str) const {
    String newStr(*this);
    return newStr += str;
}

String String::operator+(const char c) const {
    String newStr(*this);
    return newStr += c;
}

std::ostream& operator<<(std::ostream& os, const String& str) {
    os << str.pstr;
    return os;
}

:以上实现更多的在于展现思路,具体代码未经严格测试,可能存在错误,请知悉。

附录

  • 字符:百度百科对字符的定义如下

    字符指类字形单位或符号,包括字母、数字、运算符号、标点符号和其他符号,以及一些功能性符号。字符是电子计算机或无线电通信中字母、数字、符号的统称,其是数据结构中最小的数据存取单位...

    可以简单将字符视作为一种符号,但是这里就存在一种问题,因为计算机只能处理01这样的数值类型数据,所以对于字符这种符号类型数据,计算机不能直接进行处理,因此,这里存在一个转换/映射过程。

    这个映射过程就叫 编码,比如,当计算机采用 ASCII 编码时,大写字符A就会被映射为数值65,而进行打印等操作的时候,计算机会通过查询 ASCII 编码表,就可以将65转换为A进行输出,这样就完成了字符类型的输入输出操作。

    但是,ASCII 码采用 7 位二进制数表示一个字符,因此它最多能表示 128 个字符,这样有一些特殊符号就无法进行表示,后来就采用 8 位二进制数表示一个字符,称为扩展 ASCII 码,它最多能够表示 256 个字符。扩展 ASCII 码对于以英语为母语的国家已经足够使用了,但是对于亚洲等国家,扩展 ASCII 码还远远无法满足这些国家的文字映射。

    最初的一段时间,各个国家为了能让计算机支持显示自己的文字符号,就自定义了一套满足自己国家需求的编码表,比如中国制定的GB2312编码,就可以编码简体中文字符,比如日本的日文编码表Shift_JIS,比如韩国的韩文编码表Euc-kr...

    这种每个国家都自定义一套编码表存在的问题就是编码冲突,比如当不同国家相互通信的时候,就会由于解码方式不同导致乱码产生。因此,国际间需要一套标准编码表,以支持所有国家的文字符号。而这套编码表就是 Unicode。

    Unicode 本身是一种规定,它规定了每个字符对应的数字编码(即码值),但是没有规定这个码值如何存储(简而言之,Unicode 是一种字符集)。比如,对于字符A,Unicode 只规定了它对应的数字编码为65,但是是使用 1 字节进行存储,还是使用 2 字节进行存储,由不同的编码形式决定。Unicode 的编码形式一般称为 UTF-* 编码,常见的有 UTF-8、UTF-16 和 UTF-32。这里需要注意一下,早期的 Unicode 采用两字节编码方案,该方案称为 "Unicode",后来又改名为 UCS-2,但是后来发现 16 位编码无法囊括所有字符,然后引入了一种采用四字节编码的 UCS-4 方案,但是该编码方案太浪费空间了,最终为了平衡 UCS-2 和 UCS-4 两套编码之间的僵局,采用了新的编码方案 UTF-16。所以 UTF-16 其实是一种变长编码,每个字符编码为 16 位或者 32 位,对于码值在65535之内的字符,采用两字节编码,超出的字符则采用四字节编码。

    Unicode 虽然统一了编码字符集,解决了多语言乱码问题,但是它本身在空间利用率上(早期 Unicode 使用两字节方案),稍显不足,比如对于字符A,使用 ASCII 码编码的话,其二进制为01000001,而采用 Unicode 编码,则为00000000 01000001,可以看到,Unicode 对于字符A的编码,其实就是在 ASCII 编码的前面补0,数值完全一样,但是 Unicode 比 ASCII 编码多占用了一倍的存储空间,存在资源浪费问题,并且对于文本的存储与传输会带来较大的性能损耗。同时,除了在空间利用率效率低之外,Unicode 还存在无法识别问题,因为虽然大部分编码采用两个字节,但是有些偏僻字符会采用三字节或者四字节...计算机无法区分当前是两字节编码字符还是三字节编码字符...

    因此,为了解决以上问题,又出现了 UTF-8 编码,它是对 Unicode 的一种优化,将 Unicode 编码转化为可变长编码,即 UTF-8 编码对于一个 Unicode 字符,会根据其数字编码大小编码成 1~6 个字节,常用的英文字母被编码成 1 个字节,汉字通常使用 3 个字节,其他比较偏僻的字符才会被编码为 4~6 个字节,这样,就可以节省空间。同时,UTF-8 会给每个 Unicode 字符编码进行标记,使得计算机通过该标记就知道当前 Unicode 字符占据的字节数,增加容错性。

    综上所述,Unicode 是一种包含了全世界所有字符的编码集,而 UTF-* 是对 Unicode 的具体实现,其中,UTF-8 是当前使用最多的 Unicode 实现。
    在计算机内存中,统一使用 Unicode 编码,当需要保存到磁盘或者进行传输的时候,就转换为 UTF-8 编码。比如,对于以 UTF-8 保存的文本文件,程序进行读取时,指定编码格式为 UTF-8,这样就能正确识别文本字符对应的字节数并将其转换为相应的 Unicode 码值,存储到内存中。当保存数据时,程序将内存中的字符码值转换为 UTF-8 的编码格式,即将一个数值以 UTF-8 的编码格式转换为另一个数值(二进制表示),存储到文本文件中即可。

参考

  • 《大话数据结构》
  • 数据结构--字符串类String(一)
  • 廖雪峰 - 字符串和编码
  • 字符串,那些你不知道的事

你可能感兴趣的:(数据结构 - 字符串)