概述
常见的排序算法可以分为两大类:
非线性时间比较类: 通过比较决定元素间的相对次序,最小时间复杂度为 O(nlogn)
线性时间非比较类:不通过比较决定元素间的相对次数,线性时间运行
算法复杂度
1. 冒泡排序
比较相邻元素的相对大小,如果反序则交换,如果是从小到大排序,那么第一次扫描可以确定最大的元素,第二次扫描确定次大元素,以此类推,需要n-1次扫描。
代码:
void bubbleSort(std::vector<int> &v)
{
int size = v.size();
// n-1次扫描
for(int i=0; i<size-1; i++)
{
// 最多到size-i-1位置
for(int j=0;j<size-i-1;j++)
{
if(v[j]>v[j+1]) swap(v[j], v[j+1]);
}
}
}
2. 快速排序
每次从序列中找一个base,然后遍历数组中剩下的元素,将比base小的元素放在base的左边,比base大的元素放在base的右边。
过程中通过一个left以及right指针,分别找到左边第一个大于base的元素以及右边第一个小于base的元素,然后交换两者,left++, right–,知道left=right。然后将left元素与base进行交换。
void quickSort(vector<int> & v, int left, int right)
{
if(left >= right) return ;
int l = left;
int r = right;
srand((unsigned)time(NULL));
int idx = rand()%(right-left+1) + left;
swap(v[left], v[idx]);
int base = v[left];
while(left < right)
{
while(right > left && v[right] >= base) right--;
while(right > left && v[left] <= base) left++;
swap(v[left], v[right]);
}
swap(v[l], v[left]);
quickSort(v, 0, left-1);
quickSort(v, left+1, r);
}
3. 插入排序
基本思想如下:
对于A[n], 假定 A[0] - A[n-1]之间的元素是已经排序的,那么只需遍历A[0,n-1]找到第一个大于A[n]的元素,将A[n]放在这个元素前面(实现过程中比较从j=n->1比较A[j]与A[j-1]的大小)。
代码:
i从1开始到size-1, j从i开始寻找合适位置插入
void insertSort(vector<int> &v)
{
int size = v.size();
for(int i=1;i<size;i++)
{
int j = i;
int tmp = v[i];
while(j-1>=0 && tmp < v[j-1])
{
v[j] = v[j-1];
j--;
}
v[j] = tmp;
}
}
4. 希尔排序
简单的插入排序需要一步步对元素进行比较、移动、插入,尤其是极端倒序情况。希尔排序采用跳跃式分组的策略,通过增量gap对元素进行分组,然后对分组内的元素进行插入排序,然后逐步缩小增量直到gap=1。
希尔排序通过分组策略然后组内插入排序,使得数组宏观基本有序,相比插入排序不会 涉及过多的数据移动。
代码:
void shellsort(vector<int> & v)
{
int size = v.size();
// 增量gap从size/2到1
for(int gap = size/2;gap>=1;gap--)
{
// 每个组采用插入排序
for(int i=gap;i<size;i++)
{
// 保存要插入的元素信息
int temp = v[i];
int j = i;
// 寻找合适的位置进行插入
while(j-gap>=0 && temp < v[j-gap])
{
v[j] = v[j-gap];
j-=gap;
}
v[j] = temp;
}
}
}
注意虽然希尔排序使用到插入排序实现,但是在组内进行插入排序的时候相同元素可能顺序会调换,因此是不稳定的
5.选择排序
选择排序原理如下:初始状态下整个数组无序,从其中找到最小的元素放在数组第一个位置作为已排序。然后对剩下未排序元素继续寻找最小元素放在已排序元素的末尾。
与冒泡排序相比:冒泡排序每次扫描都需要交换相邻的逆序对,然后确定最大(最小)元素放到合适的位置。选择排序每次扫描直接记录最大(最小)元素的位置,然后只需要一次交换放到合适的位置。
void selectSort(vector<int> & v)
{
int size = v.size();
for(int i=0;i<size-1;i++)
{
int temp = i;
for(int j=i+1;j<size;j++)
{
if(v[temp] > v[j]) temp = j;
}
swap(v[temp], v[i]);
}
}
6. 归并排序
归并排序采用分治思想,将问题分为一些小问题然后递归求解,而治的阶段将小问题的答案结合在一起求解。
归并排序必须引入新的空间来存储原数组两部分有序子数组的合并结果,然后在拷贝回原数组之中。
void Merge(vector<int> & v, int left, int mid, int right)
{
if(left >= right) return ;
int size = right-left+1;
int temp[size];
int l = left, r = mid+1;
for(int i=0;i<size;i++)
{
// 条件一: r=right+1
if(r==right+1 || l<=mid && v[l] < v[r])
{
temp[i] = v[l++];
}
else {
temp[i] = v[r++];
}
}
for(int i=0;i<size;i++)
{
v[left+i] = temp[i];
}
}
void mergeSort(vector<int> &v, int left, int right)
{
if(left>=right) return ;
int mid = left + (right-left)/2;
// 先解决两个子问题 然后两个子问题的解进行合并
mergeSort(v, left, mid);
mergeSort(v, mid+1, right);
Merge(v, left, mid, right);
}
7. 堆排序
堆的本质是一颗完全二叉树,两个性质:
- 堆的每个父节点(大于 或者小于)子节点
- 堆的左右子树同样也是堆
由于是一颗完全二叉树,可以按照层序遍历顺序编号,使用数组表示堆, i节点的父节点的下标为(i-1)/2
, 左右孩子的下标为2*i + 1
与2*i+2
。
堆排序的步骤如下:
- 建堆(升序建立最大堆), 建堆过程中从最后一个非叶节点到根节点调整堆。
- 循环n-1次,每次将堆顶元素与未排序的最后一个元素(从n-1到1)进行交换,然后从根元素向下调整。
调整堆:
void adjust(vector<int> & v, int size, int idx)
{
int lchild = idx*2+1;
int rchild = idx*2+2;
int maxidx = idx;
if(lchild < size && v[lchild] > v[maxidx]) maxidx = lchild;
if(rchild < size && v[rchild] > v[maxidx]) maxidx = rchild;
if(maxidx != idx)
{
swap(v[idx], v[maxidx]);
adjust(v, size, maxidx);
}
}
左右孩子计算下标: idx*2 + 1(2)
获取非叶节点与左右孩子的最大值,如果不是该非叶节点需要进行交换,然后切记: 还需要对与之交换的那个节点进行调整
建立堆并实现排序:
首先从最后一个非叶节点往根节点开始遍历调整,建立堆。
然后将堆顶元素与无序数组的最后一个元素进行交换,(一共需要交换size-1次,每次都能在原数组末尾确定当前最大元素)每次交换完成之后需要重新从根节点开始调整,但是调整范围只涉及size-1个元素。
void heapSort(vector<int> & v)
{
int size = v.size();
// last no-leaf node
for(int i=(size-2)/2;i>=0;i--)
{
adjust(v, size, i);
}
// n-1 swap
for(int i=size-1;i>=1;i--)
{
swap(v[i], v[0]);
// dijian
adjust(v, i, 0);
}
}
8. 基数排序
基数排序与其他七种排序算法不同,不需要进行关键字比较。
将所有的数值按照位数从低到高的顺序计算当前位大小,然后放入对应的桶中,那么对于当前位来说,所有桶中元素的顺序即为当前位的大小顺序,即当前位有序。通过不断将位数增加,使得每一位有序,那么最终结果有序
代码:
// 统计数组内的最大位数
int getBits(vector<int> v)
{
int bits = 0;
for(int i=0; i<v.size(); i++)
{
int temp = v[i];
int cnt = 0;
while(temp)
{
temp/=10;
cnt++;
}
bits = max(bits, cnt);
}
return bits;
}
// 对每个位使用一个桶(vector)统计
void radixSort(vector<int> & v)
{
int bits = getBits(v);
vector<vector<int> > vc(10);
for(int i=0;i<bits;i++)
{
for(auto item : v)
{
int idx = item/((int)pow(10, i));
idx %=10;
vc[idx].push_back(item);
}
int index = 0;
for(auto & bucket: vc)
{
for(auto & item : bucket)
{
v[index++] = item;
}
bucket.clear();//
}
}
}
9. 桶排序
通排序思想是假定数据在 [ m i n , m a x ] [min, max] [min,max]之间均匀分布,将区间等分为n份,每份对应一个桶。将数据添加到对应的桶中,然后桶内分别进行排序。最终,桶相对有序,桶内元素有序,整体有序
极端情况,直接令每个桶的大小为1.