冒泡排序是遍历整个序列,并相邻两元素两两比较,如果反序就交换位置,最终就将最大的数放到序列末尾。遍历第二次就将倒数第二大的数放到了倒数第二的位置,重复该操作,直到遍历n-1次后,整个序列完成排序。
void bubble_sort(vector &arr)
{
// 外层循环:逐趟扫描
// i>1的原因:只有一个元素的数组自然有序
int len = arr.size();
for (int i = len; i > 1; i--)
{
int swapped = 0; // 是否发生了交换的标志
// 一趟扫描
for (int j = 1; j < i; j++)
{
if (arr[j - 1] > arr[j])
{
swap(arr[j - 1], arr[j]);
swapped = 1;
}
}
if (!swapped) break; // 若无交换发生,结束
}
}
使用了一个标志位 int swapped = 0; // 是否发生了交换的标志
作为哨兵,当某次没有出现交换提前结束循环,作为对冒泡排序算法的优化。
遍历整个序列,找到最小元素的位置,然后将其放到序列最开始作为已排序序列。然后再从剩余的序列中找到最小的元素放在已排序序列后面。依次类推直到所有元素排列完毕。
选择排序与冒泡排序的区别是:选择排序遍历完整个序列才交换一次;而冒泡排序是两两比较视情况交换,所以每遍历一个元素都可能交换。
void selection_sort(vector &arr)
{
int i, j, min;
int len = arr.size();
for (i = 0; i < len; i++)
{
min = i; //默认已排序的后一个元素最小
for (j = i + 1; j < len; j++)
{
if (arr[j] < arr[min])
min = j;
}
if( min != i) swap(arr[min], arr[i]);//进行交换
}
}
// 采用两层循环实现的方法。
// 参数arr是待排序数组的首地址,len是数组元素的个数。
void select_sort1(vector &arr)
{
int len = arr.size();
if (len < 2) return; // 数组小于2个元素不需要排序。
int ileft, iright; // 每趟排序的最左和最右的位置。
int ii; // 每趟排序的元素位置计数器。
int iminpos; // 每趟循环选出的最小值的位置(数组的下标)。
int imaxpos; // 每趟循环选出的最大值的位置(数组的下标)。
ileft = 0; iright = len - 1; // ileft从0开始,iright从len-1开始。
while (ileft < iright)
{
iminpos = imaxpos = ileft;
for (ii = ileft; ii <= iright; ii++) // 每趟循环从ileft和iright之间选取元素。
{
// 找出值更小的元素,记下它的位置。
if (arr[ii] < arr[iminpos]) iminpos = ii;
// 找出值更大的元素,记下它的位置。
if (arr[ii] > arr[imaxpos]) imaxpos = ii;
}
// 如果本趟循环的最小的元素不是最左边的元素,则交换它们的位置。
if (iminpos != ileft) swap(&arr[ileft], &arr[iminpos]);
// 如果imaxpos的位置是ileft,在上面的代码中ileft已被交换到了iminpos的位置。
// 所以imaxpos的值要修改为iminpos。
if (imaxpos == ileft) imaxpos = iminpos;
// 如果本趟循环的最大的元素不是最右边的元素,则交换它们的位置。
if (imaxpos != iright) swap(&arr[iright], &arr[imaxpos]);
ileft++;
iright--;
}
}
直接插入排序,也简称为插入排序。基本思想是:假设左边i个元素已经排好序,从i开始,从左向右开始遍历,将遍历到的元素放在已排序列中第一个小于该元素的元素后面。
直接插入排序对于最坏的情况(严格递减的序列),需要比较和移位的次数为n(n-1)/2;对于最好的情况(严格递增的序列),需要比较的次数为n-1,需要移位的次数为0。
void InsertSort(vector& nums) {
int n = nums.size();
if (n <= 1) return;
for (int i = 1; i < n; i++) { //需要比较的数的下标
for (int j = i; j > 0; j--) {//从后向前找
//循环交换
if (nums[j] < nums[j - 1]) swap(nums[j], nums[j - 1]);
else break;
}
}
return;
}
插入排序的不足之处:
1、寻找插入位置、移动元素
优化方案:
1、对于已经排序好的序列,使用二分查找算法。(lower_bound()函数)
2、数据链表化
3、希尔排序
希尔排序也是一种插入排序,是在简单插入排序基础上改进的一个高效版本,也称为缩小增量排序。该算法是第一批冲破O(n2)的算法之一。利用了插入排序的最佳时间代价特性,它试图将待排序序列变成基本有序的,然后再用插入排序来完成排序工作。
基本思想是将待排序的数分成多组,对每个组进行插入排序,是数据整体有序,然后调整分组的数据顺序,最后再使用一次插入排序,使数据全部有序。希尔排序在此基础上采用跳跃分组的策略,通过增量(跳跃的量化)将序列划分为若干组,然后分组进行插入排序,接着缩小增量,继续按组进行插入排序操作,直至增量为1。采用这个策略的希尔排序使整个序列在初始阶段从宏观上看就基本有序,小的基本在前面,大的基本在后面。然后缩小增量至增量为1时,多数情况下只需要微调就可以了,不涉及到大量的数据移动。
const int INCRGAP = 3;//增量初始化
void ShellSort(vector &nums)
{
int len = nums.size();
unsigned gap = len / INCRGAP + 1; // 步长初始化,注意如果当len=1
{
for (unsigned i = gap; i < len; ++i) // 分组,在每个子序列中进行插入排序
{
unsigned j = i;
while (j >= gap && nums[j] < nums[j - gap])//直接插入排序
{
swap(nums[j], nums[j - gap]);
j -= gap;
}
}
gap = gap / INCRGAP;
}
}
快速排序是一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-onquerMethod)。它的平均时间复杂度为O(nlogn),最坏时间复杂度为O(n^2)。
具体算法步骤为:
1、先从待排序数组中找到一个基准数
2、扫描数组,把比基准小的放在他的左边,大的放在右边(得到两个区间)。
3、再对左右区间重复 1 、2的操作。
void quicksort(int* arr, unsigned int len)
{
if (len < 2) return; // 数组的元素小于2个就不用排序了。
int itmp = arr[0]; // 选取最左边的数作为中心轴。
int ileft = 0; // 左下标。
int iright = len - 1; // 右下标。
int imoving = 2; // 当前应该移动的下标,1-左下标;2-右下标。
while (ileft < iright)
{
if (imoving == 2) // 移动右下标的情况。
{
// 如果右下标位置元素的值大于等于中心轴,继续移动右下标。
if (arr[iright] >= itmp) { iright--; continue; }
// 如果右下标位置元素的值小于中心轴,把它填到左下标的坑中。
arr[ileft] = arr[iright];
ileft++; // 左下标向右移动。
imoving = 1; // 下次循环将移动左下标。
continue;
}
if (imoving == 1) // 移动左下标的情况。
{
// 如果左下标位置元素的值小等于中心轴,继续移动左下标。
if (arr[ileft] <= itmp) { ileft++; continue; }
// 如果左下标位置元素的值大于中心轴,把它填到右下标的坑中。
arr[iright] = arr[ileft];
iright--; // 右下标向左移动。
imoving = 2; // 下次循环将移动右下标。
continue;
}
}
// 如果循环结束,左右下标重合,把中心轴的值填进去。
arr[ileft] = itmp;
quicksort(arr, ileft); // 对中心轴左边的序列进行排序。
quicksort(arr + ileft + 1, len - ileft - 1); // 对中心轴右边的序列进行排序。
}
1、选择更加合理的基准数(有效降低递归深度),选取多个数,选择中间的数据。
2、区间元素很少,采用插入排序更加好。
归并排序是利用归并的思想实现的排序方法。该方法采用分治策略(分治法将问题分解为一些小问题,然后递归求解;而治阶段将分段得到的答案合并在一起。实际是将已经有序的子序列合并得到另外一个有序的序列。
具体算法步骤为:
1、先拆分,后合并起来。
这种结构类似于完全二叉树,归并排序需要使用递归或者迭代的方法实现。
分阶段就是递归拆分子序列,递归深度为log2n。治阶段需要将两个已经有序的子序列相互比较再合并。
class Solution {
public:
void MergeSort(vector& nums) {
int n = nums.size();
if (n <= 1) return;
int left = 0, right = n - 1;
Sort(nums, left, right);//开始归并排序
}
void Sort(vector &nums, int left, int right) {
if (left >= right) return;
int mid = (left + right) / 2;//将序列分为2部分
Sort(nums, left, mid);//处理左半部分序列
Sort(nums, mid + 1, right);//处理右半部分序列
/***下面是对序列进行排序合并处理,可以单独用一个函数完成***/
vector temp;//排序的临时容器
int i = left, j = mid + 1;
while (i <= mid && j <= right) {//对两个序列进行排序并合并
if (nums[i] <= nums[j])
temp.push_back(move(nums[i++]));
else
temp.push_back(move(nums[j++]));
}
while (i <= mid) temp.push_back(move(nums[i++]));
while (j <= right) temp.push_back(move(nums[j++]));
for (int k = 0; k < right - left + 1; k++)
{
nums[left + k] = temp[k];
}
}
};