数据结构:数组与字符串(C++)

虽然笔者在去年修读过了数据结构的课程,但到现在却发现自己无论是理解深度还是使用场景都还只是一知半解。是时候重新彻底学习数据结构与算法这些大学基础知识了。


数据结构与算法基础

数据结构 Data Structure:是计算机中存储、组织数据的方式,旨在高效地访问和修改数据,是数据的逻辑结构和物理结构的结合。

算法 Algorithm:是解决特定问题的一系列明确指令或步骤。它描述了如何通过有限的步骤将输入转换为输出。具备五大特性:输入、输出、确定性、可穷性、可行性。

我们都知道,程序(Procedure)是计算机用于处理特定任务的指令集合。在程序中,数据结构决定了数据的储存和组织方式(程序的基础),算法决定了对数据的操作逻辑(程序的核心),二者相辅相成,构成了程序。

关于数据结构:

1、数据元素(Data Element):是数据结构的基本组成单位,是具有独立意义的最小数据单元。比如一个由学生们组成的集合,一个学生就是一个数据元素。

2、数据项(Data Item):用于描述数据元素的属性。承接上例,学生的学号、名字、年龄都是数据项。

3、操作(Operation):我们熟知的遍历、查找、删除这些功能。(这些操作就属于算法了。)

4、逻辑结构(Logical Structure):是指数据元素之间的相互关系,是抽象的。有以下四种。

    (1)集合结构:数据元素之间为相独立关系。无序,如数学集合。

    (2)线性结构:数据元素为一对一关系。每个元素有且只有一个前序节点和后序节点,如链表,数组,这些一对一关系的结构可以统称为线性表(Linear list)。

    (3)树形结构:数据元素为一对多关系。每个元素有一个父节点,却有多个子节点,如二叉树。

    (4)图形结构:数据元素为多对多关系。每个元素可以有多个前序节点和后序节点,如无向图。

5、物理结构(Physical Structure):是指数据元素在计算机内存中的实际布局和访问方式,故又叫存储结构。有以下两种。

    (1)顺序存储结构:数据元素按连续的物理地址存储在内存中。典型的例子就是数组。

    (2)链式存储结构:数据元素通过指针连接,物理地址并非连续的。比如链表。

数组 Array

  • 特性:是一种线性顺序存储结构,由相同类型的元素组成,其数据元素通过索引访问(即下标,从零开始计数)。(这种访问方式被称为“随机访问”,这个“随机”是指随便、任意的意思,表示可以随意指定访问任意元素。)

    数组在声明时必须定义数组的大小,狭义上的数组一般是指静态数组,在其生命周期中大小不能改变,但是通过编程语言提供的动态数组可以在运行时提供对大小的调整。

  • 常见的操作(普通静态数组,C++为例)

1、访问元素与更改元素(简单不解释)

2、插入元素

for (int i = size; i > pos; i--) {
    arr[i] = arr[i-1];  //插入位置以后的往后移
}
arr[pos] = value;  //插入新元素 时间复杂度O(n)

3、删除元素

for (int i = pos; i < size-1; i++) {
    arr[i] = arr[i+1];  //往前移动,覆盖元素
}//时间复杂度O(n)

4、查找元素

for (int i = pos; i < size; i++){
    if(arr[i] == target){
        return i;  //返回其索引
    }    
}//时间复杂度O(n)
  • 优点:随机访问,时间复杂度为O(1),在内存中连续分配,可以提高缓存命中率,加快访问速度。

  • 缺点:数组大小在声明时确定,容易导致内存浪费。其需要一次性分配大块连续内存,在内存紧张或碎片化严重时,会导致分配失败。

  • C++中的数组们:

    1、静态数组

int arr[5] = {1, 2, 3, 4, 5};
int arr[5];  //没给元素赋值必须指定大小
int arr[] = {1, 2, 3, 4, 5};  //没有指定大小必须给元素赋值

可以通过 sizeof(arr) / sizeof(arr[0]) 计算得出数组的大小。数组名为首地址,即arr <==> &arr[0]。arr+1或&arr[0]+1会跳过第1个元素(加上1个数组元素的字节数),指向数组的下1个元素。而&arr+1会跳过整个数组(加上整个数组的总字节数)。

int *p = (int *)(&arr + 1); //指针指向数组的末尾

只要记住如果指针类型是 T*,则 p+1 会跳过 sizeof(T) 字节就行了。

    2、动态数组

//C语言风格
int* arr = (int*)malloc(initialSize * sizeof(int));//初始化内存
int* temp = (int*)realloc(arr, newSize * sizeof(int));//重新分配内存
arr = temp;//注意这个temp指针是arr内存的扩展,和arr的指向相同
for (int i = initialSize; i < newSize; i++) {
    arr[i] = i + 1;//初始化新增的元素
}
free(arr);
//C++风格
int* arr = new int[initialSize];//初始化内存
int* temp = new int[newSize];//分配新内存
//这个temp指针是与arr无关的另一处地址,需要完全复制arr的值
for (int i = 0; i < initialSize; i++) {
    temp[i] = arr[i];//复制原来就有的
}
for (int i = initialSize; i < newSize; i++) {
    temp[i] = i + 1;//初始化新增的
}
delete[] arr;//先清空内存
arr = temp;
delete[] arr;

