之前学习了排序算法,现总结如下:
1.快速排序
该算法采用分治的思想,主要步骤如下所示:
1)选取一个基准数pivot(一般选取第一个数,如果是随机选取的,将该值与第一个值进行交换)
2)遍历数组,将所有比pivot小的值放在pivot左边,将比pivot大的值放在pivot的右边,该过程称为分区partition,此过程借助双指针实现
3)根据pivot将数组分为两部分,重复上述过程
int Partition(vector<int>& v, int left, int right)
{
int pivot = v[left]; // 确定基准数
while (left < right)
{
// 如果值大于等于pivot,right前移
while (left < right && v[right] >= pivot)
{
right--;
}
if (left < right)
{
v[left] = v[right];
left++;
}
// 因为left前的数保证了值小于pivot,因此left前移
while (left < right && v[left] <= pivot)
{
left++;
}
if (left < right)
{
v[right] = v[left];
right--;
}
}
// left == right
v[left] = pivot;
return left;
}
void QuickSort(vector<int>& v,int left,int right)
{
if (left >= right) return;
int mid = Partition(v, left, right);
QuickSort(v, left, mid - 1);
QuickSort(v, mid + 1, right);
}
时间复杂度:平均情况下为O(nlogn),其中N为数据长度,递归调用的深度为O(logn)。时间复杂度最坏的情况为O(n2),例数组有序时每次都选择最左侧或者最右侧为基准数时。
空间复杂度:O(logn),递归调用所需要的栈空间,最差为O(n)
稳定性:该算法是不稳定的。稳定指排序前a==b,且a在b的前面,则排序后a仍在b的前面。以arr = [1,2,2,3]为例,假设选择arr[2]为基准数,将大于等于基准数的元素放到基准数的右侧,则arr[1]会放到arr[2]的右侧,同理,选择arr[1]为基准数,将小于等于基准数的元素放到基准数的左侧,则数组中的两个2也非原序。本质上是因为不断交换left和right指针的元素,导致算法不稳定。
2.冒泡排序
每次遍历时,比较相邻元素,不符合排序准则即交换,直至将当前最大(小)元素放置到合适的位置。
void BubbleSort(vector<int>& v)
{
for (int i = 0; i < v.size()-1; i++) // 遍历次数
{
for (int j = 0; j < v.size()-1 - i; j++)// 当前遍历范围下的最大值
{
if (v[j] > v[j + 1])
{
int temp = v[j];
v[j] = v[j + 1];
v[j + 1] = temp;
}
}
}
}
时间复杂度:O(n2)
空间复杂度:O(1)
稳定性:稳定,只交换相邻元素,不改变序列原序。
3.选择排序
初始时,在整个序列中查找最小(大)值,放在元素的初始位置作为已排序元素,然后再遍历查找当前未排序元素的最小(大)值,将其放到已排序元素中,查找过程只进行一次交换。
void SelectSort(vector<int>& v)
{
for (int i = 0; i < v.size() - 1; i++) // 已排序元素个数
{
int minIndex = i;
for (int j = i+1; j < v.size(); j++)
{
if (v[j] < v[minIndex])
{
minIndex = j;
}
}
if (minIndex != i)
{
int temp = v[i];
v[i] = v[minIndex];
v[minIndex] = temp;
}
}
}
时间复杂度:O(n2)
空间复杂度:O(1)
稳定性:不稳定,由于交换导致原序会发生改变。
4.插入排序
对于第n个元素,假设前面n-1个元素都已经排好序,则将当前元素插入到有序序列中的合适的位置。
void InsertSort(vector<int>& v)
{
for (int i = 1; i < v.size(); i++) // 当前处理的元素
{
for (int j = i; j > 0; j--) // 遍历已排序序列,将当前元素插入
{
if (v[j] < v[j - 1])
{
int temp = v[j];
v[j] = v[j - 1];
v[j - 1] = temp;
}
}
}
}
时间复杂度: O(n2),最差的情况为原序列为降序(升序)序列,但需要返回升序(降序)序列,最好的情况为原序列为降序(升序)序列,但需要返回降序(升序)序列,
空间复杂度: O(1)
稳定性:稳定,按照原序列顺序进行操作。
5.希尔排序
该算法是对直接插入排序的一种改进算法,将记录按照下标的一定增量分组,对每组使用直接插入排序,随着增量的减小,每组包含的元素增大,直至增量为1时,排序完成,因此也称为缩小增量排序。一般情况下选择增量为{n/2,(n/2)/2,……},称为希尔增量。
void ShellSort(vector<int>& v)
{
for (int step = v.size() / 2; step >= 1; step /= 2) // 希尔增量
{
for (int k = 0; k < step; k++) // 多少组
{
for (int i = k+step; i < v.size(); i += step) // 对每组进行插入排序
{
for (int j = i; j > k; j -= step)
{
if (v[j] < v[j - step])
{
int temp = v[j];
v[j] = v[j - step];
v[j - step] = temp;
}
}
}
}
}
}
时间复杂度:小于等于O(n2)
空间复杂度:O(1)
稳定性:不稳定,分组插入排序时会导致原序发生改变
6.归并排序
该算法采用分治的思想,分是指将大问题拆分为小问题,治是指将小问题的结果进行合并,分为递归和迭代两种实现方法。
递归:从上向下
// 合并
void Merge(vector<int>& v, int left, int mid, int right)
{
int len = right - left + 1;
vector<int> temp(len); // 生成新的数组保存排序结果
int i = left, j = mid + 1, k = 0; // 三个序列的开始
while (i <= mid && j <= right) // 当两个序列都有值时
{
temp[k++] = v[i] < v[j] ? v[i++] : v[j++]; // 先赋值,再移动下标位置
}
while (i <= mid) // 将还没有遍历完的序列赋值给temp
{
temp[k++] = v[i++];
}
while (j <= right)
{
temp[k++] = v[j++];
}
for(int index = 0;index<len;index++)
{
v[left+index] = temp[index];
}
}
void MergeSort01(vector<int>& v,int left, int right)
{
if (left < right)
{
int mid = left + (right - left) / 2;
MergeSort01(v, left, mid);
MergeSort01(v, mid + 1, right);
Merge(v, left, mid, right);
}
}
迭代:从下到上
void MergeSort02(vector<int>& v)
{
int len = v.size();
for (int step = 1; step <= len; step <<= 1) // 步长
{
int offset = step + step;
for (int i = 0; i < v.size(); i += offset) // 分组
{
Merge(v, i, min(i + step-1,len-1), min(i + offset-1,len-1));
}
}
}
时间复杂度:O(nlogn)
空间复杂度:O(n),保存排序后元素
稳定性:稳定,都是相邻的元素或组进行操作
7.堆排序
利用堆的特性进行排序,堆是近似一种完全二叉树的结构,利用数组进行保存,对于节点i,其父节点为(i-1)/2,其左孩子为2i+1,右孩子为2i+2,且其父节点对应的值大于(小于)其子节点对应的值。
算法过程:
1)根据给定序列建堆,升序建立大根堆,降序建立小根堆
2)将堆顶元素与最后一个元素进行交换
3)交换后堆顶元素可能违反堆的性质,因此从堆顶向下调整新堆。重复2)和3)直至排序完成。
void AdjustHeap(vector<int>& v,int index,int len)
{
int child = 2 * index + 1; // 左孩子
while (child < len)
{
if (child+1<len && v[child] < v[child + 1]) child++; // 选择左右孩子中较大的那个
if (v[index] >= v[child]) break; // 父节点大于等于孩子节点中较大的值,即符合堆性质,停止调整
swap(v[index], v[child]); // 交换父节点和孩子节点
index = child; // 以孩子节点为当前节点继续向下调整
child = 2 * index + 1;
}
}
void MakeHeap(vector<int>& v)
{
// 从最后一个父节点进行建堆
for (int i = v.size()/2-1; i >= 0; i--)
{
AdjustHeap(v, i, v.size());
}
}
void HeapSort(vector<int>& v)
{
// 建堆
MakeHeap(v);
for (int i = v.size() - 1; i >= 0; i--)
{
// 交换
swap(v[0], v[i]);
// 调整新堆
AdjustHeap(v, 0,i);
}
}
时间复杂度:O(nlogn),交换后每个节点都向下调整,最多为logn层
空间复杂度:O(1)
稳定性:不稳定,因为发生了非相邻元素的交换
8.计数排序
该算法不是基于比较的排序算法,该算法是根据给定序列的元素范围生成数组,并将序列中元素个数记录在数组中,根据数组的统计结果来进行排列,即通过增加空间来降低时间复杂度。
void CountSort(vector<int>& v)
{
// 求解待排序元素中的最大值和最小值
int MaxValue = v[0];
int MinValue = v[0];
int len = v.size();
for (int i = 1; i < len; i++)
{
if (v[i] < MinValue)
{
MinValue = v[i];
}
else if(v[i]>MaxValue)
{
MaxValue = v[i];
}
}
// 生成新的数组,并将对待排序元素进行计数
vector<int> count(MaxValue - MinValue + 1);
for (int i = 0; i < len; i++)
{
count[v[i] - MinValue]++;
}
// 遍历count数组进行排序
int k = 0;
for (int i = 0; i < MaxValue - MinValue + 1; i++)
{
while (count[i] > 0)
{
v[k++] = i + MinValue;
count[i]--;
}
}
}
时间复杂度:O(n+k),输入的元素种类数为k,元素个数为n
空间复杂度:O(k),生成数组保存每个元素的个数
稳定性:稳定
9.桶排序
桶排序对计数排序进行升级,将待排序序列平均分配到有限个桶内,再对每个桶内的数据进行排序,再将每个桶内的排序结果拼接起来。
void BucketSort(vector<int>& v,int bucketSize)
{
if (v.size() <= 1) return;
vector<vector<int>> bucket(bucketSize);
// 求解序列中的最大值最小值
int minValue = v[0];
int maxValue = v[0];
for (int i = 0; i < v.size(); i++)
{
if (v[i] < minValue) minValue = v[i];
else if (v[i] > maxValue) maxValue = v[i];
}
// 将序列中的数分配到桶中
int bucketCount = (maxValue - minValue) / bucketSize+1; // 每个桶内存储的数目
for (int i = 0; i < v.size(); i++)
{
bucket[(v[i] - minValue) / bucketCount].push_back(v[i]);
}
v.resize(0);
for (int i = 0; i < bucketSize; i++)
{
if (bucket[i].size() != 0)
{
sort(bucket[i].begin(), bucket[i].end());
for (int j = 0; j < bucket[i].size(); j++)
{
v.push_back(bucket[i][j]);
}
}
}
}
可以看出桶数量越多,每个桶内数据越少,排序所用时间越少,但是相应所需空间增大。
10.基数排序
该算法按照关键字各位的值对n个元素进行若干趟分配和收集从而完成排序,主要分为两种,第一种,从低位到高位进行排序(LSD),第二种,从高位到低位(MSD)。
LSD:
int getMaxDigit(vector<int>& v)
{
int maxValue = v[0];
for (int i = 1; i < v.size(); i++)
{
if (v[i] > maxValue) maxValue = v[i];
}
int digit = 1;
while (maxValue / 10)
{
digit++;
maxValue /= 10;
}
return digit;
}
// 提取key中digit位的值
int getValue(int key,int digit)
{
while (digit != 1)
{
key /= 10;
digit--;
}
return key % 10;
}
// LSD
void RadixSort_LSD(vector<int>& v)
{
if (v.size() <= 1) return;
int len = v.size();
// 当前序列中的最大位数
int digit = getMaxDigit(v);
// 从低位到高位:分配,收集
for (int k = 1; k <= digit; k++)
{
vector<int> temp(len);
vector<int> count(10, 0);
for (int i = 0; i < len; i++)
{
count[getValue(v[i], k)]++;
}
// 累加
for (int i = 1; i < 10; i++)
{
count[i] += count[i - 1];
}
// 重新排序,从后向前,保持稳定性
for (int i = len-1; i >=0; i--)
{
int cur = getValue(v[i], k);
int index = count[cur];
count[cur]--;
temp[index - 1] = v[i];
}
for (int i = 0; i < len; i++)
{
v[i] = temp[i];
}
}
}
MSD:
void RadixSort_MSD(vector<int>& v, int digit)
{
int len = v.size();
vector<vector<int>> Radix(10);
//分配
if (len > 1 && digit >= 1)
{
for (int i = 0; i < len; i++)
{
int r = getValue(v[i], digit);
Radix[r].push_back(v[i]);
}
// 收集
for (int i = 0, j = 0; i < 10; i++)
{
RadixSort_MSD(Radix[i], digit - 1);
while (!Radix[i].empty())
{
v[j++] = Radix[i].front();
Radix[i].erase(Radix[i].begin());
}
}
}
}
时间复杂度:O(k*n),k表示最大的位数,n为序列个数,LSD时每一位都需要对n个元素进行分配收集,MSD的递归深度为k
空间复杂度:O(n+k),需要O(n)来保存收集的数据(MSD中是桶的空间)
稳定性:稳定