为什么要学习复杂度为O(N^2)的排序算法?
1.基础,编码简单,易于实现,是一些简单情景的首选
2.在一些特殊情况下,简单的排序算法更有效(插入)
3.简单排序算法的思想衍生出的其他排序方式
4.作为子过程,改进更复杂的排序算法
冒泡排序
思想:每次迭代通过比较相邻两个元素的大小,进行元素位置的调整,直到所有元素全部排好,代码如下
void bubbleSort (int *arr, int len) {
int i, j;
for (i=0; i<len-1; i++)//控制排序趟数
for (j=0; j<len-1-i; j++) {//控制每趟排序比较多少次
if (arr[j] > arr[j+1]) {
swap(arr[j],arr[j+1]);
}
}
}
复杂度分析:第一次迭代需要比较的次数为N次,第二次迭代为N-1次,依次类推,复杂度为N+(N-1)+(N-2)+……+1=N(N-1)/2=O(N^2)
改进后的冒泡排序,复杂度为O(N):
我们在原本的冒泡排序中加入一个布尔变量,发生一次交换则置为true,这样如果数组变得有序(经过某次循环后布尔变量依然为false),就可以提前终止循环。代码如下:
void bubbleSort (int arr[], int len) {
bool didswap;
for (i=0;i<len-1; i++){
for (j=0;j<len-1-i; j++) {
if (arr[j] > arr[j+1]) {
swap(arr[j],arr[j+1]);
didswap=true;
}
}
if(didswap==flase){
return;
}
}
}
冒泡排序衍生出的排序思路:
如某公司面试题:
[1,2,3,-2,-4,5,3,-2,4,1,-5,3]数组排序
要求:1.正数在左,负数在右 2.相对顺序不变 3.空间复杂度为O(1)
解题思路:采用双指针的策略会改变元素的相对顺序,利用冒泡排序的思想,比较前后两个元素的正负,若负数在前,正数在后,则交换两个元素。
代码如下:
void bubbleSort (int arr[], int len) {
for (i=0;i<len-1; i++){
for (j=0;j<len-1-i; j++) {
if (arr[j]<0 && arr[j+1]>0) {
swap(arr[j],arr[j+1]);
}
}
}
}
选择排序
思想:每次迭代从剩余元素中选择最大(小)的一个,放在数组的末尾(头部)。
void selectSort (int arr[], int len) {
for (i=0;i<len; i++){
int minIndex=i;
for (int j=i+1;j<n;j++) {
if (arr[j]<arr[minIndex]) {
minIndex=j;
}
}
swap(arr[i],arr[minIndex]);
}
}
插入排序
类比打牌时的排牌
每步将一个待排序的记录,按其关键码值的大小插入前面已经排序的文件中适当位置上,直到全部插入完为止。
void insertionSort (int arr[], int len) {
for (i=0;i<len; i++){
int tmp=arr[i];
int j;
for (int j=i;j>0 && arr[j-1]>tmp;j--) {
arr[j]=arr[j-1];
}
arr[j]=tmp;
}
}
重要性质:当数组近乎有序的时候,可以提前终止内层循环,具有非常快的排序速度,复杂度为O(N)
复杂度为O(NlogN)的排序算法
归并排序
思想:分治,将数据化成logN层级,每一层用O(N)的复杂度去解决
第一步:申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
第二步:设定两个指针,最初位置分别为两个已经排序序列的起始位置
第三步:比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
重复步骤3直到某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾
代码如下:
void mergeSort (int arr[], int l,int r) {
if(l>=r){
return;
}
int mid=l+(r-1)/2;
mergeSort(arr,l,mid);
mergeSort(arr,mid+1,r);
merge(arr,l,mid,r);
}
void merge(int arr[],int l,int mid,int r){
int aux[r-l+1];
for(int i=l;i<=r;i++){
aux[i-l]=arr[i];
}
int i=l,j=mid+1;
for(int k=l;k<=r;k++){
if(i>mid){
arr[k]=aux[j-l];j++;
}
else if(j>r){
arr[k]=aux[i-l];i++;
}
else if(aux[i-l]<aux[j-l]){
arr[k]=aux[i-l];i++;
}
else{
arr[k]=aux[j-l];j++;
}
}
}
快速排序
思想:
1.先从数列中取出一个数作为基准
2.分区,该数左边的都比该数小,右边的都比该数大
3.再对左右区间递归第二步
代码如下:
void quickSort(int arr[],int l,int r){
swap(arr[l],arr[rand()%(r-l+1)+l]);//将数组第一个元素与数组中随机一个元素交换,目的是避免数组有序导致的极端情况
int lt=l;
int gt=r+1;
int i=l+1;
while(i<gt){
if(arr[i]<arr[l]){
swap(arr[i],arr[lt+1]);
lt++;
i++;
}
else if(arr[i]>arr[l]){
swap(arr[i],arr[gt-1]);
gt--;
}
else{
i++;
}
}
swap(arr[l],arr[lt+1]);
quickSort(arr,l,lt-1);
quickSort(arr,gt,r);
}
当数组为近乎有序的时候,快排的时间复杂度降为O(N^2),因为当数组有序的时候会执行N次分区操作。
解决方法:在排序之前交换标定值和随机一个数的位置
堆排序
思想:首先建立一个堆,然后不断地比较非叶子节点和堆顶元素的大小,同时调整堆,完成对元素的排序。
堆排序主要用于动态数据的维护,如在1000000个元素中选出前100名这种N个元素中选出前M个元素的问题,用堆排序最好解决,其复杂度为NlogM
PS:建堆的复杂度为O(N)
代码如下:
void heap_adjust(vector<int>& num, int i, int n)
{
int j = i * 2 + 1;
while (j < n)
{
if (j + 1 < n && num[j] < num[j + 1])
{
j++;
}
if (num[i] > num[j])//已符合父节点大于子节点,无需交换,跳出循环
break;
swap(num[i], num[j]);
i = j;//不符合,交换后,shiftdown向下调整
j = i * 2 + 1;
}
}
void heap_sort(vector<int>&num, int n)
{
int i;
for (int i = n / 2-1; i >= 0; i--)//对每一个非叶结点进行堆调整(从最后一个非叶结点开始),建堆的过程
{
heap_adjust(num, i, n);
}
for ( i = n-1; i >= 0; i--)
{
swap(num[0], num[i]);// 将当前最大的放置到数组末尾
heap_adjust(num, 0, i);// 将未完成排序的部分继续进行堆排序
}
}