练习地址:https://leetcode-cn.com/problems/sort-an-array/
前提
●算法的输入可以互换。每个算法接收一个含有元素的数组和一个包含元素个数的整数。
●传入的元素个数N是合法的,数据从0处开始
●假设<和>运算符都存在
冒泡排序
算法
首先将第一个关键字和第二个关键字进行比较,若为逆序则将两个记录交换,然后比较第二个记录和第三个记录,依次类推,知道第n-1个记录和第n个记录的关键字进行比较为止。此过程称为第一趟起泡排序,其结果为使得关键字最大的记录被安置到最后一个记录的位置,然后进行第二趟排序。
代码
/*冒泡排序*/
void BubbleSort(vector<int>&vec, int N) {
for (int i = 0; i < N; i++) {
//N - i后面的都是排序好的大元素
for (int j = 1; j < N - i; ++j)
if (vec[j - 1] > vec[j])
swap(vec[j - 1], vec[j]);
}
}
分析
冒泡排序的平均时间复杂度是O(n^2)
插入排序
算法
//插入排序由N-1趟(pass)排序组成。对与P = 1到N-1趟,插入排序保证0到P-1位置上的元素是已排序的
//在第P趟,为了将位置P上的元素向左移动到正确的位置上。先存储P上的元素,然后把比它大的元素都往右挪,直到有小于等于它的,
void InsertionSort(vector<int>&vec, int N) {
for (int p = 1; p < N; p++) {
int j;
int tmp = vec[p];
for (j = p; j > 0 && vec[j - 1] > tmp; j--)
vec[j] = vec[j - 1];
vec[j] = tmp;
}
}
分析
●时间复杂度:2+3+4+…+N = O(N^2)
●输入已预先排序的时间复杂度:O(N),因为内层for循环总是立即终止。所以如果输入几乎被排序,那么插入排序将会运行的很快。
希尔排序(缩小增量排序)
希尔排序是冲破二次时间屏障的第一批算法之一。
思想
https://www.cnblogs.com/chengxiao/p/6104371.html
代码
这里使用流行的增量序列:初始为N/2,每次/2,即希尔增量
void ShellSort(vector<int>&vec, int N) {
//第一层循环:增量初始化为N/2,每次/2
for (int inc = N / 2; inc > 0; inc /= 2)
//第二层循环:当前增量为inc,i从inc开始是因为i-inc要存在(即前面要有数来进行插入排序,就像插入排序从1开始)
for (int i = inc; i < N; i++) {
int tmp = vec[i];
int j;
//第三层循环:对当前的从i往前间隔为inc的数组进行插入排序,上面的插入排序即inc为1时的情况
for (j = i; j >= inc && vec[j - inc] > tmp; j -= inc)
vec[j] = vec[j - inc];
vec[j] = tmp;
}
}
分析
●最坏情况:
使用希尔增量时希尔排序的最坏情形运行时间为O(N^2),Hibbard增量和Sedgewick增量序列的最坏情形比希尔增量要好。
堆排序
思想
利用之前的堆,先建堆,再DeleteMax。为了节省空间,将DeleteMax得到的值放到数组后面
代码
https://blog.csdn.net/qq_36573828/article/details/80261541
// 下滤
void percDown(vector<int>& vec, int i, int N) {
int j = 2 * i + 1;
while (j < N) {
if (j + 1 < N && vec[j + 1] > vec[j])
j++;
if(vec[i] > vec[j])
break;
swap(vec[i], vec[j]);
i = j;
j = i * 2 + 1;
}
}
void HeapSort(vector<int>& vec, int N) {
// 从中间往前建堆
for (int i = N / 2 - 1; i >= 0; i--)
percDown(vec, i, N);
// 将堆顶取出,放到后面,并将新的堆顶下滤
for (int i = N - 1; i > 0; i--) {
swap(vec[i], vec[0]);
percDown(vec, 0, i);
}
}
分析
●时间复杂度:
建立N个元素的二叉堆花费O(N),执行N次DeleteMin操作,每次花费O(logN),总运行时间是O(NlogN)。
所以堆排序花费O(NlogN)的时间,但是在实践中慢于使用Sedgewick增量序列的希尔排序。
●空间复杂度:O(N)
增加了存储需求,这是一个弊端。从第二个数组拷贝回第一个 数组消耗O(N)时间,这不会显著影响运行时间。
可以避免使用第二个数组,每次DeleteMin之后,堆缩小了1.因此位于堆中最后的元素可以用来存放刚刚删去的元素。
所以采用大顶堆的话,最后数组将会为升序。
归并排序
思想
如将8个元素的数组排序,可以递归地将前四个数据和后四个数据进行排序,然后将两部分合并。这是经典的分治思想。
代码
/*归并排序*/
//left_end等于right - 1,所以不用传入
void Merge(vector<int>&vec, vector<int>&tmp, int left, int right, int right_end) {
int left_end = right - 1;
int tmp_pos = left; //遍历tmp
int num = right_end - left + 1; //总数
while (left <= left_end && right <= right_end) {
if (vec[left] <= vec[right])
tmp[tmp_pos++] = vec[left++];
else
tmp[tmp_pos++] = vec[right++];
}
//有一个到底了
while (left <= left_end)
tmp[tmp_pos++] = vec[left++];
while (right <= right_end)
tmp[tmp_pos++] = vec[right++];
//因为right_end没变,所以从right_end拷贝回vec
for (int i = 0; i < num; i++, right_end--)
vec[right_end] = tmp[right_end];
}
//将vec中的left到right排序
void MSort(vector<int>&vec, vector<int>&tmp, int left, int right) {
int center;
if (left < right) {
center = left + (right - left) / 2;
MSort(vec, tmp, left, center);
MSort(vec, tmp, center + 1, right);
Merge(vec, tmp, left, center + 1, right);
}
}
//tmp的分析见下面
void MergeSort(vector<int>&vec, int N) {
//在这里而不是递归中声明临时数组,递归中声明的话会声明很多个
vector<int>tmp(N);
MSort(vec, tmp, 0, N - 1);
}
分析
●时间复杂度:由T(N) = 2*T(N/2) + N可以得到T(N) = O(NlogN)
●缺点:很难用于主存排序,主要问题在于两个排序的表需要线性附加内存,还要花费时间将数据拷贝,放慢了排序的速度。所以人们大多使用快速排序来进行内部排序。
●tmp的作用:如果每个递归调用均局部声明一个临时数组,那么在任一时刻可能就有logN个临时数组处在活动期,这样会消耗很多内存。使用tmp在任一时刻只需要一个临时数组活动。
快速排序
思想
枢纽元的选取
错误方法
直接选取第一个元素时,如果输入是预排序或者反序的,会产生劣质的分割,因为所有的元素不是都在S1就是都在S2。
随机选取
指针方法比较安全,但是随机数的生成比较昂贵,减少不了算法其余部分的平均时间。
三数中值
枢纽元的最好的选择是所有数的中值,但是这样代价昂贵。因此使用左端、右端和中心位置上三个元素的中值作为枢纽元
代码
/*快速排序*/
int partition(vector<int>&nums, int left, int right) {
//int pivot = (left + right) / 2;
int pivot = (rand() % (right - left + 1)) + left;
swap(nums[pivot], nums[left]);
int i = left, j = right, k = nums[left];
//这里是挖坑法,还有一种交换法
while(i < j){
while(i < j && nums[j] >= k)
j--;
nums[i] = nums[j];
while(i < j && nums[i] <= k)
i++;
nums[j] = nums[i];
}
//此时i = j
nums[i] = k;
return i;
}
// 快排
void quickSort(vector<int>&nums, int left, int right) {
if(left >= right)
return;
int index = partition(nums, left, right);
// 此时index左边<=nums[index],右边>=nums[index],再将左右分别排序就好
quickSort(nums, left, index - 1);
quickSort(nums, index + 1, right);
}
分析
小数组时
小数组(N<=20),快速排序不如插入排序好,因为快速排序是递归的。所以在小数组时采用插入排序这种对小数组有效的排序算法。
时间复杂度
●最坏情况:枢纽元始终是最小元素。T(N) = T(N - 1) + cN,N > 1,时间复杂度为O(N^2)
●最好情况:枢纽元正好位于中间。T(N) = 2T(N/2) + cN,时间复杂度为O(NlogN)
应用
快速选择
用于求得第k个最大(最小)元
https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof/
桶排序
思想
桶排序将输入数据的区间均匀分成若干份,每一份称作“桶”。分别对每一个桶的内容进行排序,再按桶的顺序输出则完成排序。
代码
/*桶式(链表)排序*/
int find_max(vector<double>vec, int N) {
int max_val = vec[0];
for (int i = 1; i < N; i++)
max_val = max(max_val, int(vec[i]));
return max_val;
}
//输入为大于0的double类型数据
void BucketSort(vector<double>&vec, int N) {
//找出最大值,根据最大值得到桶数
int size = find_max(vec, N);
vector<list<double>>list_vec(size + 1);
//把数据放到相应的桶
for (int i = 0; i < N; i++)
list_vec[int(vec[i])].push_back(vec[i]);
//每个桶内部排序
for (int i = 0; i <= size; i++)
list_vec[i].sort();
int j = 0;
//遍历每个桶,将数据拷贝回去
for(int i = 0; i <= size; i++)
while (list_vec[i].size() > 0) {
vec[j++] = list_vec[i].front();
list_vec[i].pop_front();
}
}
分析
●桶排序是稳定的
●桶排序是常见排序里最快的一种,比快排还要快…大多数情况下
●桶排序非常快,但是同时也非常耗空间,基本上是最耗空间的一种排序算法