排序算法总结(待续)

排序算法总结

所有的排序算法,默认: 小的在前 大的在后

参考博客


  • 非线性时间排序:通过元素比较来决定相对次序. 时间复杂度不能突破 O(NlogN)
  • 线性时间排序: 不通过比较元素来决定次序. 可以在线性时间运行. 因此称为线性时间非比较类排序;

img

算法复杂度:

img


冒泡排序

  • 冒泡排序:

    它重复走过一次要排序的数列,一次比较两个元素,若他们顺序错误,就进行交换.越小的元素会慢慢 浮到数列前面.

    每次确定一个未排序中的最大一个数!!!

算法步骤:

  1. 比较相邻两个元素,若第一个比第二个大 就交换顺
  2. 每一对都交换顺序,经过一轮比较之后,最后一个元素就是最大
  3. 每一轮可以确定一个最大的数.需要 数组个数次

代码实现:

/*
	冒泡排序原理:
	1. 比较相邻两个元素,若第一个比第二个大 就交换顺序
	2. 每一对都交换顺序,经过一轮比较之后,最后一个元素就是最大了.
	3. 每一轮可以确定一个最大的数.需要 数组个数次
*/
void bubbleSort(vector<int> &arr) {
    int _len = arr.size();
    // 外循环,一共进行多少次比较
    for (int times = 0; times < _len; times++) {
        for (int i = 0; i < _len - times-1; i++) {
            if (arr[i] > arr[i + 1]) {
                swap(arr[i], arr[i + 1]);
            }
        }
    }
}
  • 改进1,2
// 改进1: 用标志来表明是否进行了数据的交换,若无数据交换,说明已经排好序了.
void bubbleSort1(vector<int> &arr) {
    int _len = arr.size();
    // 外循环,一共进行多少次比较
    for (int times = 0; times < _len; times++) {
        bool sorted = true;// 是否已经有序
        for (int i = 0; i < _len - times - 1; i++) {
            if (arr[i] > arr[i + 1]) {
                swap(arr[i], arr[i + 1]);
                if(sorted)
                    sorted = false;
            }
        }
        if (sorted)
            break;
    }
}

// 改进2:每次增加一个反向冒泡,确定最小值
void bubbleSort2(vector<int> &arr) {
    int _len = arr.size();
    // 外循环,
    int low = 0;
    int high = _len - 1;
    while (low < high) {
        for (int i = low; i != high; i++) {
            if (arr[i] > arr[i + 1] && i + 1 <= high) {
                swap(arr[i], arr[i + 1]);
            }
        }
        high--;// 排完序以后,high位置确定了.所以--

        for (int j = high; j != low; j--) {
            if (arr[j] < arr[j - 1] && j - 1 >= low) {
                swap(arr[j], arr[j - 1]);
            }
        }
        low++;// 排完序以后,low位置确定了.所以++

    }
}

算法性能分析:

  • 冒泡排序对 n 个元素需要 O(n^2) 的比较次数,且可以原地排序,无需辅助空间。
  • 仅适用于对于含有较少元素的数列进行排序。
  • 最差时间复杂度 O(n^2)
  • 平均时间复杂度 O(n^2)
  • 最优时间复杂度 O(n)
  • 最差空间复杂度 O(n)
  • 辅助空间 O(1)

选择排序

  • 选择排序的工作原理
  • 首先在未排序排序序列中选择最小的元素,放到起始位置
  • 然后在剩余的选择最小的元素放到已排序的末尾
  • 直至所有元素有序

代码实现:

void selectionSort(vector<int> &arr) {
    int _len = arr.size();
    for (int i = 0; i < _len; i++) {
        int smallIdx = i;
        for (int j = i + 1; j < _len; j++) {
            if (arr[j] < arr[smallIdx]) {
                smallIdx = j;
            }
        }
        if (smallIdx != i)
            swap(arr[i], arr[smallIdx]);
    }
}

算法性能分析:

  • 最好情况时间:O(n^2)
  • 平均时间复杂度:O(n^2)
  • 最坏情况时间:O(n^2)
  • In-place sort,不稳定

插入排序

  • 插入排序,对于未排序的数据,从已排序的序列中向后扫描,找到相应位置并插入.
  • 插入排序在实现上,通常采用in-place排序(即只需用到O(1)}的额外空间的排序) 因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

