1. 冒泡排序法
相邻元素不断比对,在对比的过程中小的数浮起来(降序)
#include<iostream>
#include<vector>
using namespace std;
void BubbleSort(vector<int> &input) {
//i是第表示第i+1次排序 每次排序找到一个最大值,所以需要排序size-1次 i < input.size()-1
for (int i = 0; i < input.size()-1; ++i) {
//排序i+1次后,找到最大的i+1个值 所以冒泡范围相对要缩小
//input[j + 1] j < input.size()-1
for (int j = 0; j < input.size() - 1 - i; ++j)
//注意比较j 升序 input[j]>input[j+1] 交换
// 降序 input[j]<input[j+1] 交换
if (input[j] < input[j + 1]) swap(input[j], input[j+1]);
}
return;
}
int main() {
int n;//表示数组的大小
cin >> n;
vector<int> vec(n);
for (int i = 0; i < n; i++) {
cin >> vec[i];
}
BubbleSort(vec);
for (int i = 0; i < n; i++) {
cout << vec[i]<<" ";
}
return 0;
}
输入
5
17 39 4 29 89
输出
89 39 29 17 4
数据完全有序的时候展现出最优时间复杂度,为O(n)。其他情况下,几乎总是O( n2 ),平均时间复杂度为O
(
n
2
)
(n^2)
(n2),稳定
稳定就是指两个元素相同,经排序后不会改变相对位置。
改进:增加一个flag的标志,当前一轮没有进行交换时,说明数组已经有序,没有必要再进行下一轮的循环了,直接退出
void BubbleSort(vector<int> &input) {
//i是第表示第i+1次排序
bool flag;
for (int i = 0; i < input.size()-1; ++i) {
flag=false;
//排序i+1次后,找到最大的i+1个值 所以冒泡范围相对要缩小
for (int j = 0; j < input.size() - 1 - i; ++j)
//注意比较j 升序 input[j]>input[j+1] 交换
// 降序 input[j]<input[j+1] 交换
if (input[j] < input[j + 1]) {
swap(input[j], input[j+1]);
flag=true;
}
if (flag==false){
break;
}
}
return;
}
2. 选择排序
不断选择最大的数放在结尾或者不断寻找最小的数字放到数字开头(升序)
void SelectionSort(vector<int>& input) {
//i是第表示第i+1次排序 如果只剩下一个数,不用寻找最小值了(i+1),所以i < input.size() - 1
for (int i = 0; i < input.size() - 1; ++i) {
//不断寻找最小的数字放在开头
int min_index = i;
//从i+1开始比较
for (int j = i + 1; j < input.size(); ++j) {
//寻找最小值的索引
if (input[min_index] > input[j]) {
min_index = j;
}
}
//找到的最小值放到前面
swap(input[i], input[min_index]);
}
return;
}
复杂度为O ( n 2 ) (n^2) (n2),用数组实现不稳定 例如15(1) 15(2) 5 经过一次排序后5 15(2)15(1)改变了相对位置
3. 插入排序
把未排序的元素插入已经排好序的子数组中,从而保证排序子数组一直有序,可认为第一个元素是排好序的。
void InsertionSort(vector<int>& input) {
//默认第一个元素是有序的
for (int i = 1; i < input.size() - 1; ++i) {
int position = i;
//把要插入的数暂时保存下来,后续移动的时候会覆盖
int value = input[i];
//把比插入数大的所有数都向后移动一位
while (position > 0 && input[position - 1] > value) {
input[position] = input[position - 1];
position--;
}
//在position位置把元素插入
input[position] = value;
}
return;
}
平均时间复杂度为O ( n 2 ) (n^2) (n2),稳定
4.快排
采用递归的方法,首先找到一个基准pivot,比其小放左边,比其大放右边(这个过程叫分区partition),然后分别不断递归左边和右边,使得每个递归区间只有一个元素。
#include<iostream>
#include<vector>
using namespace std;
int partition(vector<int>& arr, int left, int right) {
//选择左边界的元素作为基准
int pivot = arr[left];
int start = left;
//双指针法
while (left < right) {
//找到右边第一个更小的 对相等的值不做改变,继续遍历
while (left < right && arr[right] >= pivot) right--;
//找到左边第一个更大的 对相等的值不做改变,继续遍历,比如第一个遍历的点就是基准
while (left < right && arr[left] <= pivot) left++;
if(left<right) swap(arr[left], arr[right]);
}
//两个指针相遇后,调换相遇点和基准点 可举例5 1 2 6 9 4
swap(arr[start], arr[left]);
return left;
}
void QuickSort(vector<int>& input,int left,int right) {
//注意区间是左闭右闭
//递归终止条件,只有一个元素或者[left,right]区间非法
if (left >= right) return;
//通过partition函数分区并找到pivot
int pivot = partition(input, left, right);
//分别递归排序其左区间和右区间
QuickSort(input, left, pivot - 1);
QuickSort(input, pivot + 1, right);
}
int main() {
int n;//表示数组的大小
cin >> n;
vector<int> vec(n);
for (int i = 0; i < n; i++) {
cin >> vec[i];
}
QuickSort(vec,0,n-1);
for (int i = 0; i < n; i++) {
cout << vec[i] << " ";
}
return 0;
}
平均时间复杂度为O
(
n
l
o
g
n
)
(nlogn)
(nlogn),不稳定举例 5 6 5 6 3 6 5
每一次根据基准划分为两部分,相当于二叉树,并且我们知道二叉树的深度是logn,partition耗费的时间复杂度是O(n),最终的O
(
n
l
o
g
n
)
(nlogn)
(nlogn)其实是计算来的。
快速排序的性能高度依赖于你选择的基准值。
最糟情况
假设你总是将第一个元素用作基准值,且要处理的数组是有序的。由于快速排序算法不检查输入数组是否有序,因此它依然尝试对其进行排序。注意,数组并没有被分成两半,相反,其中一个子数组始终为空,这导致调用栈非常长。
算法总结:
5.Topk
class Solution {
public:
// //时间复杂度O(nlogn)
// int findKthLargest(vector<int>& nums, int k) {
// sort(nums.begin(),nums.end());
// int size=nums.size();
// return nums[size-k];
// }
//快排 超时
// int partition(vector<int>& arr, int left, int right) {
// //选择左边界的元素作为基准
// int pivot = arr[left];
// int start = left;
// //双指针法
// while (left < right) {
// //找到右边第一个更小的 对相等的值不做改变,继续遍历
// while (left < right && arr[right] >= pivot) right--;
// //找到左边第一个更大的 对相等的值不做改变,继续遍历,比如第一个遍历的点就是基准
// while (left < right && arr[left] <= pivot) left++;
// if(left<right) swap(arr[left], arr[right]);
// }
// //两个指针相遇后,调换相遇点和基准点 可举例5 1 2 6 9 4
// swap(arr[start], arr[left]);
// return left;
// }
// void QuickSort(vector<int> &nums, int left, int right) {
// if (left >=right)
// return;
// int pivot = partition(nums,left,right);
// QuickSort(nums, left, pivot - 1);
// QuickSort(nums, pivot + 1, right);
// }
// int findKthLargest(vector<int> &nums, int k) {
// int n = nums.size();
// QuickSort(nums, 0,n-1 );
// return nums[n-k];
// }
//改造快排 只对左右区间中的一个区间进行递归,保证时间复杂度为O(n),证明过程在算法导论9.2
int result;
void findtopk(vector<int> &nums,int left,int right,int k){
//分区
int pivot=nums[left];
int start=left,end=right;
//while(start<end){
// while(start<end && nums[end]>=pivot) end--;
// nums[start]=nums[end];
// while(start<end && nums[start]<=pivot) start++;
// nums[end]=nums[start];
// }
// //此时start=end为双指针相遇点
// nums[start]=pivot;
while(start<end){
while(start<end && nums[end]>=pivot) end--;
while(start<end && nums[start]<=pivot) start++;
if(start<end) swap(nums[start],nums[end]);
}
//此时start=end为双指针相遇点
swap(nums[start],nums[left]);
if(start==nums.size()-k) {
result=nums[start];
return;
}
else if(start<nums.size()-k)
findtopk(nums,start+1,right,k);
else
findtopk(nums,left,start-1,k);
}
int findKthLargest(vector<int>& nums, int k) {
findtopk(nums,0,nums.size()-1,k);
return result;
}
};
用优先级队列 维持前k个元素
greater 升序 小顶堆
lesser 降序 大顶堆
和队列基本操作相同:
top 访问队头元素
empty 队列是否为空
size 返回队列内元素个数
push 插入元素到队尾 (并排序)
emplace 原地构造一个元素并插入队列
pop 弹出队头元素
swap 交换内容
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
int main()
{
int n,k;
cin >> n>>k;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
//小顶堆
priority_queue<int, vector<int>, less<int>> pri_que;
for (int i = 0; i < n; i++) {
pri_que.push(a[i]);
if (pri_que.size() > k) {
pri_que.pop();
}
}
cout << pri_que.top() << endl;
return 0;
}
时间复杂度O(nlogk),空间复杂度O(n)
分析:
你使用了一个最小堆(priority_queue)来维护当前前k个最大的元素。
初始化堆的时间复杂度是O(k),因为你首先将数组中的前k个元素放入堆中。
然后,对于剩余的n-k个元素,你需要遍历它们一次,并执行以下操作:
将元素插入堆中,插入操作的时间复杂度是O(logk),因为堆的大小最多为k。
如果堆的大小超过k,你会弹出堆顶元素,弹出操作的时间复杂度也是O(logk)。
因此,总的时间复杂度可以表示为:
初始堆建立:O(k)
插入操作次数:n - k 次
弹出操作次数:n - k 次
总的时间复杂度是O(k + (n - k) * logk),在渐进意义上,这可以简化为O(nlogk),
6.问题总结
- 归并排序和快速排序的时间复杂度都是 O(nlogn),为什么说快速排序一般优于归并排序?
答:快速排序中效率的主要来源之一是引用位置,在引用位置中,计算机硬件经过优化,因此访问彼此相邻的内存位置往往比访问分散在整个内存中的内存位置更快。quicksort中的分区步骤通常具有很好的局部性,因为它访问前面和后面附近的连续数组元素。因此,快速排序往往比其他排序算法(如heapsort)执行得更好,尽管它通常执行大致相同数量的比较和交换,因为在heapsort的情况下,访问更加分散。
此外,quicksort通常比其他排序算法快得多,因为它在原地运行,而不需要创建任何辅助数组来保存临时值。与merge sort相比,这是一个巨大的优势,因为分配和释放辅助数组所需的时间是显而易见的。就地操作也提高了quicksort的位置。
使用链表时,这两个优点都不一定适用。由于链表单元通常分散在整个内存中,因此访问相邻的链表单元没有额外的局部性好处。因此,quicksort的一个巨大的性能优势被消耗殆尽。类似地,就地工作的好处不再适用,因为merge sort的链表算法不需要任何额外的辅助存储空间。
也就是说,快速排序在链接列表中仍然非常快。合并排序往往更快,因为它更均匀地将列表分成两半,并且每次执行合并所做的工作比执行分区步骤所做的工作更少。
原文链接:https://blog.csdn.net/u011947630/article/details/104691611
-下列排序方法中,最坏情况下时间复杂度最小的是
A)直接插入排序
B)堆排序
C)冒泡排序
D)快速排序
答案:B
【解析】对长度为n的线性表排序,在最坏情况下,冒泡排序、快速排序和直接插入排序需要比较的次数为n(n-1)/2,而堆排序需要比较的次数为O(nlog2n)。
- 5.在最坏情况下
A)快速排序的时间复杂度比希尔排序的时间复杂度要小
B)快速排序的时间复杂度与希尔排序的时间复杂度是一样的
C)希尔排序的时间复杂度比直接插入排序的时间复杂度要小
D)快速排序的时间复杂度比冒泡排序的时间复杂度要小
答案:C
【解析】对长度为n的线性表排序,在最坏情况下,冒泡排序、快速排序和直接插入排序需要比较的次数为n(n-1)/2,希尔排序所需要的比较次数为O(n1.5)。