排序算法整理

排序算法

首先,这里的是否稳定评判标准如下:假设$a$在$b$的前面,且$a=b$,若排序之后,$a$和$b$的顺序没变,则该排序算法是稳定的,若$a$到了$b$的后面,则该排序算法不稳定。

冒泡排序(优化)

基本思想:两两比较,如果反序则交换,每次冒泡,都会有一个元素到达最终的位置。

冒泡排序对n个数据操作n-1轮,每轮找出一个最大(小)值。

  • 最好时间复杂度 ∗ ∗ O ( n ) ∗ ∗ **O(n)** O(n):输入的数组刚好是顺序,比较n-1便,不需要交换操作。但需要采用优化后的代码。
  • 最坏时间复杂度 ∗ ∗ O ( n 2 ) ∗ ∗ **O(n^2)** O(n2):输入的数组完全逆序,每轮排序的每一次比较都要交换。
  • 平均时间复杂度 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度 O ( 1 ) O(1) O(1):交换法,仅在交换时需要一个元素的额外空间。
  • 稳定性:元素相同时不做交换,是稳定的排序算法。

代码如下:

void BubbleSort(vector<int>& arr) {
    int n = arr.size();
    if (n < 2) { return; }
    for (int i = 0; i < n - 1; ++i) {
        for (int j = 0; j < n - 1 - i; ++j) {
            if (arr[j] > arr[j + 1]) {
                swap(arr[j], arr[j + 1);
            }
        }
    }
}

void BubbleSort(vector<int>& arr) {
	//优化版,只有在优化版中,最好时间复杂度才是O(n)
    int n = arr.size();
    if (n < 2) { return; }
    for (int i = 0; i < n - 1; ++i) {
        bool did_swap = false;
        for (int j = 0; j < n - 1 - i; ++j) {
            if (arr[j] > arr[j + 1]) {
                did_swap = true;
                swap(arr[j], arr[j + 1);
            }
        }
        if (!did_swap) { break; }
    }
}

快速排序(单向遍历、双向遍历)

基本思想:通过一次排序将待排的记录分割成两个独立的部分,其中一部分记录的关键字均比另一部分的关键字小,则可以分别对这两部分记录继续进行排序,以达到整个序列有序。

  1. 从数列中调出一个元素,称为**“基准”**(pivot);
  2. 重新排序数组,所有元素比基准小的放在基准前面所有元素比基准大的放在基准后面(相同的数可以到任意一边)。在这个分区退出之后,该基准就出于数组的中间位置,这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数组和大于基准值元素的子数组排序;

分析:

  • 最好时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n):每次选择基准时刚好把数组分为平均的两部分,划分次数为 O ( l o g 2 n ) O(log_2n) O(log2n),每次划分比较一遍 O ( n ) O(n) O(n),时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)
  • 最坏时间复杂度 O ( n 2 ) O(n^2) O(n2):每次选基准都选到了最大值或者最小值,那么就需要n次的分区操作,每次需要比较n次。
  • 平均时间复杂度 ∗ ∗ O ( n l o g 2 n ) ∗ ∗ **O(nlog_2n)** O(nlog2n):大部分情况下,很难选到极端情况。
  • 空间复杂度:快排的空间复杂度和递归的深度有关,最坏情况下,需要n次的分区操作,所以递归要用到的栈的大小也为 O ( n ) O(n) O(n),平均情况为 O ( l o g 2 n ) O(log_2n) O(log2n)
  • 稳定性:快速排序的分区操作涉及到交换操作,是不稳定更多排序算法。

代码如下:

int Partition_in_Quick_1(vector<int>& arr, int left, int right) {
	// 单向遍历
    int pivot = arr[left], pos = 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]);
        }
    }
    arr[pos] = arr[left];
    arr[left] = pivot;
    return left;
}

int Partition_in_Quick_2(vector<int>& arr, int left, int right) {
	// 双向遍历
    int pivot = arr[left];
    while (left < right) {
        while (left < right && arr[right] >= pivot) {
            --right;
        }
        arr[left] = arr[right];
        while (left < right && arr[left] <= pivot) {
            ++left;
        }
        arr[right] = arr[left];
    }
    arr[left] = pivot;
    return left;
}