算法步骤:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 把第二个元素到最后一个元素当成是未排序序列
  3. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  4. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。 (如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的 后面

代码实现:


void insertSort(vector<int> &arr) {
    int _len = arr.size();
    // arr[0]认为已经是有序的
    for (int i = 1; i < _len; i++) { // 从1开始执行!
        int key = arr[i]; // 拿到当前要移动的这个值
        int j = i - 1;// 前面是以前排序的
        while ((j >= 0) && (key < arr[j])) {
            arr[j + 1] = arr[j]; // 若值比arr[j]小,arr[j]向后移动一位
            j--;
        }
        arr[j + 1] = key; //找到了对应位置赋值
    }
}

算法性能分析:

  • 最优时间复杂度:当输入数组就是排好序的时候,复杂度为O(n),而快速排序在这种情况下会产生O(n^2)的复杂度。
  • 最差时间复杂度:当输入数组为倒序时,复杂度为O(n^2) ;
  • 插入排序比较适合 “少量元素的数组排序”!!!
  • 插入排序的复杂度和 逆序对的个数 一样. 当数组倒序时, 逆序对的个数为 n(n-1)/2 ; 此时复杂度为O(n^2) ;
  • 插入排序是 稳定的 in-place 算法!!!
// 链表的插入排序程序

归并排序

  • 归并: 将两个 有序 的序列合并成一个有序序列.
  • 归并采用的是 分治 的思想: 即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
  • 归并排序包括 从上往下从下往上 两种方式

算法步骤

  • 归并排序需要申请额外空间. 使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
  • 设定两个指针,最初位置分别为两个已经排序序列的起始位置(也可以是末尾)
  • 比较两个指针所指向的元素,选择相对小(大)的元素放入到合并空间,并移动指针到下一位置,重复步骤直到某一指针达到序列尾
  • 将另一序列剩下的所有元素直接复制到合并序列尾

从下往上的归并排序

  • 将待排序的数列分成若干个长度为1的子数列
  • 将这些数组两两合并;得到若干个长度为2的序列.
  • 再将这些序列两两合并…直至一个数列为止…
// array是排序的输入,copy是输出(有序)
void mergeSortCore(vector<int> &array, vector<int> &copy, int start, int end) {
    if (start == end) {
        copy[start] = array[start]; 
        return;
    }
    int mid = (end + start) / 2;
    mergeSortCore(copy, array, start, mid); // 注意:copy和array的参数顺序,经过排序后,array[start,end]是有序的!
    mergeSortCore(copy, array, mid + 1, end);

    int i = mid;
    int j = end;
    int copyIndex = end;
    while (i >= start&&j >= mid + 1) {
        if (array[i] > array[j]) { // 排序后
            copy[copyIndex--] = array[i--];
        }
        else {
            copy[copyIndex--] = array[j--];
        }
    }

    while( i >= start) {
        copy[copyIndex--] = array[i--];
    }
    while (j >= mid + 1) {
        copy[copyIndex--] = array[j--];
    }
    return;
}
// 从上到下归并排序算法!!!
void mergeSort(vector<int> &array) {
    int len = array.size();
    vector<int> copy(array.begin(), array.end());
    mergeSortCore(copy, array, 0, len - 1);
}

算法性能分析

  • 分治思想, 稳定 , 需要额外空间,非in-place
  • 最坏情况运行时间:O(nlgn)
  • 最佳运行时间:O(nlgn)
  • 最优时间复杂度 O(n)
  • 最差空间复杂度 O(n)

##快速排序

  • 在平均状况下,排序 n 个项目要Ο(n log n)次比较
  • 在最坏状况下则需要Ο(n^2)次比较,但这种状况并不常见。
  • 快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。

算法步骤:

  1. 从数列中选择一个作为 基准 . pivot
  2. 重新排序数组, 实现: 所有大于 pivot都在它后面, 小于pivot都在它前面
  3. 划分的这个步骤称作 分区 partition
  4. 递归的将两个分区进行排序.

代码实现:

int partition(vector<int> &array, int start, int end) {
    // 选择一个pivotIndex
    int pivotIndex = rand() % (end - start + 1) + start; // 随机选一个
    swap(array[pivotIndex], array[end]); // 将pivot换到末尾
    int small = start;
    for (int i = start; i < end; i++) {
        if (array[i] < array[end]) {
            swap(array[small], array[i]);
            small++;
        }
    }
    swap(array[small], array[end]);
    return small;
}

void quciSortCore(vector<int> &array, int start, int end) {

    if (start <= end) {
        int pivotIndex = partition(array, start, end);
        quciSortCore(array, start, pivotIndex - 1);
        quciSortCore(array, pivotIndex + 1, end);
    }

}
void quciSort(vector<int> &array) {

    int len = array.size();
    if (len <= 0)
        return;

    quciSortCore(array, 0, len - 1);

}

算法性能分析:

  • 算法改进:

    1 选取随机数作为枢轴。

    2 使用左端,右端和中心的中值做为枢轴元。

    扫描二维码关注公众号,回复: 4638825 查看本文章
  • 复杂度分析:

    1 最坏运行时间:当输入数组已排序时,时间为O(n^2)

    当然可以通过随机化来改进(shuffle array 或者 randomized select pivot),使得期望运行时间为O(nlogn)。

    2 最佳运行时间:O(nlogn)

    当输入数组的所有元素都一样时,不管是快速排序还是随机化快速排序的复杂度都为O(n^2)

    可以使用 三路快排

  • 特点: 不稳定, In-place


堆排序

  • C++ Heap 堆

  • C++优先队列

  • 堆:本身就是一个完全二叉树

    当二叉树的每个节点都大于等于它的子节点的时候,称为大顶堆,

    当二叉树的每个节点都小于它的子节点的时候,称为小顶堆,上图即为小顶堆。

算法步骤:

  • 将数组堆化…
  • 输出堆顶元素,再调整堆
  • 反复直至堆为空

代码实现:

#include <iostream>
#include <algorithm>
using namespace std;

void max_heapify(int arr[], int start, int end) {
	//建立父節點指標和子節點指標
	int dad = start;
	int son = dad * 2 + 1;
	while (son <= end) { //若子節點指標在範圍內才做比較
		if (son + 1 <= end && arr[son] < arr[son + 1]) //先比較兩個子節點大小,選擇最大的
			son++;
		if (arr[dad] > arr[son]) //如果父節點大於子節點代表調整完畢,直接跳出函數
			return;
		else { //否則交換父子內容再繼續子節點和孫節點比較
			swap(arr[dad], arr[son]);
			dad = son;
			son = dad * 2 + 1;
		}
	}
}

void heap_sort(int arr[], int len) {
	//初始化,i從最後一個父節點開始調整
	for (int i = len / 2 - 1; i >= 0; i--)
		max_heapify(arr, i, len - 1);// 先堆化,
	//先將第一個元素和已经排好的元素前一位做交換,再從新調整(刚调整的元素之前的元素),直到排序完畢
	for (int i = len - 1; i > 0; i--) {
		swap(arr[0], arr[i]);
		max_heapify(arr, 0, i - 1);
	}
}

int main() {
	int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4, 7, 0, 1, 8, 9, 7, 3, 1, 2, 5, 9, 7, 4, 0, 2, 6 };
	int len = (int) sizeof(arr) / sizeof(*arr);
	heap_sort(arr, len);
	for (int i = 0; i < len; i++)
		cout << arr[i] << ' ';
	cout << endl;
	return 0;
}

