数据结构总结
- 排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 希尔排序
- 堆排序
- 快速排序算法
- 归并排序
- 计数排序
- 基数排序
- 树
- 红黑树
- 基本概念
- 规则
- B树
- 基础知识
- 规则
- B+树
- 图
- 回溯算法
- 并查集
- 拓扑排序
- 其他算法
- KMP算法
- 例题
- 数组类
- 求最大和子数组
- 求子数组最大乘积
- 删除重复链表元素
十大排序算法参考
排序算法
冒泡排序
冒泡排序通过俩个for循环来实现排序,每次循环将一个最大的数放在左侧,直到所有数排列完毕位置。
时间复杂度
o
(
n
2
)
o(n^2)
o(n2),空间复杂度
o
(
1
)
o(1)
o(1)。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void sortArray(vector<int>& nums);
int main(){
vector<int> nums({321,-3123,413,43,0,43,2,3,5,7,5,3,2,21,1,52,6,45,76,532,-4,341,-4343,24,-432,34,-43,2423});
sortArray(nums);
for(int &i:nums) cout << i << " ";
return 0;
}
void sortArray(vector<int>& nums) {
int len = nums.size();
for(int i=0;i<len;++i){
for(int j=0;j<len-i-1;++j){
if(nums[j] > nums[j+1]){
swap(nums[j], nums[j+1]);
}
}
}
return ;
}
选择排序
选择排序的思想是:双重循环遍历数组,每经过一轮比较,找到最小元素的下标,将其交换至首位。
时间复杂度
o
(
n
2
)
o(n^2)
o(n2),空间复杂度
o
(
1
)
o(1)
o(1)。
图中可以看出,每一轮排序都找到了当前的最小值,这个最小值就是被选中的数字,将其交换至本轮首位。这就是「选择排序法」名称的由来。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void sortArray(vector<int>& nums);
int main(){
vector<int> nums({321,-3123,413,43,0,43,2,3,5,7,5,3,2,21,1,52,6,45,76,532,-4,341,-4343,24,-432,34,-43,2423});
sortArray(nums);
for(int &i:nums) cout << i << " ";
return 0;
}
void sortArray(vector<int>& nums) {
int len = nums.size();
for(int i=0;i<len;++i){
int minindex = i;
for(int j=i+1;j<len;++j){
if(nums[minindex] > nums[j]) minindex = j;
}
swap(nums[i], nums[minindex]);//和最小值互换
}
return ;
}
插入排序
插入排序的思想非常简单,生活中有一个很常见的场景:在打扑克牌时,我们一边抓牌一边给扑克牌排序,每次摸一张牌,就将它插入手上已有的牌中合适的位置,逐渐完成整个排序。
时间复杂度
o
(
n
2
)
o(n^2)
o(n2),空间复杂度
o
(
1
)
o(1)
o(1)。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void sortArray(vector<int>& nums);
int main(){
vector<int> nums({321,-3123,413,43,0,43,2,3,5,7,5,3,2,21,1,52,6,45,76,532,-4,341,-4343,24,-432,34,-43,2423});
sortArray(nums);
for(int &i:nums) cout << i << " ";
return 0;
}
void sortArray(vector<int>& nums) {
int len = nums.size();
for(int i=1;i<len;++i){
int curnum = nums[i];
int j = i-1;
//向后移动大数
while(j >= 0 && nums[j] > curnum){
nums[j+1] = nums[j];
--j;
}
nums[j+1] = curnum;
}
return ;
}
希尔排序
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void sortArray(vector<int>& nums);
int main(){
vector<int> nums({321,-3123,413,43,0,43,2,3,5,7,5,3,2,21,1,52,6,45,76,532,-4,341,-4343,24,-432,34,-43,2423});
sortArray(nums);
for(int &i:nums) cout << i << " ";
return 0;
}
void sortArray(vector<int>& nums) {
// 间隔序列,在希尔排序中我们称之为增量序列
for (int gap = nums.size()/2; gap > 0; gap /= 2) {
// 分组
for (int groupStartIndex = 0; groupStartIndex < gap; groupStartIndex++) {
// 插入排序
for (int currentIndex = groupStartIndex + gap; currentIndex < nums.size(); currentIndex += gap) {
// currentNumber 站起来,开始找位置
int currentNumber = nums[currentIndex];
int preIndex = currentIndex - gap;
while (preIndex >= groupStartIndex && currentNumber < nums[preIndex]) {
// 向后挪位置
nums[preIndex + gap] = nums[preIndex];
preIndex -= gap;
}
// currentNumber 找到了自己的位置,坐下
nums[preIndex + gap] = currentNumber;
}
}
}
}
堆排序
原理描述
代码实现
#include <iostream>
#include <vector>
using namespace std;
void heap_adjust(vector<int>& nums, int begin, int end);
void sort_heap(vector<int>& nums, int begin, int end);
int main(){
vector<int> nums({2,4,5,7,8,2,1,5,7,856,545,23,4,1,23,23,213,1323,-321,-32,-321,-1,2,0,6435,65,80});
sort_heap(nums, 0, nums.size());
for(int &i:nums) cout << i << " ";
return 0;
}
void sort_heap(vector<int>& nums, int begin, int end){
for(int i=(end-1)/2;i>=0;--i){
heap_adjust(nums, i, end);
}
for(int i=end;i>0;--i){
swap(nums[0], nums[i-1]);
heap_adjust(nums, 0, i-1);
}
}
void heap_adjust(vector<int>& nums, int begin, int end){
int father = begin;
int son = 2*father+1;
while(son+1 < end && son < end){
if(nums[son] < nums[son+1]) ++son;
if(nums[father] < nums[son]){
swap(nums[father], nums[son]);
father = son;
son = 2*father+1;
}else{
return;
}
}
}
快速排序算法
快速排序算法由 C. A. R. Hoare 在 1960 年提出。它的时间复杂度也是 (nlogn)O(nlogn),但它在时间复杂度为O(nlogn) 级的几种排序算法中,大多数情况下效率更高,所以快速排序的应用非常广泛。再加上快速排序所采用的分治思想非常实用,使得快速排序深受面试官的青睐,所以掌握快速排序的思想尤为重要。
快速排序算法的基本思想是:
从数组中取出一个数,称之为基数(pivot)
遍历数组,将比基数大的数字放到它的右边,比基数小的数字放到它的左边。遍历完成后,数组被分成了左右两个区域,将左右两个区域视为两个数组,重复前两个步骤,直到排序完成。
事实上,快速排序的每一次遍历,都将基数摆到了最终位置上。第一轮遍历排好 1 个基数,第二轮遍历排好 2 个基数(每个区域一个基数,但如果某个区域为空,则此轮只能排好一个基数),第三轮遍历排好 4 个基数(同理,最差的情况下,只能排好一个基数),以此类推。总遍历次数为 logn~n 次,每轮遍历的时间复杂度为 O(n),所以很容易分析出快速排序的时间复杂度为
O
(
n
l
o
g
n
)
~
O
(
n
2
)
O(nlogn) ~ O(n^2)
O(nlogn)~O(n2),平均时间复杂度为 O(nlogn)。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void temp(vector<int> &nums, int l, int r);
int n;
int main(){
vector<int> nums({321,-3123,413,43,0,43,2,3,5,7,5,3,2,21,1,52,6,45,76,532,-4,341,-4343,24,-432,34,-43,2423});
n = nums.size();
temp(nums, 0, n);
for(int &i:nums) cout << i << " ";
return 0;
}
void sortArra(vector<int> &nums, int l, int r){
//当需要排序的数组中只有一个或者没有元素时直接跳出
if(r-l <= 1) return ;
int pivot = l;//基数位置选取
for(int i=l+1;i<r;++i){
//判断是否需要进行位置更新
if(nums[i] < nums[pvio]){
nums.insert(nums.begin()+pvio, nums[i]);
nums.erase(nums.begin()+i+1);
++pvio;
}
}
//递归求解
temp(nums, l, pvio);
temp(nums, pvio+1, r);
return ;
}
归并排序
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
算法步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
时间复杂度 o ( n l o g n ) o(nlogn) o(nlogn),空间复杂度 o ( n ) o(n) o(n)。
#include <iostream>
#include <vector>
using namespace std;
void sortArray(vector<int>& nums);
int main(){
vector<int> nums({312,312,312,3213,312,12,43,2,345,356,44,5,658,56,546,5,32,0,1});
sortArray(nums);
for(int &i:nums) cout << i << " ";
return 0;
}
void sortArray(vector<int>& nums){
int len = nums.size();
vector<int> temp = nums;
//排序数组大小
for(int seg=1;seg<len;seg += seg){
//对每个子数组进行排序
for(int start=0;start<len;start += 2*seg){
int l = start, m = min(len, start+seg), h = min(len, start+2*seg);
int k = l;
int s1 = l, e1 = m;
int s2 = m, e2 = h;
//选取俩个子数组中最小的进行填充
while(s1 < e1 && s2 < e2){
temp[k++] = nums[s1] < nums[s2] ? nums[s1++]:nums[s2++];
}
//将剩余的进行尾加
while(s1 < e1){
temp[k++] = nums[s1++];
}
while(s2 < e2){
temp[k++] = nums[s2++];
}
}
//保留排序后的数组
swap(temp, nums);
}
}
计数排序
算法的步骤如下:
- 找出待排序的数组中最大和最小的元素
- 统计数组中每个值为i的元素出现的次数,存入数组C的第i项
- 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1
时间复杂度
o
(
n
)
o(n)
o(n),空间复杂度
o
(
n
+
k
)
o(n+k)
o(n+k)。
基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
时间复杂度
o
(
n
)
o(n)
o(n),空间复杂度
o
(
n
+
k
)
o(n+k)
o(n+k)。
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
void sortArray(vector<int>& nums);
int main(){
vector<int> nums({312,312,312,3213,312,12,43,2,345,356,44,5,658,56,546,5,32,0,1});
sortArray(nums);
for(int &i:nums) cout << i << " ";
return 0;
}
//求解最大数据位数
int maxbit(vector<int>& nums){
int maxbit = 0;
int maxnum = *max_element(nums.begin(), nums.end());
while(maxnum){
++maxbit;
maxnum /= 10;
}
return maxbit;
}
void sortArray(vector<int>& nums){
int len = nums.size();
int d = maxbit(nums);
vector<int> temp = nums;
//创建排序桶
vector<int> count(10, 0);
for(int i=0;i<d;++i){
count = vector<int>(10, 0);
//计数值求解
for(int j=0;j<len;++j){
count[(int)(nums[j]/pow(10, i))%10]++;
}
//保证后续元素正确插入相关的位置
for(int j=1;j<10;++j) count[j] += count[j-1];
//从后向前插入数据
for(int j=len-1;j>=0;--j){
temp[--count[(int)(nums[j]/pow(10, i))%10]] = nums[j];
}
swap(nums, temp);
}
return ;
}
树
红黑树
基本概念
在具体实现红黑树之前,必须弄清它的基本含义。红黑树本质上是一颗二叉搜索树,它满足二叉搜索树的基本性质——即树中的任何节点的值大于它的左子节点,且小于它的右子节点。理论上,一颗平衡的二叉搜索树的任意节点平均查找效率为树的高度h,即O(lgn)。但是如果二叉搜索树的失去平衡(元素全在一侧),搜索效率就退化为O(n),因此二叉搜索树的平衡是搜索效率的关键所在。于是红黑树就是通过自身的特性去维持整个搜索叔的平衡性的。
规则
顾名思义,红黑树的节点是有颜色概念的,即非红即黑。通过颜色的约束,红黑树维持着二叉搜索树的平衡性。一颗红黑树必须满足以下几点条件:
- 根节点必须是黑色。
- 任意从根到叶子的路径不包含连续的红色节点。
- 任意从根到叶子的路径的黑色节点总数相同。
B树
基础知识
B树就是一棵平衡的多叉查找树。用于实现快速查找,相对于二叉树,具有更多的分支,更小的高度。查找树的高度决定了查找过程中访问磁盘的次数,而磁盘的访问速度低。由于B树具有更小的高度,因此在查找时对磁盘的访问会大大降低,从而相对于二叉查找树有更高的效率。
规则
下面的 m 是 B 树的阶,阶就是一颗树每个结点最多包含的孩子数,m 取值与磁盘页大小有关
1.根结点至少两个子女
2.每个中间结点有 k-1 个元素,和有 k 个孩子,m/2 <= k <= m(m/2 向上取整)
3.每个叶子结点有 k-1 个元素,m/2 <= k <= m(m/2 向上取整)
4.所有叶子结点都位于同一层
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划
B+树
B+树和B树的区别:
- B+跟B树不同在于B+树的非叶子节点不保存关键字记录的指针,只进行数据索引,这样使得B+树每个非叶子节点所能保存的关键字大大增加;
- B+树叶子节点保存了父节点的所有关键字记录的指针,所有数据地址必须要到叶子节点才能获取到。所以每次数据查询的次数都一样;
图
回溯算法
Backtracking:回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法。
相关例题
代码实现:
class Solution {
public:
vector<vector<int>> ans;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<int> tempcand;
temp(candidates, tempcand,target, 0);
return ans;
}
void temp(vector<int>& candidates, vector<int>& tempcand, int target, int idx){
//判断是否生成满足条件的结果
if(target == 0) {
ans.push_back(tempcand);
return ;
}
//判断是否需要剪枝
if(target < 0) return ;
//遍历所有值进行回溯
for(int i=idx;i<candidates.size();++i){
vector<int> v = tempcand;
v.push_back(candidates[i]);
temp(candidates, v, target-candidates[i], i);
}
return ;
}
};
并查集
参考资料
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。主要涉及到俩种操作:
- 查询(Find):查询图中的两个顶点是不是在同一个集合中。
注意:并查集只回答两个顶点之间是否有一条路径连接,而不回答怎么连接。 - 合并(Union):将两个不相交集合进行合并。
首先需要初始化父节点数组,初始化状态下默认自己为自身的父节点。
vector<int> fa;
for(int i=0;i<26;++i) fa.push_back(i);
之后设计查询函数
int find(int x){
if(fa[x] == x) return x;
else{
fa[x] = find(fa[x]);
return fa[x];
}
}
最后设计合并操作
void merg(int a, int b){
fa[find(a)] = find(b);
}
在实际应用中需要通过判断是否位于一个组合进而判断是否满足要求并进行下一步的操作。
练习题目
拓扑排序
参考链接
拓扑排序实质是对有向图的顶点排成一个线性序列,生成的排序满足顶点之间的相互依赖关系。
相关例题
代码实现:
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
//保存依赖关系
vector<vector<int>> edgs(numCourses);
//记录每个顶点的入度
vector<int> index(numCourses, 0);
//初始化依赖关系和顶点入度
for(auto &v:prerequisites){
edgs[v[1]].push_back(v[0]);
++index[v[0]];
}
vector<int> ans;
deque<int> d;
//初始条件下入度为空的顶点入队列。
for(int i=0;i<numCourses;++i){
if(index[i] == 0){
ans.push_back(i);
d.push_back(i);
--index[i];
}
}
//结合队列实现广度优先队列
while(!d.empty()){
int currnum = d.front();
d.pop_front();
//对应的依赖节点入度-1
for(int &i:edgs[currnum]){
if(--index[i] == 0){
--index[i];
ans.push_back(i);
d.push_back(i);
}
}
}
//如多还有
if(ans.size() != numCourses) return vector<int>();
return ans;
}
};
其他算法
KMP算法
例题
数组类
求最大和子数组
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
vector<int> findMaxsubarr1(vector<int>& nums);
vector<int> findMaxsubarr2(vector<int>& nums);
int main(){
vector<int> nums({-1,1,2,3,-4,5,6,-3,8,10});
vector<int> ans = findMaxsubarr2(nums);
for(int &i:ans) cout << i << " ";
return 0;
}
vector<int> findMaxsubarr1(vector<int>& nums){
int len = nums.size();
int sum = accumulate(nums.begin(), nums.end(), 0);
int tempsum = sum;
vector<int> ans;
int l = 0, r = len-1;
while(l < r){
if(nums[l] < nums[r]){
sum -= nums[l++];
}else{
sum -= nums[r--];
}
if(tempsum < sum) ans = vector<int>(nums.begin()+l, nums.begin()+r+1);
}
return ans;
}
vector<int> findMaxsubarr2(vector<int>& nums){
int len = nums.size();
int maxnum = 0;
int tempsum = 0;
int l = 0, r = 0;
vector<int> ans;
for(int i=0;i<len;++i){
tempsum += nums[i];
r = i;
if(maxnum < tempsum){
ans = vector<int>(nums.begin()+l, nums.begin()+r+1);
maxnum = tempsum;
}
if(tempsum < 0){
l = i+1;
r = l;
tempsum = 0;
}
}
return ans;
}
求子数组最大乘积
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;
int maxProduct(vector<int>& nums);
int main(){
vector<int> nums({-1,1,2,3,-4,5,6,-3,8,10});
cout << maxProduct(nums);
return 0;
}
int maxProduct(vector<int>& nums) {
int len = nums.size();
//维持一以当前元素结尾,最大和最小队列
vector<int> fmax(len, 0), fmin(len, 0);
fmax[0] = nums[0], fmin[0] = nums[0];
for(int i=1;i<len;++i){
//同时更新当前的最大最小队列状态,
fmax[i] = max(fmax[i-1]*nums[i], max(nums[i], fmin[i-1]*nums[i]));
fmin[i] = min(fmin[i-1]*nums[i], min(nums[i], fmax[i-1]*nums[i]));
}
return *max_element(fmax.begin(), fmax.end());
}
删除重复链表元素
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Listnode{
int val;
Listnode* next;
};
Listnode* temp(Listnode* head);
int main(){
Listnode n0{1},n1{2},n2{2},n3{2},n4{2},n5{6};
n0.next = &n1, n1.next = &n2, n2.next = &n3, n3.next = &n4, n4.next = &n5, n5.next = nullptr;
Listnode *node = temp(&n0);
while(node){
cout << node->val << " ";
node = node->next;
}
return 0;
}
Listnode* temp(Listnode* head){
Listnode* node = head;
Listnode* pre = new Listnode{0};
Listnode* tempnode = nullptr;
pre->next = node;
while(node){
int num = node->val;
if(node->next && node->next->val == num){
while(node->next && node->next->val == num){
node = node->next;
}
node = node->next;
}
if(!tempnode) tempnode = node;
pre->next = node;
pre = node;
node = node->next;
}
return tempnode;
}