void QuickSort(vector<int>& arr, int left, int right) {
    if (left < right) {
        int pos = Partition_in_Quick_1(arr, left, right);
				// int pos = Partition_in_Quick_2(arr, left, right);
        QuickSort(arr, left, pos - 1);
        QuickSort(arr, pos + 1, right);
    }
}

插入排序

插入排序(Insertion-Sort)是通过构建有序序列,对于未排序的数据,在已排序的序列中从后往前扫描,找到相应位置并插入。

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果扫描的的元素大于正在排序的元素,则将扫描到的元素移动到下一个位置;
  4. 重复上一步骤,直到扫描到已经排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置;
  6. 重复步骤2-5;

简单插入排序童谣操作n-1轮,每轮都将一个未排序的数据插入到已经排序的数据中。

  • 最好时间复杂度 O ( n ) O(n) O(n):当数组刚好顺序,每次只用一次比较即可,重复n-1次。
  • 最坏时间复杂度 O ( n 2 ) O(n^2) O(n2):当数组完全逆序时,要比较1+2+3+…+n-1次才能完成整个排序。
  • 平均时间复杂度 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度 O ( 1 ) O(1) O(1):仅在移位时需要过渡空间。
  • 稳定性:元素相同时不做交换,是稳定的排序算法。

代码如下:

void InsertSort(vector<int>& arr) {
    int n = arr.size();
    if (n < 2) { return; }
    for (int i = 1; i < n; ++i) {
        int target = arr[i];
        int j = i - 1;
        while (j >= 0 && arr[j] > target) {
            swap(arr[j], arr[j + 1]);
            --j;
        }
    }
}

希尔排序

Shell Sort是简单插入排序的改进,又称缩小增量排序。它与插入排序的不同之处在于,它会优先比较距离较远的元素。

希尔排序将序列按固定间隔划分为多个子序列,在子序列中简单插入排序,先做远距离移动使序列基本有序;逐渐缩小间隔重复操作,最后间隔为1时即简单插入排序。

希尔排序的时间复杂度主要由其增量序列来决定,目前最好的增量序列下,希尔排序的时间复杂度大致为 O ( n 1.3 ) O(n^{1.3}) O(n1.3)。希尔排序的平均复杂度是否能达到 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)也是一个尚未解决的问题。

其空间复杂度也是 O ( 1 ) O(1) O(1)

注意,由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的**

代码如下:

void ShellSort(vector<int>& arr) {
    int n = arr.size();
    if (n < 2) { return; }
    int step = n >> 1;
    while (step) {
        for (int i = step; i < n; ++i) {
            while (i >= step && arr[i] < arr[i - step]) {
                swap(arr[i], arr[i - step]);
                i -= step;
            }
        }
        step >>= 1;
    }
}

选择排序

Selection Sort是同样对数据操作n-1轮,每轮找出一个最小(大)值。以此类推,直到所有元素均排序完毕。

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。

时间复杂度均为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)。由于会交换顺序,所以选择排序是不稳定的。

代码如下:

void SelectSort(vector<int>& arr) {
    int n = arr.size();
    if (n < 2) { return; }
    for (int i = 0; i < n - 1; ++i) {
        int min_index = i;
        for (int j = i + 1; j < n; ++j) {
            min_index = (arr[j] < arr[min_index]) ? j : min_index;
        }
        if (i != min_index) {
            swap(arr[i], arr[min_index]);
        }
    }
}

堆排序

HeapSort是指利用堆这种数据结构所设计的一种排序算法。堆是一个近似完全二叉树的结构,其满足子节点的键值或索引总是小于(或者大于)它的父节点。排序过程如下:

  1. 构造一个大顶堆,取堆顶数字A[0](也就是当期最大值)与最后一个元素A[n-1]交换;
  2. 新的无序区(A[0]、A[1]、…、A[n-2])和新的有序区(A[n-1])。再将剩下的数字构建一个大顶堆,取堆顶数字(也就是剩下值当中的最大值)A[0]与无序区最后一个元素A[n-2]交换;
  3. 得到新的无序区(A[0]、A[1]、…、A[n-3])和新的有序区(A[n-2]、A[n-1]);
  4. 重复以上操作,直到取完堆中的数字;

