编程语言(3)—数据结构和算法
数据结构(容器)库和算法库是最基础的库之一。字符串是人类语言的记录,是计算机处理的主要对象。
C和C++
由于C语言不提供数据结构和算法库,这里专指C++的STL stand template library。
数据结构
std::array,静态数组,编译时确定数组长度,数组创建在栈。相当于C语言的int[2] = {1,2}
std::vector,动态数组,可动态扩展大小。当 size() 即将超过 capacity() 时,vector 会自动分配更大的内存空间(一般是翻倍),并将原有元素移动或复制到新空间。
因此std::vector会有迭代器失效的问题,需要及时重新获取迭代器。
数组随机访问和更新效率高,为O(1),但随机插入删除低。适合大量访问少量插入删除的场景。vector建议使用.at(x)索引元素,具有边界检查,如果越界会抛出异常。
1 |
|
std::list 是双向链表,能高效插入或删除元素,但随机访问效率低。适合维护不需要大量随机访问和更新的结构(即不需要用vector的都建议上链表)。例如队列,环形buffer等。
1 |
|
std::deque 是双端队列,使用链接起来的若干内存块构成。deque的头部和尾部可以进行高效的插入和删除操作,同时也提供随机访问接口,但随机访问比vector慢。deque实现复杂,实际应该少用。
1 |
|
std::stack是可以传入底层容器的配接器,默认底层容器是std::deque,但可以指定底层容器为std::vector。同样std::queue也是个配接器,默认底层容器是std::deque,但可以指定底层容器为std::list。
1 | std::stack<T> s; // 默认使用 deque |
std::set 是一个有序关联容器,基于红黑树(平衡二叉搜索树)实现,元素会自动排序且不允许重复,CRUD复杂度都是O(logn)。unordered_set 是一个不排序的集合,基于哈希表实现, 复杂度为O(1),但会面临哈希碰撞和rehash的问题。
std::set 创建需要传入comparator (operator <,即Less<T>
升序排列)。而std::unordered_set需要提供hash function和operator==。
1 | // 按字符串长度排序 |
哈希表的元素个数除以哈希表长度是哈希表的负载因子load factor。当负载超过负载因子时,哈希表会自动进行扩容。
- 计算新桶数量:新桶数量通常为不小于 size() / max_load_factor() 的质数。
- 重新分配桶数组:分配新桶数组
- 重新哈希所有元素:将所有元素重新插入到新桶中
- 最后释放旧桶数组
rehash期间hash表会停止工作, rehash完毕后旧的迭代器会失效
数组和unorder_set/unorder_map使用时建议预分配空间(reserve)以减少重新分配。
map类似set,只是元素是键值对pair,根据键进行排序。unordered_map类似unordered_set,是一个无序映射,使用哈希表进行存储。
1 |
|
C++ STL使用迭代器来标识容器中的元素,以及实现容器中元素的访问。迭代器支持重载*
和->
运算符来解引用或访问元素成员变量
- 随机访问迭代器 (Random Access Iterator),类似指针,通过加减算数提供随机访问。对应容器
std::vector、std::deque、std::array
。支持随机访问迭代器作为参数的算法函数,一般也支持指针作为参数。 - 双向迭代器(Bidirectional Iterator),允许在容器中向前和向后移动。对应容器
std::list、std::set、std::map。
迭代器是使用继承结构组织的,随机访问迭代器也是一种双向迭代器
.begin(), .end() 分别代表首尾迭代器
算法库algorithm
std::sort 对容器中的元素进行排序,默认升序。sort可以用std::less和std::greater 进行降序和升序排序。
1 |
|
std::sort 内部实现原理,
- 快速排序(Quicksort):作为主要排序算法,处理大规模数据。
- 堆排序(Heapsort):在快速排序递归深度过大时触发,避免最坏情况下的 O(n²) 时间复杂度。
- 插入排序(Insertion Sort):在小规模子数组(通常 ≤16 个元素)时使用,减少递归开销。
常见排序算法的稳定性
1 | 排序算法 最佳时间复杂度 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 原地排序 备注 |
std::sort是不稳定的,std::stable_sort提供稳定排序,即保证相同元素的相对顺序。内部实现原理是基于 归并排序(Merge Sort) 的变体,并结合优化策略以优化空间。
std::find查找容器中的某个元素,返回第一个匹配元素的迭代器。
1 | auto it = std::find(vec.begin(), vec.end(), 2); |
std::lower_bound,传入排序好的数组,返回第一个大于或等于指定元素的迭代器。
std::upper_bound:返回指向已排序数组中第一个大于指定元素的迭代器。
需要随机访问迭代器
1 | auto it = std::lower_bound(vec.begin(), vec.end(), 3); |
std::reverse, 反转容器中的元素顺序。
1 | std::reverse(vec.begin(), vec.end()); |
std::fill, 将容器中的所有元素设置为指定值。
1 | std::fill(vec.begin(), vec.end(), 0); |
std::transform, 对容器中的每个元素应用指定的操作。
1 | std::transform(vec.begin(), vec.end(), vec.begin(), [](int x) { return x * 2; }); |
std::copy:将容器中的元素复制到另一个容器。std::swap, 交换两个容器的内容。std::move, 移动容器,避免复制。
1 | std::copy(vec, dest); |
std::count, 统计容器中某个元素出现的次数。std::count_if:统计符合特定条件的元素的数量。
1 | int count = std::count(vec.begin(), vec.end(), 2); |
std::accumulate, 对容器中的元素进行累加。
1 | int sum = std::accumulate(vec.begin(), vec.end(), 0); |
std::min_element, 返回容器中最小元素的迭代器。std::max_element:返回容器中最大元素的迭代器。
1 | auto min_it = std::min_element(vec.begin(), vec.end()); |
std::unique, 将容器中相邻的重复元素移动到末尾,返回新逻辑结尾的迭代器,不修改容器大小
unique需要和sort一起连用
1 | std::vector<int> v = {1, 1, 2, 2, 3, 3, 3, 4}; |
字符串处理函数
由于C++兼容C语言,因此支持C风格和C++风格的字符串处理算法。C风格字符串函数参数是指针,C++风格是随机访问迭代器。
C风格的字符串处理函数, 在C++中,需要#include <cstring>
,C语言的字符串处理函数作为std namespace的全局函数。
C 语言字符串处理函数
- strcpy(dest, src), strncpy(dest, src, n), 字符串拷贝
- strcat(dest, src), strncat(dest, src, n), 字符串拼接
- strcmp(s1, s2), strncmp(s1, s2, n), 字符串比较
- strlen(s), 字符串长度
- strchr(s, c), strrchr(s, c), 字符串查找
- memset, memove, memcpy 内存赋值/移动/拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// strcpy(dest, src), strncpy(dest, src, n), 字符串拷贝
char src[] = "Hello, World!";
char dest[20];
std::strcpy(dest, src);
std::strncpy(dest, src, 5);
// strcat(str1, str2) 将str2拼接到str1
std::strcat(str1, str2);
std::strncat(str1, str2, 5);
// strlen(str),返回 C 风格字符串的长度(不包括 \0 终止符)
// strcmp(str1, str2), strncmp(str1, str2, n) 如果str1 > str2, 返回大于0
std::strcmp(str1, str2)
// strchr(str, c),查找字符c在字符串str中第一次出现的位置。如果找到了,返回指向该字符的指针;如果没找到,返回 nullptr。
char* ptr = std::strchr(str, 'o');
// strstr(str, sub), 查找子字符串sub在字符串str中的第一次出现。如果找到了,返回指向子字符串的指针;如果没找到,返回 nullptr。
const char* str = "Hello, World!";
const char* sub = "World";
char* ptr = std::strstr(str, sub);
// memset(str, c, n) 按字节为单位对内存初始化
char str[20];
std::memset(str, '*', 5); // 将前 5 个字符设置为 '*'
memcpy(dest, src, n) 从src拷贝n个字节到dest
char src[] = "Hello";
char dest[10];
std::memcpy(dest, src, 6);
C++ string函数
- append(xxx) 追加字符串
- substr(start, length) 截取字符串, 需要拷贝
- find(xxx) 查找子串
- replace(start, length, xxx) 替换字符串
- erase(start, length) 删除字符串, 是真正的删除(性能差)
- stoi(xxx) 字符串转整数
- std::string, .c_str()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43// size() 或 length() 获取字符串长度
// append(string),将内容追加
std::string str = "Hello";
str.append(", World!");
// push_back(c),添加一个字符
str.push_back('!');
// insert(index, string), 指定位置插入字符串
str.insert(5, ", World");
// erase(index, length), 删除指定位置区间的字符串
str.erase(5, 7); // 从位置 5 开始删除 7 个字符
// replace(index, length, newstring), 替换字符串中的指定部分。
str.replace(7, 5, "C++");
// resize(size) 改变字符串的大小,如果增加字符则填充,减少字符则截断。
str.resize(5);
// std::find(string) 查找子字符串首次出现的位置。如果找不到返回 std::string::npos。rfind, 从字符串末尾向前查找字符串
size_t pos = str.find("World");
// find_first_of(),find_last_of()
size_t pos = str.find_first_of("o");
// substr(index, length) 提取子字符串
std::string sub = str.substr(7, 5);
// compare 字符串比较
std::string str1 = "Hello";
std::string str2 = "World";
int result = str1.compare(str2);
// stoi(), stol(), stoll() 字符串转为整型
int num = std::stoi(str);
// to_string(),转为字符串
std::string str = std::to_string(num);
// c_str(), data() 返回 C 风格字符串(const char*)。
const char* cstr = str.c_str();
string_view 是C++17的特性,旨在解决substr()字符串切片会导致拷贝造成性能损失的问题
- string_view不管理内存,仅保存指向外部数据的指针和长度。不能通过string_view修改底层数据。
- 构造时不复制数据,可以直接传递字符串参数,避免临时对象的开销。
1 | void process(std::string_view text) { |
JAVA
数据结构
静态数组,
- 固定长度:数组在初始化时指定长度,后续无法修改。
- 连续内存:元素在内存中连续存储,支持快速随机访问(时间复杂度 O(1))。
- 数组本身也是对象(继承自 Object 类),存储在堆内存中。
java只有8种基本类型变量(byte, short, int, long, float, double, char, boolean)存放在栈里,数组不是
1 | int[] numbers; // 声明一个整型数组, 此时 arr = null |
ArrayList,等于C++的vector,动态数组。JAVA的List是一个接口,实现了动态数组和链表。
1 | import java.util.ArrayList; |
LinkedList,双向链表,相当于C++的list
1 | import java.util.LinkedList; |
HashSet, 基于哈希表实现,不保证元素的顺序。
LinkedHashSet, 基于哈希表和链表实现,能够保证元素的插入顺序。
TreeSet, 基于红黑树实现,元素按自然顺序或自定义顺序排列。
HashMap,基于哈希表实现,允许 null 键和值。
LinkedHashMap,基于哈希表和链表实现,保持插入顺序(或访问顺序)
TreeMap,基于红黑树实现,键按自然顺序或自定义顺序排序。
PriorityQueue,基于堆实现的队列,元素按优先级顺序排列
LinkedList 也实现了 Queue 接口,支持双端队列操作。
1 | import java.util.LinkedList; |
算法
Arrays用于操作数组,例如.sort() 用于对静态数组进行排序,数组是基本类型数组int[] arr
,或对象数组new Integer[10]
.
java的.length用于获取静态数组长度
1 | int[] arr = {5, 3, 1, 4, 2}; |
Collections用于容器算法,.sort() 用于对集合(如 List)进行排序
1 | import java.util.*; |
字符串处理
JAVA的字符串操作类主要是String, StringBuilder, StringBuffer。
String一旦创建,内容不可修改。任何修改操作(如拼接、替换)都会生成新对象。(如果固定内存的字符串需要修改,用字符数组更好)
1 | // 创建 |
StringBuilder 和 StringBuffer 是用于可变字符串的类。StringBuilder只能用于单线程环境, 直接修改内部字符数组,避免频繁创建新对象。StringBuffer 所有方法均用synchronized修饰, 性能低于StringBuilder, 但多线程安全。
修改字符串某位置的字符
1 | // String 字符串不可修改某位置字符,可以利用stringBuilder |
Go
数据结构
golang 用语言关键字提供了容器,不必额外导入包
静态数组, 固定大小的同一类型的元素。Go 数组的大小在声明时确定并且内容不可更改。
1 | var arr [5]int // 定义一个长度为 5 的整型数组 |
切片slice,动态数组。[]byte表示动态字符串。slice 的切片截取操作无须拷贝,
1 | // 创建切片 |
map,哈希表实现的容器。map底层实现和C++的unorder_map、JAVA的hashmap不同
map的底层结构体是hmap,hmap里维护着若干个bucket数组 (即桶数组)。
- 每个桶存储固定数量的键值对(通常为 8 对),如果超过8个键值对了, 会先把多余的pair放到一个溢出桶中
- map负载因子的定义是map中元素的个数 / map中当前桶的个数。当装载因子 > 6.5, 或溢出桶的数量过多,就会执行哈希表扩容。扩容非一次性完成, 每次对map进行删改操作时,会触发从oldbucket中迁移到bucket的操作, 在扩容没有完全迁移完成之前,每次get或者put遍历数据时,都会先遍历oldbuckets,然后再遍历buckets。
1 | myMap := map[string]int{ |
链表,使用container/list
1 | package main |
channel,channel可认为是一个线程安全的协程间任务队列,内部通过链表和环形链表实现,这个可以在并发编程处详细展开。
算法
排序,import “sort”标准库
golang自定义struct的排序比较麻烦, 需要对自定义的struct实现Len() int
, Less(i, j int) bool
, Swap(i, j int)
三个接口
1 | package main |
字符串处理函数
Go中,string是不可变的 UTF-8 编码字符串, 无法原地修改字符串(编译失败)。[]byte是字符序列切片,可原地修改字符串。
golang字符串处理函数strings,如果要修改字符串, 必然会有内存拷贝。
1 | s := "Hello, World!" |
bytes 包处理 []byte
1 |
|
Python
数据结构
列表,动态数组。和go一样,python列表支持切片。
1 | my_list = [1, 2, 3, 4, 5] |
元组Tuple,不可变,只可被访问。
1 | my_tuple = (1, 2, 3) |
字典dict, 无序(Python 3.7 及以上版本中插入有序)、键值对、键唯一。
- dict的内部实现基于开放寻址法的哈希表
- python3.7之后, 字典内部新增一个双向链表(或紧凑数组),按插入顺序记录所有键值对。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18my_dict = {"name": "Alice", "age": 25}
my_dict["age"] = 30
要求:对象必须实现__hash__和__eq__方法。
__hash__:返回唯一且不变的哈希值。
__eq__:判断键是否相等。
class User:
def __init__(self, id):
self.id = id
def __hash__(self):
return hash(self.id)
def __eq__(self, other):
return self.id == other.id
users = {User(1): "Alice", User(2): "Bob"}
集合set,无序、不重复。
1 | my_set = {1, 2, 3, 4, 5} |
有序字典, 保留键值对插入顺序
1 | from collections import OrderedDict |
python的 str不可变, 任何修改生成新对象(原对象不变)
1 |
|
算法
排序
sorted(iterable, key=None, reverse=False), 返回一个新的排序列表,不改变原列表。
1 | nums = [5, 2, 9, 1] |
字符串处理
1 | s = "Hello" |
对于python2, 还有个编码的问题。处理办法是,对于u开头的字符串或者unicode类型的字符串,都使用encode(“utf-8”) 编码为str类型后,再使用。
1 | # coding=utf-8 |
本文标题:编程语言(3)—数据结构和算法
文章作者:Infinity
发布时间:2024-12-10
最后更新:2025-06-02
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!