Algorithm WebSite-知乎
排序 - Sort
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。
时间复杂度
算法名称 | 类型 | 最坏情况 | 最好情况 | 平均情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|---|
冒泡排序 | 非线性时间 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
快速排序 | 非线性时间 | O(n2) | O(nlog2n) | O(nlog2n) | O(nlog2n) | 不稳定 |
简单插入排序 | 非线性时间 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
希尔排序 | 非线性时间 | O(n2) | O(n) | O(n1.3) | O(1) | 不稳定 |
简单选择排序 | 非线性时间 | O(n2) | O(n) | O(n2) | O(1) | 不稳定 |
堆排序 | 非线性时间 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
归并排序 | 非线性时间 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
计数排序 | 线性时间 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
桶排序 | 线性时间 | O(n2) | O(n) | O(n+k) | O(n+k) | 稳定 |
基数排序 | 线性时间 | O(n*k) | O(n*k) | O(n*k) | O(n+k) | 稳定 |
冒泡排序法
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间
/**
* 冒泡排序法
* asc 用来判断排序顺序
* 时间复杂度: O(n^2)
* 空间复杂度: S(1)
*/
void BubbleSort(vector<int>& vec, int asc = 1) {
int size = vec.size(), temp; //temp用做中间变量进行交换数值
//外循环控制循环次数,对 n 个数来说,一共要循环 n - 1 次, 因为每次都会确定一个当前最大(小)的数
for (int i = size - 1; i > 0; i--) {
//从第1个数开始相邻数比较,将较大(小)的数移动到后面
for (int j = 0; j < i; j++) {
if (asc ? vec[j] > vec[j + 1] : vec[j] < vec[j + 1]) {
swap(vec[j], vec[j + 1]);
}
}
}
}
选择排序
/**
* 选择排序法
* asc 是否是顺序排序
* 时间复杂度 O(n^2)
* 空间复杂度 O(1)
* 不断的从头到尾进行查找,每次查找中,将比结尾大的放到结尾,比开头小的放到开头
*/
void SelectSort(vector<int>& vec, int asc = 1) {
int left = -1, right = vec.size();
while (++left <= --right) {
for (int i = left; i < right; i++) {
if (asc ? vec[i] > vec[right] : vec[i] < vec[right]) swap(vec[i], vec[right]); //找到当前最大的数并将其置换到最后
if (asc ? vec[i] < vec[left] : vec[i] > vec[left]) swap(vec[i], vec[left]); //找到当前最小的数并将其置换到当前位置的最前
}
}
}
鸡尾酒排序
鸡尾酒排序又称双向冒泡排序、鸡尾酒搅拌排序、搅拌排序、涟漪排序、来回排序或快乐小时排序, 是冒泡排序的一种变形。该算法与冒泡排序的不同处在于排序时是以双向在序列中进行排序。
原理:数组中的数字本是无规律的排放,先找到最小的数字,把他放到第一位,然后找到最大的数字放到最后一位。然后再找到第二小的数字放到第二位,再找到第二大的数字放到倒数第二位。以此类推,直到完成排序。
/**
* 鸡尾酒排序法
* asc 是否是顺序排序
* 时间复杂度 O(n^2)
* 空间复杂度 O(1)
* 不断进行查找,先前往后找到最大的数将它放到最后面,然后再从后往前将最小的数放到最前面
*/
void CocktailSort(vector<int>& vec, int asc = 1) {
int size = vec.size(), left = 0, right = size - 1, times = 0, max, min;
while (times < size / 2) { //每次为一个来回,即从left到right,再从right - 1 到 left + 1
times++; //统计次数
max = right, min = left; //记录最大/小值
for (int i = left; i <= right; i++) { //第一趟,从left到right找最大值
if (asc ? vec[i] > vec[max] : vec[i] < vec[max]) max = i;
}
swap(vec[max], vec[right--]); //交换最大值和最右面
for (int i = right; i >= left; i--) { //第二趟,从right - 1 到 left + 1 找最小值
if (asc ? vec[i] < vec[min] : vec[i] > vec[min]) min = i;
}
swap(vec[min], vec[left++]); //交换最小值和最右面
}
}
/**
* 鸡尾酒排序法2
* asc 是否是顺序排序
* 时间复杂度 O(n^2)
* 空间复杂度 O(1)
* 不断进行查找,从前往后不断交换不是顺序的数,将最大的数放到最后,再从后往前不断交换,将较小的数放到最前
*/
void CocktailSort2(vector<int>& vec, int asc = 1) {
int size = vec.size(), left = 0, right = size - 1, times = 0, flag = 1;
while (times < size / 2) { //每次为一个来回,即从left到right,再从right - 1 到 left + 1
times++; //统计次数
for (int i = left; i < right; i++) { //第一趟,从left到right找最大值
if (asc ? vec[i] > vec[i + 1] : vec[i] < vec[i + 1]) {
swap(vec[i], vec[i + 1]);
flag = 0;
}
}
right--;
for (int i = right; i > left; i--) { //第二趟,从right - 1 到 left + 1 找最小值
if (asc ? vec[i] < vec[i - 1] : vec[i] > vec[i - 1]) {
swap(vec[i], vec[i - 1]);
flag = 0;
}
}
left++;
}
}
/**
* 鸡尾酒排序法2 PLUS
* asc 是否是顺序排序
* 特点:冒泡排序进阶,适用于大部分都是有序的情况
* 时间复杂度 O(n^2)
* 空间复杂度 O(1)
* 不断进行查找,先前往后将最大的数放到后面,然后再从后往前将较小的数放到最前面
* 如果当前查找没有进行交换,则说明当前查找的范围[left, right]内是有序的,所以不用继续排序下去
*/
void CocktailSortPlus(vector<int>& vec, int asc = 1) {
int size = vec.size(), left = 0, right = size - 1, times = 0, flag = 1;
while (times < size / 2) { //每次为一个来回,即从left到right,再从right - 1 到 left + 1
times++; //统计次数
flag = 1;
for (int i = left; i < right; i++) { //第一趟,从left到right找最大值
if (asc ? vec[i] > vec[i + 1] : vec[i] < vec[i + 1]){
swap(vec[i], vec[i + 1]);
flag = 0;
}
}
right--;
if (flag) break;
flag = 1;
for (int i = right; i > left; i--) { //第二趟,从right - 1 到 left + 1 找最小值
if (asc ? vec[i] < vec[i - 1] : vec[i] > vec[i - 1]) {
swap(vec[i], vec[i - 1]);
flag = 0;
}
}
if (flag) break;
left++;
}
}
插入排序法
所谓插入排序法,就是检查第i个数字,如果在它的左边的数字比它大,进行交换,这个动作一直继续下去,直到这个数字的左边数字比它还要小,就可以停止了。插入排序法主要的回圈有两个变数:i和j,每一次执行这个回圈,就会将第i个数字放到左边恰当的位置去。
void InsertSort(vector<int>& vec) {
int size = vec.size();
//外层循环控制当前查找交换的位置,内层循环进行控制判断和交换
//lastChangeIndex记录当前内循环最终进行交换的位置,如果当前循环并没有进行交换,则使用lastChangeIndex增一进行增加外层循环
for (int i = 0, lastChangeIndex = 0; i < size - 1; i = ++lastChangeIndex) {
for (int j = size - 1; j > i; j--) {
if (vec[j] < vec[j - 1]) {
swap(vec[j], vec[j - 1]);
lastChangeIndex = j - 1;
}
}
}
}
另一情况下,向已经有序的数列中插入一个数,则从前往后或者从后往前进行查找到当前结点应该在的位置,找到后,则从后往前移动结点,将应该插入的位置空出来,放入待插入结点
桶排序
将数组分到有限数量的桶子里。每个桶子再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间O(n)。但桶排序并不是 比较排序,他不受到 O(nlogn)下限的影响。
/**
* 桶排序
* 时间复杂度:O(n+k),k为 n*logn-n*logm
* 空间复杂度:O(n+m), m是桶的数量
*
* 先遍历一遍找到最大值和最小值,然后通过将这个区间进行等分,然后对数组进行遍历,将其放入对应的链式区间内
* 遍历结束后,对每个区间进行排序,然后重新进行合并
*/
void BucketSort(vector<int>& vec) {
int size = vec.size(), bucketNum, min = vec[0], max = vec[0], gap;
for (int i = 1; i < size; i++) {
min = min > vec[i] ? vec[i] : min;
max = max < vec[i] ? vec[i] : max;
}
//0~100 10个数 gap = 11 bucketSize = 10
gap = (max - min) / size + 1; //确定每个桶之间的间隔
bucketNum = (max - min) / gap + 1; //确定桶的数量
vector<vector<int>> buckets(bucketNum);
for (int i = 0; i < size; i++) {
buckets[(vec[i] - min) / gap].push_back(vec[i]);
}
for (int i = 0; i < bucketNum; i++)
sort(buckets[i].begin(), buckets[i].end());
for (int i = 0, index = -1; i < bucketNum; i++) {
for (int sz = buckets[i].size(), j = 0; j < sz; j++) {
vec[++index] = buckets[i][j];
}
}
}
计数排序
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为
Ο(n+k)
(其中k是整数的范围),快于任何比较排序算法。当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))
的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n))
, 如归并排序,堆排序)
算法思想
计数排序对输入的数据有附加的限制条件:
1、输入的线性表的元素属于有限偏序集S;
2、设输入的线性表的长度为n,|S|=k(表示集合S中元素的总数目为k),则k=O(n)。
在这两个条件下,计数排序的复杂性为O(n)。
计数排序的基本思想是对于给定的输入序列中的每一个元素x,确定该序列中值小于x的元素的个数(此处并非比较各元素的大小,而是通过对元素值的计数和计数值的累加来确定)。一旦有了这个信息,就可以将x直接存放到最终的输出序列的正确位置上。例如,如果输入序列中只有17个元素的值小于x的值,则x可以直接存放在输出序列的第18个位置上。当然,如果有多个元素具有相同的值时,我们不能将这些元素放在输出序列的同一个位置上,因此,上述方案还要作适当的修改。
/**
* 计数排序
* 时间复杂度O(n+k),k是整数的范围
* 空间复杂度O(k)
*/
void CountingSort(vector<int>&vec, int asc){
int size = vec.size(), max = vec[0], min = vec[0];
for (int i = 1; i < size; i++) {//找到最小值和最大值
max = (vec[i] > max ? vec[i] : max);
min = (vec[i] < min ? vec[i] : min);
}
int gap = max - min + 1; //计算出最大值和最小值的差值,得出统计数组的大小
vector<int>cnt_arr = vector<int>(gap);
for (int i = 0; i < size; i++) { //统计每个大小的元素的个数
cnt_arr[vec[i] - min]++;
}
int sum = 0;
for (int i = 0; i < gap; i++) { //统计到当前位置的元素的和并赋值给当前位置,说明该值对应的元素在排序后应该在的位置
sum += cnt_arr[i];
cnt_arr[i] = sum;
}
vector<int> backup = vector<int>(vec);
for (int i = size - 1; i >= 0; i--) { //获取当前元素在排序后应该在的位置
//为什么要当前元素所在的位置后为什么还要 减一? 因为计算出的位置是从1开始数的
vec[cnt_arr[backup[i] - min] - 1] = backup[i];
cnt_arr[backup[i] - min]--;
}
}
归并排序
采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
/**
* 归并排序-递归法
* 时间复杂度:O(nlogn)
* 空间复杂度:O(n)
* 先将当前数组分成两个部分,然后分别进行排序,然后进行归并
*/
void MergeSort_Recursive_Exec(vector<int>& vec, vector<int>& temp, int start, int end) {
if (start >= end) return;
int mid = (end + start) / 2;
//进行排序
MergeSort_Recursive_Exec(vec, temp, start, mid);
MergeSort_Recursive_Exec(vec, temp, mid + 1, end);
//进行归并,借用中间数组temp 进行排序
int i, j, k;
for (i = start, j = mid + 1, k = 0; i <= mid && j <= end; ) {
if (vec[i] > vec[j])
temp[k++] = vec[j++];
else
temp[k++] = vec[i++];
}
while (j <= end) temp[k++] = vec[j++];
while (i <= mid) temp[k++] = vec[i++];
//将中间数组的数复制到原数组中
for (i = start, j = 0; j < k; i++, j++) {
vec[i] = temp[j];
}
}
void MergeSort_Recursive(vector<int>& vec) {
int size = vec.size();
vector<int> temp(size + 1);
MergeSort_Recursive_Exec(vec, temp, 0, size - 1);
}
/**
* 归并排序-迭代法
* 时间复杂度:O(nlogn)
* 空间复杂度:O(n)
* 先将当前数组分成两个部分,然后分别进行排序,然后进行归并
*/
void MergeSort_Iteration(vector<int>& vec) {
int size = vec.size(), left = 0, right = size - 1, mid, segment = 1, tloc = 0, i, j, k;
vector<int> tArr(size + 1); //创建一个中间数组,用来存放中间的顺序关系
while (segment < size) {
for (i = 0; i * segment < size; tloc = 0, i += 2) {
//确定当前最左最右边界,右边界不能超过当前数组的长度
left = i * segment, right = (i + 2) * segment - 1;
mid = (left + right) >> 1;
right = (right > size - 1 ? size - 1 : right);
//优化部分,如果当前left到right的距离小于segment,说明该部分已经排过序了
if (right - left < segment) continue;
//这个地方放一个 left < right 的判断,防止越界
//然后将 left - mid 和 mid + 1 - right 的两块通过中间数组 tArr进行排序
for (j = left, k = mid + 1; j <= mid && k <= right;) {
tArr[tloc++] = vec[j] < vec[k] ? vec[j++] : vec[k++];
}
while (k <= right) tArr[tloc++] = vec[k++];
while (j <= mid) tArr[tloc++] = vec[j++];
for (k = 0, j = left; k < tloc; k++, j++) {
vec[j] = tArr[k];
}
}
segment *= 2;
}
}
/************** NOT PERFECT ONE*********************/
void MergeSort_Iteration(vector<int>& vec) {
int size = vec.size(), left = 0, right = size - 1, mid, times = size, timevar, segment = 1, tloc = 0, i, j, k;
vector<int> tArr(size + 1); //创建一个中间数组,用来存放中间的顺序关系
while (segment < size) {
times = (times + 1) / 2;
for (i = 0, timevar = 0; timevar++ < times; tloc = 0, i += 2) {
//确定当前最左最右边界,右边界不能超过当前数组的长度
left = i * segment, right = (i + 2) * segment - 1;
mid = (left + right) >> 1;
right = (right > size - 1 ? size - 1 : right);
//优化部分,如果当前left到right的距离小于segment,说明该部分已经排过序了
if (right - left < segment) continue;
//这个地方放一个 left < right 的判断,防止越界
//然后将 left - mid 和 mid + 1 - right 的两块通过中间数组 tArr进行排序
for (j = left, k = mid + 1; j <= mid && k <= right;) {
tArr[tloc++] = vec[j] < vec[k] ? vec[j++] : vec[k++];
}
while (k <= right) tArr[tloc++] = vec[k++];
while (j <= mid) tArr[tloc++] = vec[j++];
for (k = 0, j = left; k < tloc; k++, j++) {
vec[j] = tArr[k];
}
}
segment *= 2;
}
}
排序二叉树
二叉排序树
定义一
一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
(4)没有键值相等的结点。
定义二
一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于或等于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
定义三
一棵空树,或者是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有结点的值均小于或等于它的根结点的值;
(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(3)左、右子树也分别为二叉排序树;
【注】:以上的三种定义在不同的数据结构教材中均有不同的定义方式 但是都是正确的 在开发时需要根据不同的需求进行进行选择
鸽巢排序
鸽巢排序(Pigeonhole sort),也被称作基数分类,是一种时间复杂度为O(n)(大O符号)且在不可避免遍历每一个元素并且排序的情况下效率最好的一种排序算法。但它只有在差值(或者可被映射在差值)很小的范围内的数值排序的情况下实用。
当涉及到多个不相等的元素,且将这些元素放在同一个"鸽巢"的时候,算法的效率会有所降低。为了简便和保持鸽巢排序在适应不同的情况,比如两个在同一个存储桶中结束的元素必然相等
我们一般很少使用鸽巢排序,因为它很少可以在灵活性,简便性,尤是速度上超过其他排序算法。事实上,桶排序较鸽巢排序更加的实用。
/**
* 鸽巢排序
* 时间复杂度:O(n)
* 空间复杂度:O(N), N为当前待排序中的最大值
* @param:vec 待排序序列
* @step:
* 创建一个以当前数组最大值为数组大小的备用数组(鸽巢),遍历待排序序列,将所有的数存放到备用数组中,数组的索引即为数的值
*/
void PigeonHoleSort(vector<int>& vec) {
int size = vec.size(), max = vec[0];
for (int i = 1; i < size; i++)
if (vec[i] > max) max = vec[i];
vector<int> pigeonHole(max + 1, 0);
for (int i = 0; i < size; i++)
pigeonHole[vec[i]]++;
for (int i = 0, j = -1; i <= max; ) {
if (pigeonHole[i]) {
vec[++j] = i;
pigeonHole[i]--;
}
else i++;
}
}
基数排序
属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或binsort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O(nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的稳定性排序法。
算法分类
最高位优先(Most Significant Digit first)法,简称MSD法:先按k1排序分组,同一组中记录,关键码k1相等,再对各组按k2排序分成子组,之后,对后面的关键码继续这样的排序分组,直到按最次位关键码kd对各子组排序后。再将各组连接起来,便得到一个有序序列。
最低位优先(Least Significant Digit first)法,简称LSD法:先从kd开始排序,再对kd-1进行排序,依次重复,直到对k1排序后便得到一个有序序列。
/**
* 基数排序 - LSD
* radix -- 基数,根
* 类型1:最低位优先
* 限制:必须都为正整数(可等于0)
* 步骤:
* 先计算出当前的数组中的最大值,然后计算出该最大值的位数,用来作为基数循环的次数
* 然后,依次将基数从个位到最大位进行循环
* 每次循环下
* 设置两个数组:count(同基数计数,初始值为0)和record(当前基数排序后记录排序后数组顺序的数组,数组大小和原数组相同)
* 按顺序遍历数组,根据基数在count对应位置增一,然后将count数组当前项等于前一项和当前项之和,得出最终各个数在record数组中的位置
* 接着,逆序遍历原数组(原因在下方),根据基数大小存放到record对应的位置
* 最后将record数组依次更新原数组
*/
void RadixSort_LSD(vector<int>&vec) {
int max = vec[0], size = vec.size(), maxRound = 1, radix = 1, i;
for (i = 0; i < size; i++) //获取最大值
max = max < vec[i] ? vec[i] : max;
while (max /= 10) maxRound++; //获取最大值的最大位数
//count[10]数组,用来存放当前基数的数的个数,recod[n]用来暂时存放按照索引排序的数
vector<int> count(10, 0), record(size);
while (maxRound--) {
//循环一遍将当前位数的值放入对应的桶中
for (i = 0; i < 10; i++) count[i] = 0;
for (i = 0; i < size; i++)
count[(vec[i] / radix) % 10]++;
//根据基数计数数组,将当前项和前一项相加,最终得到当前计数的值对应的值应该在新排序数组下的位置
for (i = 1; i < 10; i++)
count[i] += count[i - 1];
//将vec中的数组按照count的顺序放到record中
//如果是正序遍历-->错误,如果改为倒序-->正确?
//[分析]
//正序计数完之后,如果还是正序取,则在当前基数的情况下,同一基数的数的相对位置相反,如果改为逆序取出,在后面的数在较大的位置,则同一基数的数相对位置不变
//为什么相对位置不变才能保证最终有序呢?
//因为每一次根据基数排序,都必须确保该基数条件下较小基数在前,较大基数在后,基数变大后,在原相对位置的基础上,将新基数下较大的数后移,较小的数前移
for (i = size - 1; i >= 0; i--)
record[--count[(vec[i] / radix) % 10]] = vec[i];
//将桶中所有的数按照基数的顺序重新放入原数组中
for (i = 0; i < size; i++)
vec[i] = record[i];
radix *= 10;
PrintVector(vec, to_string(maxRound).append(" Round"), 0);
}
}
/**
* 基数排序 - MSD
* 类型2:最高位优先
* 限制:必须都为正整数(可等于0)
* 步骤:
* 基数从最大位到最小位排序
* 每次排序后生成一个子桶,然后再对该子桶进行基数降位排序
*/
void RadixSort_MSD_Exec(vector<int>& vec, int begin, int end, int radix) {
if (end <= begin || radix <= 0) return;
vector<int> count(10, 0), count_bu(10), record(end - begin + 1);
for (int i = begin; i <= end; i++)
count[(vec[i] / radix) % 10]++;
count_bu[0] = count[0];
for (int i = 1; i < 10; i++)
count_bu[i] = count[i] += count[i - 1];
for (int i = end; i >= begin; i--)
record[--count[(vec[i] / radix) % 10]] = vec[i];
for (int i = begin; i <= end; i++)
vec[i] = record[i - begin];
//出现逻辑错误的地方,没有弄明白当前的位置,count[9]最终的值是当前序列的长度即end - begin + 1,所以需要从begin开始
RadixSort_MSD_Exec(vec, begin, begin + count_bu[0] - 1, radix / 10);
for (int i = 1; i < 10; i++)
RadixSort_MSD_Exec(vec, begin + count_bu[i - 1], begin + count_bu[i] - 1, radix / 10);
}
void RadixSort_MSD(vector<int>&vec) {
int max = vec[0], radix = 1, size = vec.size();
for (int i = 0; i < size; i++) max = max > vec[i] ? max : vec[i];
while (max /= 10) radix *= 10;
RadixSort_MSD_Exec(vec, 0, size - 1, radix);
}
希尔排序
希尔排序(Shell’s Sort)是插入排序的一种又称“缩小增量排序”(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。该方法因 D.L.Shell 于 1959 年提出而得名。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至 1 时,整个文件恰被分成一组,算法便终止。
算法背景
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
希尔排序属于插入类排序的,先将序列中每个di个数单独取出排序,之后再插入其中。当前序列采用该增量排序、插入完成后,再采用较小的增量进行排序
主要就是三层循环
for(控制步长)
for(增加比较数字)
for(对新增的数,从后往前进行比较交换)
/**
* 希尔排序
* 时间复杂度:O(n^2/3)
* param: vec: 存数数组 delta: 增量
* 根据增量进行排序
*/
void ShellSort(vector<int>& vec) {
int size = vec.size();
//控制间隔delta,delta初始为数列长度的一半,然后逐层递减
for (int delta = size / 2; delta > 0; delta /= 2)
//控制外层循环,即控制开始比较的范围
for (int i = delta; i < size; i++)
//控制内层循环,找到i - delta位置上的数应该出现在的位置,其实就是逆向冒泡法
//但是加上了一个附加的判断语句,因为在外循环控制的参与对比的数字向后移了一位,如果这后加的一位比前面最大的一位还大也就不用移动了
for (int j = i; j >= delta && vec[j] < vec[j - delta]; j -= delta)
swap(vec[j], vec[j - delta]);
}
能否优化?下面以一组10个数的数据进行观察(不一定能优化啊,先写在这里,看看最后的结果怎么样)
堆排序
指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:
- 最大堆调整(Max Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
- 创建最大堆(Build Max Heap):将堆中的所有数据重新排序
- 堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
0 1 2 3 4 5 67 8 9 10 11 12 13 14上面就是堆在数组中的排放方式设父亲节点为 n ,则左右子节点分别为:2n + 1, 2n + 2设当前堆的大小为size,则最后一个非叶子加点数组下标为:size / 2 - 1/** * 堆排序-大顶堆调整函数 * param: vec存数数组,start开始位置,end当前数组结束的位置(不是真正的vec.size(),最后是已经排序过后的数) * 以start为根节点进行遍历子树,如不不是则递归地调整子树结点直到为大顶堆 */ void Max_Heapify(vector<int>& vec, int start, int end) { for (int father = start, son; father * 2 + 1 < end; father *= 2) { son = father * 2 + 1; if (son + 1 < end && vec[son] < vec[son + 1]) son++; //如果可以交换则交换左右节点中最大的结点 if (vec[father] < vec[son]) { //如果最大的子节点比父节点都小,则本身就是大顶堆,否则进行调整 swap(vec[father], vec[son]); Max_Heapify(vec, son, end); } else return; }}/** * 堆排序 * 时间复杂度:建堆O(n) + 排序O(nlogn) * 空间复杂度:O(1) * 步骤: * 先根据堆排序调整的方式创建一个大顶堆(从最后一个非叶结点开始往前循环调整堆) * 创建完成后,交换当前最后一个元素和第一个元素,这样最后一个元素就是当前最大的元素 * 堆大小减一,从堆顶开始调整使成为大顶堆 * 循环交换当前最后一个元素和第一个元素,堆大小减一,调整堆,直到堆的大小为1 */void HeapSort(vector<int>& vec) { int size = vec.size(); for (int i = size / 2 - 1; i >= 0; i--) Max_Heapify(vec, i, size); PrintVector(vec, "Create MAX-Heap", 0); for (int i = size - 1; i > 0; i--) { swap(vec[i], vec[0]); Max_Heapify(vec, 0, i); }}
一、堆是什么?
堆可以分为大顶堆和小顶堆。
大顶堆:每个结点的值都大于或等于其左右孩子结点的值。
小顶堆:每个结点的值都小于或等于其左右孩子结点的值。
如果是排序,求升序用大顶堆,求降序用小顶堆。
一般我们说 topK
问题,就可以用大顶堆或小顶堆来实现,
最大的 K 个:小顶堆
最小的 K 个:大顶堆
二、大顶堆的构建过程
大顶堆的构建过程就是从最后一个非叶子结点开始从下往上调整。
最后一个非叶子节点怎么找?这里我们用数组表示待排序序列,则最后一个非叶子结点的位置是:数组长度/2-1。假如数组长度为9,则最后一个非叶子结点位置是 9/2-1=3。
比较当前结点的值和左子树的值,如果当前节点小于左子树的值,就交换当前节点和左子树;
交换完后要检查左子树是否满足大顶堆的性质,不满足则重新调整子树结构;
再比较当前结点的值和右子树的值,如果当前节点小于右子树的值,就交换当前节点和右子树;
交换完后要检查右子树是否满足大顶堆的性质,不满足则重新调整子树结构;
无需交换调整的时候,则大顶堆构建完成
快速排序算法
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
/** * 快速排序 * 时间复杂度:O(nlogn) * 空间复杂度:O(1) * 设置第一个数为哨兵点,然后从第1个位置开始向右查找,直到找到一个比哨兵大的数,然后再右往左查找,直到找到一个比哨兵小的位置 * 然后交换两个位置的数,直到两个搜索Pointer相等,然后比较Pointer所指的位置上的数和哨兵的大小,转-->循环结束后的注释 */ void QuickSort_Exec(vector<int>& vec, int begin, int end) { if (begin >= end) return; int left = begin + 1, right = end; while (left < right) { while (left < right && vec[left] <= vec[begin]) left++; //从左往右寻找比哨兵位大的位置 while (left < right && vec[right] >= vec[begin]) right--; //从右往左寻找比哨兵位小的位置 if (left < right) swap(vec[left], vec[right]); //如果left位置上的数大于right的,则交换两个位置 } //循环结束后,left = right,left左边的数都比哨兵位小,右边的都比哨兵位大 //如果end - begin = 1,此时left = right = end = begin - 1,则会执行交换中的判断数组,小则不变,大则交换 //此时需要判断left位置上的数是否比哨兵位小,小则交换 //如果大,则--left,再进行交换 //新的区间为begin~left - 1和left + 1 ~ end swap(vec[begin], vec[left] < vec[begin] ? vec[left] : vec[--left]); QuickSort_Exec(vec, begin, left - 1); QuickSort_Exec(vec, left + 1, end);}void QuickSort(vector<int>& vec) { QuickSort_Exec(vec, 0, vec.size() - 1);}
树形选择排序
树形选择排序又称锦标赛排序(Tournament Sort),是一种按照锦标赛的思想进行选择排序的方法。首先对n个记录的关键字进行两两比较,然后在n/2个较小者之间再进行两两比较,如此重复,直至选出最小的记录为止。
0 1 2 3 4 5 67 8 9 10
搜索 - Search
深度优先搜索DFS
使用栈(后入先出)
思想
假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点,然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。 若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
宽度优先搜索BFS
启发式搜索
蚁群算法
遗传算法
计算几何 - Compute Geometry
凸包
图论 - Graph
哈夫曼编码
Huffuman Coding, 是可变字长编码的一种,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码
亦即带权路径最小的树, 权值最小的结点远离根结点, 权值越大的结点越靠近根结点
概念
带权路径长度最小的二叉树称为最优二叉树或哈夫曼树
路径长度:从树中一个结点到另一个结点之间的分支构成这两个结点间的路径,路径上的分支数目称作路径长度。
树的路径长度:从树根到每个结点的路径长度之和
完全二叉树的树的路径长度最小
结点的带权路径长度:从该结点到树根之间的路径长度于结点上权的乘积。
树的带权路径长度:数中所有叶子节点的带权路径长度之和,记作:
W
P
L
=
∑
k
=
1
n
w
k
l
k
WPL=\sum_{k=1}^{n}{w_kl_k}
WPL=∑k=1nwklk
::叶子结点带权,叶子节点的路径长度为从根到结点的长度
哈夫曼算法:构造哈夫曼二叉树
(1)根据给定的n个权值
{
w
1
,
w
2
,
.
.
.
,
w
n
}
\{w_1,w_2,...,w_n\}
{w1,w2,...,wn}构成n棵二叉树的集合
F
=
{
T
1
,
T
2
,
.
.
.
,
T
n
}
F=\{T_1,T_2,...,T_n\}
F={T1,T2,...,Tn},其中每棵二叉树Ti中只有一个带权为wi的根节点,其左右子树均为空
(2)在F中选取两棵根节点的权值最小的树作为左右子树构造一棵新的二叉树,且置新的二叉树的根节点的权值为其左右子树上根结点的权值之和
(3)在F中删除这两棵树,同时将新得到的二叉树加入F中
(4)重复(2)和(3),直到F只含一棵树为止
二叉树遍历
最短路径
Dijkstra算法
A*算法
SPFA算法
Bellman-Ford算法
floyd-warshall算法
Dijkstra算法
最小生成树
Prim算法
网络流
动态规划 - Dynamic Programming
动态规划
哈密顿图
递推
动态规划优化 - DP Optimize
优先队列
单调队列
四边形不等式
其他 - Other
随机化算法
递归
穷举搜索法
1. 穷举法概念
穷举法又称穷举搜索法,是一种在问题域的解空间中对所有可能的解穷举搜索,并根据条件选择最优解的方法的总称。
数学上也把穷举法称为枚举法,就是在一个由有限个元素构成的集合中,把所有元素一一枚举研究的方法。
穷举法一般用来找出符合条件的所有解,但是如果给出最优解的判断条件,穷举法也可以用于求解最优解问题。
2. 设计思路
使用穷举法解决问题,基本上就是以下两个步骤:
确定问题的解(或状态)的定义、解空间的范围以及正确解的判定条件;
根据解空间的特点来选择搜索策略,逐个检验解空间中的候选解是否正确;
2.1 解空间定义
解空间就是全部可能的候选解的一个约束范围,确定问题的解就在这个约束范围内,将搜索策略应用到这个约束范围就可以找到问题的解。
2.2 穷举解空间的策略
穷举解空间的策略就是搜索算法的设计策略,根据问题的类型,解空间的结构可能是线性表、集合、树或者图,对于不同类型的解空间,需要设计与之相适应的穷举搜索算法。
如果选择一种搜索策略,不带任何假设的穷举搜索,不管行不行,眉毛胡子一把抓,把所有可能的解都检查一遍,这样的搜索通常被称为“盲目搜索”。
与之对应的是利用某种策略或计算依据,由启发函数策动有目的的搜索行为,这些策略和依据通常能够加快算法的收敛速度,或者能够划定一个更小的、最有可能出现解的空间并在此空间上搜索,这样的搜索通常称为“启发性搜索”。
一般来说,为了加快算法的求解,通常会在搜索算法的执行过程中辅助一些剪枝算法,排除一些明显不可能是正确解的检验过程,来提高穷举的效率。
剪枝一个很形象的比喻,如果某一个状态节点确定不可能演化出结果,就应该停止从这个状态节点开始的搜索,相当于状态树上这一分枝就被剪掉了。
除了采用剪枝策略,还可以使用限制搜索深度的方法加快算法的收敛,但是限制搜索深度会导致无解,或错过最优解,通常只在特定的情况下使用,比如博弈树的搜索。
2.3 剪枝策略
对解空间穷举搜索时,如果有一些状态节点可以根据问题提供的信息明确地被判定为不可能演化出最优解,也就是说,从此节点开始遍历得到的子树,可能存在正确的解,但是肯定不是最优解,就可以跳过此状态节点的遍历,这将极大地提高算法的执行效率,这就是剪枝策略,应用剪枝策略的难点在于如何找到一个评价方法(估值函数)对状态节点进行评估。
贪心算法
一、基本概念
所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性(即某个状态以后的过程不会影响以前的状态,只与当前状态有关。)
所以,对所采用的贪心策略一定要仔细分析其是否满足无后效性。
二、贪心算法的基本思路
建立数学模型来描述问题
把求解的问题分成若干个子问题
对每个子问题求解,得到子问题的局部最优解
把子问题的解局部最优解合成原来问题的一个解
三、该算法存在的问题
不能保证求得的最后解是最佳的
不能用来求最大值或最小值的问题
只能求满足某些约束条件的可行解的范围
四、贪心算法适用的问题
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
实际上,贪心算法适用的情况很少。一般对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可以做出判断。
五、贪心选择性质
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,换句话说,当考虑做何种选择的时候,我们只考虑对当前问题最佳的选择而不考虑子问题的结果。这是贪心算法可行的第一个基本要素。贪心算法以迭代的方式作出相继的贪心选择,每作一次贪心选择就将所求问题简化为规模更小的子问题。对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所作的贪心选择最终导致问题的整体最优解。
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。
六、贪心算法的实现框架
从问题的某一初始解出发:
while (朝给定总目标前进一步)
{
利用可行的决策,求出可行解的一个解元素。
}
由所有解元素组合成问题的一个可行解;
分治法
迭代法
加密算法
回溯法
弦截法
迭代法
背包问题
背包问题(Knapsack problem)是一种组合优化的NP完全问题。
问题描述
给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
问题的名称来源于如何选择最合适的物品放置于给定背包中。
也可以将背包问题描述为决定性问题,即在总重量不超过W的前提下,总价值是否能达到V?
基础问题(01背包)
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
void Basic01Problem(void) {
int N = 0, V = 0, tempv, tempw;
cin >> N >> V;
vector<int> vol, val;
vol.push_back(0), val.push_back(0);
for (int i = 0; i < N; i++) {
cin >> tempv >> tempw;
vol.push_back(tempv);
val.push_back(tempw);
}
memset(dp, 0, sizeof(dp));
// the out cycle: the i-th item
// the inner cycle: the j volume
// with the i-th item and j volume, then compare
// [0] [i-1 item][j volome]
// [1] [i item][j volume] (initial is zero)
// [2] [i item][j - volume[i]] + value[i]
// after compare, the [i-item][j volume] represents the maximum value of the top i-items and the volume size-j
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= V; j++) {
dp[i][j] = max(dp[i - 1][j], j >= vol[i] ? dp[i][j - vol[i]] + val[i] : dp[i][j]);
}
}
cout << endl << dp[N][V] << endl;
}
/***** Optimization Edition *****/
八皇后问题
百鸡问题
二分法
kmp算法
遗传算法
矩阵乘法
Floyd算法
路由算法
ICP算法
约瑟夫环
约瑟夫问题
AVL树
红黑树
退火算法
并查集
线段树
左偏树
Treap
Trie树
RMQ
LCA
矩阵乘法
高斯消元
银行家算法
BACKUP
//鸡尾酒排序
void CocktailSort(vector<int>& vec, int asc = 1) {
int min, max, left = 0, right = vec.size() - 1;
while (left <= right) {
min = left, max = right;
for (int i = left; i < right; i++) {
if (vec[i] > vec[max]) max = i;
if (vec[i] < vec[min]) min = i;
}
swap(vec[left], vec[min]);
if (max == left) max = min;
swap(vec[right], vec[max]);
right--;
left++;
}
}
//出现的问题是中间的数不能得到有效的排序
数据结构
树
树的定义
树是n个结点的有限集
基本操作
基本操作 | 说明 | 操作结果 |
---|---|---|
InitTree(&T) | ||
DestoryTree(&T) | ||
CreateTree(&T, defintion) | ||
ClearTree(&T) | ||
TreeDepth(T) | ||
Root(T) | ||
Value(T, cur_e) | ||
Assign(T, cur_e, value) | ||
Parent(T, cur_e) | ||
LeftChild(T, cur_e) | ||
RightSibling(T, cur_e) | ||
InsertChild(&T, &p, i, c) | ||
DeleteChild(&T, &p, i) | ||
TraverseTree(T, visit()) | ||
二叉树
定义:每个结点至多只有两棵子树,并且,二叉树的子树只有左右之分,其次序不能任意颠倒
算法合集 Algorithm Collection
一些常用的不常用的,反正都是自己遇到的
Num0 基本的逻辑思维
实现nn次计算
计算nn
使用递归来写
//用i来记录运算到的地方
int calc(int n,int i)
{
if(i==n) return n;
return calc(n,++i)*n;
}
Num1 顺序表List
线性表 SequenceList
线性表相当于一个可变数组,其结构体如下:
#define LISTINTSIZE 100
#define LISTINCTEMENT 10
typedef struct{
ElemType* base; //存储线性表的基地址0
int length; //记录当前线性表中存储元素的个数
int listsize; //记录当前线性表最大的大小
}
Num2 图
BFS 和 DFS
Breath First Search 宽度优先遍历
Depth First Search 深度优先遍历
宽度优先遍历使用的是递归实现遍历,深度优先遍历使用队列实现进行遍历
floodfill 种子填充
什么是种子填充
种子填充算法假设在多边形或区域内部至少有一个像素是已知的。然后设法找到区域内所有其他像素,并对它们进行填充。区域可以用内部定义或边界定义。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bMirqXId-1636022427601)(https://i.loli.net/2020/05/10/3FXBJ2m4RbQcCvI.jpg)]
#include<stdio.h>
#include<string.h>
#define MAXN 5
char graph[MAXN][MAXN] = { {"****@"}, {"*@@*@"},{"*@**@"} ,
{"@@@*@"},{"@@**@"}};
int vis[MAXN][MAXN] = { 0 };
void PrintGraph(void);
void dfs(int x,int y,int id)
{
//当前位置超出范围
if (x < 0 || x >= MAXN || y < 0 || y >= MAXN) return;
//当前位置已经被访问或者不是@
if (graph[x][y] != '@' || vis[x][y]) return 0;
//给连图元素加上id
vis[x][y] = id;
graph[x][y] = '#';
printf("\n-----(%d,%d)-----\n", x+1, y+1);
PrintGraph();
for (int dr = -1; dr <= 1; dr++)
for (int dc = -1; dc <= 1; dc++)
if(dr != 0 || dc != 0) dfs(x + dr, y + dc, id);
return;
}
void PrintGraph(void)
{
int i, j;
printf("\n");
for (i = 0; i < MAXN; i++)
{
for (j = 0; j < MAXN; j++)
{
printf(" %c ", graph[i][j]);
}
printf("\n");
}
return;
}
int main()
{
int i, j, cnt=0;
PrintGraph();
for (i = 0; i < MAXN; i++)
for (j = 0; j < MAXN; j++)
if (vis[i][j] == 0 && graph[i][j] == '@') dfs(i, j, ++cnt);
printf("%d\n", cnt);
return 0;
}
另一种情况的代码
#include<stdio.h>#include<string.h>#define MAXN 100char dg[MAXN + 1][MAXN + 1];int vi[MAXN + 1][MAXN + 1] = { 0 };int stx, sty, endx, endy;int dfs(int row, int col, int x, int y, int step){ //先判断位置合法性 if (x < 0 || y < 0 || x >= row || y >= col || vi[x][y] == -1) return 0; //再判断有没有继续深度遍历的必要 if (vi[x][y] == 0 || vi[x][y] > step) vi[x][y] = step; else return 0; if (dg[x][y] == 'E') return 0; int mx, my; for (mx = -1; mx <= 1; mx++) { for (my = -1; my <= 1; my++) { //mx和my必须为一个0一个不是0 //都不是0 或 mx都为0 if ((mx && my) || (mx == 0 && my == 0)) continue; dfs(row, col, x + mx, y + my, step + 1); } } return 1;}int solve(int row, int col){ int ans; dfs(row, col, stx, sty, 0); ans = vi[endx][endy]; ans = ans ? ans : -1; return ans;}int main(){ int L, i, j, row, col, ans; scanf("%d", &L); //此处有回车 getchar(); while (L--) { memset(vi, 0, sizeof(vi)); //此处中间有空格 scanf("%d %d", &row, &col); //此处有回车 getchar(); for (i = 0; i < row; i++) { for (j = 0; j < col; j++) { dg[i][j] = getchar(); if (dg[i][j] == 'S') { stx = i; sty = j; } if (dg[i][j] == 'E') { endx = i; endy = j; } if (dg[i][j] == '#') { vi[i][j] = -1; } } //此处有回车 getchar(); } ans = solve(row, col); printf("%d\n", ans); }}
使用BFS计算最短路径
注:
strchr
函数
函数原型为:char *strchr(const char *str, int c)
功能:在参数 str 所指向的字符串中搜索第一次出现字符 c(一个无符号字符)的位置。
库:strchr函数包含在C 标准库<string.h>
中。