堆排序的初始建堆过程恒比较复杂,要对 O ( n ) O(n) O(n)级别个非叶子节点进行堆调整操作 O ( l o g 2 n ) O(log_2n) O(log2n),时间复杂度即为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。之后每一次堆调整操作确定一个数的次序,时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。合起来时间复杂度即为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

空间复杂度为 O ( 1 ) O(1) O(1),在调整堆的过程中需要暂存空间。

代码如下:

void HeapAdjust(vector<int>& arr, int i, int n) {
    int largest = i, left = 2 * i + 1, right = 2 * (i + 1);
    if (left < n && arr[left] > arr[largest]) {
        largest = left;
    }
    if (right < n && arr[right] > arr[largest]) {
        largest = right;
    }
    if (largest != i) {
        swap(arr[i], arr[largest]);
        HeapAdjust(arr, largest, n);
    }
}

void HeapSort(vector<int>& arr) {
    int n = arr.size();
    for (int i = n / 2; i >= 0; --i) {
        HeapAdjust(arr, i, n);
    }
    for (int j = n - 1; j > 0; --j) {
        swap(arr[0], arr[j]);
        HeapAdjust(arr, 0, j);
    }
}

归并排序(递归、非递归、辅助数组归并、原地归并)

归并排序的原理其实是分治法。将数组不断二分,直到最后每个部分只包含一个数据,然后再对每个部分分别进行排序,最后将排序号的相邻的两部分合并在一起。这就是2路归并。

其时间复杂度均为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)。空间复杂度在使用辅助数组时为 O ( n ) O(n) O(n),不采用辅助数字时为 O ( 1 ) O(1) O(1)

代码如下:

void Merge(vector<int>& arr, int left, int mid, int right) {
    int n_temp = right - left + 1;
    vector<int> temp(n_temp);
    int t = 0; //辅助数组的起始下标
    int i = left, j = mid + 1; //两个子序列的起始位置
    while (i <= mid && j <= right) {
        if (arr[i] < arr[j]) {
            temp[t++] = arr[i++];
        }else {
            temp[t++] = arr[j++];
        }
    }
    while (i <= mid) {
        temp[t++] = arr[i++];
    }
    while (j <= right) {
        temp[t++] = arr[j++];
    }
    for (int k = 0; k < n_temp; ++k) {
        arr[left + k] = temp[k];
    }
}

void _reverse(vector<int>& arr, int begin, int end) {
    while (begin < end) {
        swap(arr[begin++], arr[end--]);
    }
}

void MergeInPlace(vector<int>& arr, int left, int mid, int right) {
	// 原地归并,不需要辅助数组
    int i = left, j = mid + 1;
    while (i < j && j <= right) {
        while (i < j && arr[i] <= arr[j]) {
            ++i;
        }
        int old_j = j;
        while (j <= right && arr[i] > arr[j]) {
            ++j;
        }
        _reverse(arr, i, old_j - 1);
        _reverse(arr, old_j, j - 1);
        _reverse(arr, i, j - 1);
        i += (j - old_j);
    }
}

void MergeSort(vector<int>& arr, int left, int right) {
	// 递归
    if (left < right) {
        int mid = left + ((right - left) >> 1);
        //int mid = (left + right) / 2;
        MergeSort(arr, left, mid);
        MergeSort(arr, mid + 1, right);
        Merge(arr, left, mid, right);
    }
}

void MergeSort_Non_Recursive(vector<int>& arr) {
	// 非递归
    int n = arr.size(), cur_len = 1;
    while (cur_len <= n) {
        for (int i = 0; i <= n - cur_len; i += cur_len * 2) {
            int left = i, mid = i + cur_len - 1, right = i + cur_len * 2 - 1;
            right = (right >= n) ? n - 1 : right;
            //Merge(arr, left, mid, right); // 辅助数组合并
            MergeInPlace(arr, left, mid, right); // 原地合并
        }
        cur_len *= 2;
    }
}

本文部分理论内容参考夏普通

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值