这样的动态数组时间复杂度很高,还很麻烦,得程序员手动更改分配内存的大小。但至少与静态数组不同,可以在运行时改变数组的大小。

    3、STL数组array

#include <array>
//基本和普通的数组没区别
std::array<int, 3> arr1;
std::array<int, 3> arr2 = { 1, 2, 3 };
std::array<int, 3> arr3{ 4, 5, 6 };// 直接初始化
std::array<int, 5> arr4 = { 7, 8 };//未被初始化的默认为0
//可以为全元素赋一样的值
std::array<int, 5> arr5;
arr5.fill(10);

这个数组在原来普通的数组之上添加的安全检查,可以通过其at()方法进行安全访问。同时也加入了迭代器(有点类似于STL容器专用的指针),性能更高。具体的其他方法可以参考官方文档,有点类似于vector。

    4、STL动态数组vector

#include <vector>
vector<int> vec1;
vector<int> vec2 = { 1, 2, 3 };// C++11起可省略等号,其他类似的容器都是这样
vector<int> vec3{ 4, 5, 6 };
vector<int> vec4(5, 10);//元素个数为5,并指定所有元素为同一个值10
vector<std::vector<int>> matrix = { //二维动态数组
    {1, 2, 3},
    {4, 5, 6}
};
// 从数组、另一个vector、甚至是字符串初始化
int arr[] = { 7, 8, 9 };
string str = "I Love Keqing";
vector<int> vec5(arr, arr + 3);//这时,两个参数都是指针,用来确定范围
vector<int> vec6(vec2.begin(), vec2.end());//也可以是迭代器
vector<char> vec7(str.begin(), str.end());

vector是C++中最灵活高效的容器之一,具备随机访问和动态内存的管理。能够自动管理内存,根据需要动态调整大小(通常以 2 倍当前容量的标准进行扩容),提供迭代器、元素访问、容量管理、修改操作等多种方法。

  • 二分查找法:

适用于有序数组的查找。

int binarySearch(vector<int> arr, int value) {
    int left = 0;
    int right = arr.size()-1;
    while (left <= right) {
        int mid = left + (right - left) / 2;//更新目标值所在范围,将mid设置在这个范围中间
        if (arr[mid] == value) {
            return mid;
        }
        if (arr[mid] < value) {
            left = mid + 1;
        }
        else {
            right = mid - 1;
        }
    }
    return -1;
}

字符串 String

  • 特性:字符串的本质就是一个字符数组,所以数组的操作也适用于字符串。但C语言标准库(<string.h>)提供了一系列函数操作字符串,如strlen、strcpy、strcmp、strcat、strstr。其以“\0”作为结束标志,使得字符串的长度可以动态变化。

//这是在C语言中
//必须手动添加\0
char str1[6] = {'h', 'e', 'l', 'l', 'o', '\0'};
// 自动添加\0
char str2[6] = "hello";
// 不指定大小可以直接计算长度,加上1
char str3[] = "world";
  • C++中的字符串:

相比于C风格的字符串,C++标准库中的string类能够自动管理内存,更安全方便。

#include <string>
string s1;//空的
string s2("Hello");
string s3 = "World";//等号赋值初始化
string s4(5, 'A');//将5个字符都赋值为'A'
string s5(s2);//直接复制
string s6 = s2 + s3;//直接连接
string vec2 = { "I love Keqing" };// C++11起,和vector一样
string vec3{ "I love Keqing" };

关于字符串的遍历,可以采用范围for循环的方法。范围for循环可以用于任何提供begin()和end()成员函数或自由函数的容器。

for(char c : str) {}//类型 名称 : 容器名

关于string大部分的操作和vector有些相似,这里介绍特殊且常见的几个。

str.append(" World");//追加字符串
size_t pos = text.find("world");// 返回该子串首次出现位置
int result = a.compare(b);//a<b返回负,a==b返回0,a>b返回正
bool cmp3 = a.starts_with("app");//C++20加入比较前后缀
bool cmp4 = b.ends_with("ana");
//字符串数值转换
string numStr = "123.45";
int i = stoi(numStr);//转int
long l = stol(numStr);//转long
float f = stof(numStr);//转float
double d = stod(numStr);//转double
int val = 42;
string s = to_string(val);//转string
//获取子串
string str = "Hello World";
string sub = str.substr(6, 5);//从位置6开始数5个字符

小结

总结一下,字符串的东西看起来还真不多,主要是C++STL库里的函数有些过于丰富了,可能还是单独消化比较好。累了累了,我去歇着了~

如有补充纠正欢迎留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值