算法性能分析:

  • 平均时间复杂度:O(NlogN)
  • 空间复杂度: O(1)

希尔排序

  • [待续]

计数排序

  • 不是基于比较的排序算法
  • 在于将输入的数据值转化为键存储在额外开辟的数组空间中!!!
  • 计数排序要求输入的数据必须是 有确定范围的整数
  • 例如: 计数排序是用来排序0到100之间的数字的最好的算法 (统计年龄?)

算法步骤:

  1. 找出待排序的数组中最大和最小的元素;
  2. 统计数组中每个值为i的元素出现次数,存入数组C的第i项
  3. 对所有的计数累加
  4. 反向填充目标数组

代码实现:

void countingSort(vector<int> &arr) {

    // 先找出arr中的最大值
    int k = INT_MIN;
    for (auto x : arr) {
        if (x > k) {
            k = x;
        }
    }
    k += 1; // 为了让数组空间足够大!
    int _len = arr.size();

    int *p = new int[_len];  // [0,_len-1]
    int *q = new int[k + 1]; // [0,k]

    // 初始化arr中元素出现次数都为0
    for (int i = 0; i < k; i++) {
        q[i] = 0;
    }

    // 统计arr数组中元素的出现次数,存储到q中.
    // 值k的下标也为k
    for (int i = 0; i < _len; i++) {
        q[arr[i]]++;
    }

    // 将所有计数次数累加,即统计这个元素,以及它之前的元素共出现了几次
    // 确定了这个元素的最后位置+1
    for (int i = 1; i < k; i++) {
        q[i] = (q[i] + q[i - 1]);
    }

    // 将元素重新输入,次数最小为1,数组开始为0.所以要减一
    for (int i = 0; i < _len ; ++i) {
        int index = q[arr[i]] - 1;
        p[index] = arr[i];
        q[arr[i]]--; // 出现出现索引减1
    }

    // 将排序结果拷贝回去
    for (int j = 0; j < _len; j++)
        arr[j] = p[j];

    // 释放空间
    delete[] q;
    delete[] p;
}

算法性能分析:

  • 计数排序是一个稳定的排序算法

  • 当输入的元素是 n 个 0到 k 之间的整数时,

    时间复杂度是O(n+k),空间复杂度也是O(n+k)

  • 当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。


桶排序

基数排序

猜你喜欢

转载自blog.csdn.net/qjh5606/article/details/84930897