插入排序
插入排序分为直接插入排序和希尔排序两种,其核心思想都是将待排值插入到指定位置来实现数据的排序
直接插入排序
把待排元素插入到有序的序列中,最终得到有序序列
// C语言代码
void InsertSort(int* a,int n)
{
// 假设有n个元素,最后一趟排序只需把最后一个元素插入前面的n-1个元素的有序序列中即可
for (int i = 0; i < n - 1; ++i)
{
int end = i; // 第i趟(从第0趟开始算起)可认为前i个元素有序,把后边的元素插入前i个元素的有序序列
int tmp = a[end + 1]; // 第i趟排序时,前i个元素有序,把第i+1个元素保存起来
while (end >= 0)
{
if (tmp < a[end]) // 判断第i+1个元素(保存在tmp中)是否小于第i个元素(有序序列的最后一个元素)
{
a[end + 1] = a[end]; // 若小于就把当前元素后挪
end--; // 有序序列的尾向前移,直到比较完有序序列的所有元素
}
else
{
break; // 只要遇到不符合条件的元素(当前元素<tmp中的元素),则直接跳出循环,因为前面的序列已经有序
}
}
a[end + 1] = tmp; // 把保存在tmp中的元素放到
}
}
void TestInsertSort()
{
int a[] = { 0,8,5,6,9,7,4,2,3,1 };
InsertSort(a, sizeof(a) / sizeof(a[0]));
PrintArray(a, sizeof(a) / sizeof(a[0]));
}
// C++代码
void InsertSort(vector<int>& v, int n)
{
// 假设有n个元素,最后一趟排序只需把最后一个元素插入前面的n-1个元素的有序序列中即可
for (int i = 0; i < n - 1; ++i)
{
int j = i; // 设前i个元素有序
int tmp = v[j + 1]; // 第i+1个元素为待排元素,存入tmp中
while (j >= 0 && tmp < v[j])
{
v[j + 1] = v[j]; // 待排元素小于第j个元素时,把第j个元素后挪一下
--j; // 再把j--,继续比较待排元素tmp是否小于第j个元素,直到j<=0
}
v[j + 1] = tmp; // 最后当第j个元素<待排元素时,再把待排元素放到第j+1个位置
}
}
void InsertSort(vector<int>& v, int n)
{
// 设i位置之前的元素有序,从i位置开始插入到有序序列中
for (int i = 1; i < n; ++i)
{
int j = i; // 设置待插元素的目标待插位置
int tmp = v[i]; // 标记待插元素
while (j > 0 && tmp < v[j-1])
{
v[j] = v[j - 1]; // 当待插元素<目标待插位置之前的元素,将目标待插位置之前的元素后挪一位
j = j - 1; // 再把待插位置前挪
}
v[j] = tmp; // 最后将待插元素插入待插位置
}
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
希尔排序
希尔排序法的基本思想是:先选定一个间隔,把待排序文件中所有记录分组,所有间隔为gap的记录分在同一组内,并对每一组内的记录进行排序。然后缩小gap,重复上述分组和排序的工作。当gap到达=1时,所有记录在统一组内排好序。
void ShellSort(vector<int>& v, int n)
{
for (int gap = n / 2; gap > 0; gap /= 2)
{
for (int i = 0; i < n - gap; ++i)
{
int j = i; // 设前i个元素有序
int tmp = v[j + gap]; // 第j+gap个元素为待排元素,存入tmp中
while (j >= 0 && tmp < v[j])
{
v[j + gap] = v[j]; // 待排元素小于第j个元素时,把第j个元素后挪gap个位置
j -= gap; // 再把j-gap,继续比较待排元素tmp是否小于第j个元素,直到j<=0
}
v[j + gap] = tmp; // 最后当第j个元素<待排元素时,再把待排元素放到第j+gap个位置
}
}
}
希尔排序的特性总结:
- 希尔排序是对直接插入排序的优化。
- 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就
会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。 - 希尔排序的时间复杂度不好计算,需要进行推导,推导出来平均时间复杂度: O(N1.3—N2)
- 稳定性:不稳定
选择排序
选择排序主要分为简单选择排序和堆排序,它们都是基于选择的思想实现的。每一次从待排序的数据元素中选出最小或最大的一个元素,存放在序列的指定位置,直到全部待排序的数据元素排完 。
简单选择排序
每次从待排序列中选择最小的数,存放到已排序序列的后面
void SelectSort(vector<int>& v, int n)
{
for (int i = 0; i < n - 1; ++i)
{
// 设第i个数是最小值
int min = i;
// 每次遍历前,i位置之前的值已经有序且都小于剩下的值,每次从剩余序列中找出最小值
// 从第i+1个值遍历到最后一个值
for (int j = i + 1; j < n; ++j)
{
// 若遇到比设定的最小值更小的值,则将最小值设定为这个值
if (v[j] < v[min])
{
min = j;
}
}
// 找出这趟遍历后遇到的最小值,与i位置元素交换
if (min != i)
{
swap(v[i], v[min]);
}
}
}
简单选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
void AdjustDown(vector<int>& v, int n, int root)
{
int parent = root;
int child = parent * 2 + 1; // 左孩子
while (child < n)
{
// 选出左右孩子中大的那个
if (child + 1 < n && v[child + 1] > v[child]) // 若当前根的右孩子存在,且右孩子>左孩子
{
++child; // 使右孩子称为child
}
// 如果孩子>父亲,则交换,继续向下调整
// 如果孩子<父亲,则调整结束
if (v[child] > v[parent])
{
swap(v[child], v[parent]);
// 交换父子之后,可能引发子树变为不满足大堆的状态,此时需要再对子树进行调整
parent = child; // 将子树根设为父亲
child = parent * 2 + 1; // 重新计算孩子
}
else
{
break;
}
}
}
void HeapSort(vector<int>& v, int n)
{
// 排升序,建大堆
for (int i = n / 2 - 1; i >= 0; --i)
{
// 向下调整
AdjustDown(v, n, i);
}
int end = n - 1;
while (end)
{
// 把当前堆顶最大的数依次换到最后
swap(v[0], v[end]);
// 调堆选出剩下的数当中最大的换到堆顶
AdjustDown(v, end, 0);
--end;
}
}
堆排序的特性总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
交换排序
所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序
将相邻的元素两两比较,若前面的>后面的,就进行交换;在一趟排序完成后,可以让最大的值交换到最后面。
void BubbleSort(vector<int>& v, int n)
{
// end为乱序序列末尾下标,每排一趟,乱序序列元素-1
for (int end = n - 1; end > 0; --end)
{
int flag = false; // 设置一个标记,将其置为false
// 排一趟,排到只剩一个元素时不用再排,因此条件控制为i<n-1
for (int i = 0; i < n - 1; ++i)
{
if (v[i] > v[i + 1])
{
swap(v[i], v[i + 1]);
flag = true; // 只要某趟排序发生了交换,标记就会变为true
}
}
if (flag == false) return; // 若这趟排序完后,标记没变,说明没有发生交换,也就说明此时已经有序
}
}
冒泡排序的特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
快速排序
快速排序(Quicksort)是对冒泡排序的一种改进。
快速排序由Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
// 三数取中
int GetMidIndex(vector<int>& v, int begin, int end)
{
int mid = (begin + end) >> 1;
if (v[begin] < v[end])
{
if (v[begin] < v[mid])
{
if (v[mid] < v[end])
{
return mid;
}
else
{
return end;
}
}
return begin;
}
else // begin>end
{
if (v[begin] > v[mid])
{
if (v[mid] > v[end])
{
return mid;
}
else
{
end;
}
}
return begin;
}
}
// 左右指针法
int LRPointer(vector<int>& v, int begin, int end)
{
// 先三数取中,再将中间值和右边元素交换,避免有序情况下的巨大空间开销
int mid = GetMidIndex(v, begin, end);
swap(v[mid], v[end]);
// 右边做key,左边先走(若左边做key,则右边先走)
int key = v[end];
int key_pos = end; // 记录key的下标
while (begin < end)
{
// begin找比key大的元素,
while (begin < end && v[begin] <= key) // 使用<=是因为若有相同元素,则会陷入死循环
{
++begin;
}
// end找比key小的元素
while (begin < end && v[end] >= key)
{
--end;
}
swap(v[begin], v[end]); // begin位置元素比key大,end位置元素比key小,交换
}
swap(v[begin], v[key_pos]); // 最后将相遇位置与key下标元素交换
return begin; // 返回相遇的位置下标
}
// 挖坑法
int DigHole(vector<int>& v, int begin, int end)
{
// 先三数取中,再将中间值和右边元素交换,避免有序情况下的巨大空间开销
int mid = GetMidIndex(v, begin, end);
swap(v[mid], v[end]);
int key = v[end]; // 将end位置元素设为基准,将end位置置为坑
while (begin < end)
{
// begin找比key大的元素
while (begin < end && v[begin] <= key) // 使用<=是因为若有相同元素,则会陷入死循环
{
++begin;
}
// 用begin找到的比基准大的元素填坑,并将begin置为新的坑
v[end] = v[begin];
// end找比key小的元素
while (begin < end && v[end] >= key)
{
--end;
}
// 用end找到的比基准小的元素填坑,并将end置为新的坑
v[begin] = v[end];
}
v[begin] = key; // 最后将相遇位置置为基准值
return begin; // 返回相遇的位置下标
}
// 快慢指针法
int FastSlowPtr(vector<int>& v, int begin, int end)
{
// 先三数取中,再将中间值和右边元素交换,避免有序情况下的巨大空间开销
int mid = GetMidIndex(v, begin, end);
swap(v[mid], v[end]);
int key = v[end]; // 设定基准值
int fast = begin; // 设定快指针为第一个元素的下标
int slow = begin - 1; // 设定慢指针为第一个元素的前一个下标
while (fast < end) // 快指针遇到key的位置就结束
{
// 若快指针所在元素<=基准值,++慢指针
if (v[fast] <= key && ++slow != fast)
{
// 若慢指针++后两指针不在同一位置,就交换两个指针所在位置的元素
swap(v[fast], v[slow]);
}
++fast; // 若快指针所在位置元素>基准值 或++慢指针后,两指针会到同一位置 则只++快指针
}
++slow; // 最后++慢指针下标
swap(v[slow], v[end]); // 并且交换慢指针下标处和基准值位置的元素
return slow; // 返回基准值下标
}
void RecQuickSort(vector<int>& v, int begin, int end) // 递归快排
{
// 递归出口:begin=end时,区间只有一个值
if (begin >= end) return;
// [begin,pos-1] [pos] [pos+1,end]
//int pos = LRPointer(v, begin, end); // 左右指针法
//int pos = DigHole(v, begin, end); // 挖坑法
int pos = FastSlowPtr(v, begin, end); // 快慢指针法
RecQuickSort(v, begin, pos - 1);
RecQuickSort(v, pos + 1, end);
}
void NRecQuickSort(vector<int>& v, int begin, int end) // 非递归快排
{
stack<int> s;
s.push(begin);
s.push(end);
while (!s.empty())
{
int right = s.top();
s.pop();
int left = s.top();
s.pop();
int pos = FastSlowPtr(v, left, right); // 快慢指针法
// 此时v被pos一分为二: [left,pos-1] pos [pos+1,right]
if (left < pos - 1)
{
s.push(left);
s.push(pos - 1);
}
if (pos + 1 < right)
{
s.push(pos + 1);
s.push(right);
}
}
}
void QuickSort(vector<int>& v, int n)
{
int begin = 0;
int end = n - 1;
//return RecQuickSort(v, begin, end); // 递归快排
return NRecQuickSort(v, begin, end); // 非递归快排
}
快速排序的特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
void _MergeSort(vector<int>& v, int begin, int end, vector<int>& tmp)
{
// 递归出口
if (begin >= end)
{
return;
}
// 先划分
int mid = (begin + end) >> 1;
// [begin,mid][mid+1,end]
_MergeSort(v, begin, mid, tmp);
_MergeSort(v, mid + 1, end, tmp);
// 归并两段有序区间
int begin1 = begin, end1 = mid;
int begin2 = mid + 1, end2 = end;
int index = begin; // 辅助空间tmp的下标
while (begin1 <= end1 && begin2 <= end2) // 两段区间都没到尾时继续,其中一段到尾就结束
{
// 比较第一段和第二段的开始元素,哪个小就存入临时数组tmp中
if (v[begin1] < v[begin2])
{
tmp[index] = v[begin1]; // 若第一段的开头元素小,就把第一段的开头元素存入临时数组
index++; // 再把第一段开头下标和临时数组下标都向后挪
begin1++;
}
else
{
tmp[index] = v[begin2]; // 若第二段的开头元素小,就把第二段的开头元素存入临时数组
index++; // 再把第二段开头下标和临时数组下标都向后挪
begin2++;
}
}
// 第二段已经到尾
if (begin1 <= end1)
{
while (begin1 <= end1) // 当第一段没到尾,把剩下元素都挪入临时数组
{
tmp[index] = v[begin1];
index++;
begin1++;
}
}
// 第一段已经到尾
else
{
while (begin2 <= end2) // 当第二段没到尾,把剩下元素都挪入临时数组
{
tmp[index] = v[begin2];
index++;
begin2++;
}
}
for (int i = begin; i <= end; ++i) // 把归并后存入临时数组的有序序列段挪入原数组中
{
v[i] = tmp[i];
}
}
void MergeSort(vector<int>& v, int n)
{
vector<int> tmp;
tmp.resize(n);
int begin = 0;
int end = n - 1;
_MergeSort(v, begin, end, tmp);
}
归并排序的特性总结:
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
void CountSort(vector<int>& v, int n)
{
// 定义一个map
map<int, int> m;
// 把n个数存进map中
while (n)
{
// 遍历数组v
for (auto& e : v)
{
// 定义一个map的迭代器,找数组v中的元素是否在map中
auto key = m.find(e);
// 若在map中找到了某个数,说明这个数已经在map中了
if (key != m.end())
{
key->second++; // 则将map中这个数的second++
n--; // --剩余所需插入的数据个数n
}
// 若在map中没找到某个数,说明这个数不在map中
else
{
m.insert(make_pair(e, 1)); // 则将这个数插入map中,将其first设为1
n--; // --剩余所需插入的数据个数n
}
}
}
// 清空原数组
v.clear();
// 遍历map
for (auto k : m)
{
while (k.second) // 当map中某个值的second不为0
{
v.push_back(k.first); // 将这个值尾插入数组中
k.second--; // 并且将second--
}
}
}
计数排序的特性总结:
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定
STL中的sort
stl中的sort主要使用快速排序的方法,但当快速排序的排序区间小到一定区间时,会转而使用插入排序;另外,在快速排序递归层数过高时,也会使用堆排序来实现