冒泡排序(稳定排序)
(1)算法思路
冒泡排序的思想就是比较当前数和后一个数的大小,将较大的数往后移动,这样可以确保一轮下来能将最大的数放在数组的最末端。然后重复此操作即可完成排序。
(2)算法步骤
上面第一轮比较完,我们可以看到最大的数5已经被放在了最端,此时我们只需要将去掉最大的数的那部分(2,3,1,4)进行重复的操作。
(3) 算法描述
void bubbleSort(vector<int>& arr) {
int n = arr.size();
for (int i = 0; i < n - 1; ++i) {
for (int j = 0; j < n - i - 1; ++j) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
(4)时间复杂度
冒泡排序最好的时间复杂度为 O(n),冒泡排序的最坏时间复杂度为O(n2)
综上,因此冒泡排序总的平均时间复杂度为O(n2)
归并排序(稳定排序)
(1)算法思路
待排序的元素分解成两个规模大致相等的子序列。如果不易分解,将得到的子序列继续分解,直到子序列中包含的元素个数为1。因为单个元素的序列本身就是有序的,此时便可以进行合并,从而得到一个完整的有序序列。即先使每个子序列有序,再使子序列段间有序。
(2)算法步骤
(1)分解:
将待排序的元素分成大小大致一样的两个子序列。
(2)治理:
对两个子序列进行个并排序。
(3)合并:
将排好序的有序子序列进行合并,得到最终的有序序列。
(3) 算法描述
mergeSort(nums, 0, nums.size()-1);
//归并排序
void mergeSort(vector<int>& nums, int left, int right) {
if(left < right) {
int mid = (left + right) / 2;
mergeSort(nums, left, mid); //对 nums[left,mid]进行排序
mergeSort(nums, mid+1, right); //对 nums[mid+1,right]进行排序
merge(nums, left, mid, right); //进行合并操作
}
}
//合并函数
void merge(vector<int>& nums, int left, int mid, int right) {
vector<int> temp(right-left+1);
int i = left, j = mid+1, k = 0;
while(i <= mid && j <= right) {
if(nums[i] <= nums[j]) {
temp[k++] = nums[i++]; //按从小到大存放在 temp 数组里面
} else {
temp[k++] = nums[j++];
}
}
while(i <= mid) { // j 序列结束,将剩余的 i 序列补充在 temp 数组中
temp[k++] = nums[i++];
}
while(j <= right) { // i 序列结束,将剩余的 j 序列补充在 temp 数组中
temp[k++] = nums[j++];
}
for(int i = left, k = 0; i <= right; ++i) {
nums[i] = temp[k++];
}
}
(4)时间复杂度
从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
快速排序(不稳定排序)
(1) 算法思路
它的基本思想是将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据小,然后再按此方法对这两部分数据进行快速排序,整个排序过程可以递归进行,以此使所有数据变成有序序列。
先从数列中取出一个元素作为基准元素。依基准元素为标准,将问题分解为两个子序列,使小于等于基准元素的子序列在左侧,使大于基准元素的子序列在右侧。
(2) 算法步骤
假设当前的待排序的序列为 R[low, high] , 其中 low <= high。同时选取首元素为基准元素。
-
步骤一:选取首元素的第一个元素作为基准元素 pivot = R[low] ,i = low ,j = high;
-
步骤二:用 j 从右向左扫描,找第一个小于等于 pivot 的数,如果找到,R[i] 和 R[j] 交换 ,i++;
-
步骤三:用 i 从左向右扫描,找第一个大于 pivot 的数,如果找到,R[i] 和 R[j] 交换,j–;
-
步骤四:重复步骤二 ~ 步骤三,直到 j 与 i 的指针重合,返回基准元素位置 mid = i 。
至此为一趟排序,此时以 mid 为界线,将数据分割为两个子序列,左侧子序列都比 pivot 数小,右侧子序列都比 pivot 数大,然后再进行递归,分别对这两个子序列进行快速排序。
(3) 举例图解
以序列(30,24,5,58,18,36,12,42,39)为例,进行图解。
- 初始化,i = low ,j = high,pivot = R[low] = 30。如下图所示:
- 从右向左扫描,从数组的右边位置向左找,一直找一个小于等于 pivot 的数,找到R[j] = 12,R[i]与R[j]交换,i++。如下图所示:
- 从左向右扫描,从数组的左边位置向右找,一直找到一个比 pivot 大的数,找到 R[i] = 58,R[i] 与 R[j] 交换 ,j–。如下图所示:
- 从右向左扫描,从数组的右边位置向左找,一直找到小于等于 pivot 的数,找到R[j] = 18,R[i]与R[j]交换,i++。如下图所示:
-
直到 j 与 i 的指针重合,则找到了基准元素的位置 i ,保证左侧子序列都小于等于基准元素,右侧子序列都大于基准元素。说明基准元素 pivot = 30 找到了其正确排序后的位置。
-
然后再分别对这两个序列(12,24,5,18)和(36,58,42,39)进行快速排序(递归)。
(4) 算法描述
// 对数组nums在区间[low : high]内进行快速排序
void quickSort(vector<int>& nums, int low, int high) {
if(low < high) {
int mid = part(nums, low, high); //返回基准元素位置
quickSort(nums, low, mid-1); //左区间递归快速排序
quickSort(nums, mid+1, high); //右区间递归快速排序
}
}
// 划分函数:确定基准元素的位置,使其左边序列都小于等于基准元素,右边序列都大于基准元素
int part(vector<int>& nums, int low, int high) {
int midNum = nums[low]; //基准元素
while(low < high) {
// 从右向左开始找一个小于等于midNum的数值
while(low < high && nums[high] > midNum) {
--high;
}
// 找到后交换nums[low]和nums[high],同时low向右移动一位
if(low < high) {
swap(nums[low++], nums[high]);
}
// 从左向右开始找一个大于midNum的数值
while(low < high && nums[low] <= midNum) {
++low;
}
// 找到后交换nums[low]和nums[high],同时high向左移动一位
if(low < high) {
swap(nums[low], nums[high--]);
}
}
return low;
}
堆排序(不稳定排序)
(1) 算法思路
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏最好,平均时间复杂度均为O(nlogn),是不稳定排序。
堆是具有以下性质的完全二叉树,每个节点的值都大于或等于其左右孩子节点的值,称为大顶堆。每个结点的值都小于或等于其左右孩子节点的值,称为小顶堆。注意:没有要求节点的左孩子的值和右孩子的值的大小关系。
大顶堆举例:
对堆中的节点按层进行编号,映射到数组中就是下面这个样子:
(2) 算法步骤
- 将待排序序列构造成一个大顶堆,构造步骤如下:
- 从最后一个非叶子节点开始,对应数组下标
n/2 - 1
,找其子节点,看是否比它大,若比它大的话,就交换节点,交换之后继续向下检查; - 再继续找上面的非叶子节点,进行相同操作;
- 直到根节点交换完成后,此时根节点就是最大的。
- 从最后一个非叶子节点开始,对应数组下标
- 此时整个序列的最大值就是顶堆的根节点
- 将其与末尾元素进行交换,此时末尾就为最大值
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。
可以看到在构建大顶堆的过程中,元素的个数逐渐减少,最后就得到一个有序序列了
(3) 代码实现
void adjustHeap(vector<int>& v, int n, int i) {
int largest = i;
int left = 2 * i + 1, right = 2 * i + 2;
if(left < n && v[left] > v[largest]) {
largest = left;
}
if(right < n && v[right] > v[largest]) {
largest = right;
}
// 如果最大值不是根节点
if(largest != i) {
swap(v[i], v[largest]);
adjustHeap(v, n, largest); // 递归地堆化子树
}
}
void heapSort(std::vector<int>& arr) {
int n = arr.size();
// 构建堆(重组数组)
for(int i = n/2 - 1; i >= 0; --i) {
adjustHeap(arr, n, i);
}
for(int i = n-1; i > 0; --i) {
swap(arr[0], arr[i]); // 将当前根节点移动到数组的末尾
adjustHeap(arr, i, 0); // 调整剩余元素以保持堆性质
}
}