排序是在生活中经常遇到的事情,比如在学校时会按成绩排名,会按身高排座位。在逛电子商铺时,会按销售量/人气值筛选,刷微博时,会有热搜榜等。将很多无序的事情变的有序,事情就好处理了。其中用到的就是排序算法。
拿班主任给班级学生成绩排序来举例。
O(n^2)复杂度算法
冒泡排序
第一轮从成绩单中挑出来成绩最高的,第二轮从成绩单中挑出来成绩次高的。。。这么重复下去,直到班级内全部学生全部挑出来完。
因为每一次选出一个最好的,就像从水里冒出的气泡,所以叫冒泡排序。
void BubbleSort(vector<int> &sums)
{
// 第i轮次挑学生
for (int i = 0; i < nums.size() - 1; i++)
{
// 成绩单中还没被挑出来的学生
for (int j = 0; j < nums.size() - 1 - i; j++)
{
if (nums[j] < nums[j+1])
{
swap(nums[j],nums[j+1]);
}
}
}
}
插入排序
先把成绩单中第一个学生的成绩写到一张白纸的中间,如果第二个学生成绩比他高则写到第一位同学的上方,如果第二个学生成绩比他低,则写到第一个同学下方。等到了第三个同学的时候,拿第三名同学的成绩和白纸上的两个同学成绩比较,插入到相应的位置。排序的纸要留好空位,以方便插入后来的同学的名字。
这个排序算法就像平时大扑克摸牌一样,后摸的牌和手里的牌比较后,插入到手中合适的位置,所以叫插入排序。
void InsertSort(vector<int> &nums)
{
// 要插入到白纸上的同学的序号
for (int i = 1; i < nums.size(); i++)
{
int tmp = nums[i];
// j是白纸上的同学的末尾序号
int j = i - 1;
// 循环找出插入的位置
while (j >=0 && nums[j] < tmp)
{
nums[j+1] = nums[j];
j--;
}
nums[j+1] = tmp;
}
}
选择排序
从1轮从成绩单中选出成绩最高的同学,写到第一个位置,再选择次高的成绩,写到第二个位置。。。以此类推,很像像冒泡排序,不同的是:先假设成绩单中无序区的第一个学生成绩为最好,拿排好序的学生成绩最好的那个和这个成绩相比,并决定是否交换。
void SelectSort(vector<int> &nums)
{
for (int i = 0; i < nums.size() - 1; i++)
{
// 记录位置
int max_loc = i;
for (int j = i + 1; j < nums.size(); j++)
{
// 成绩单中无序区
if (nums[j] > nums[max_loc])
{
max_loc = j;
}
}
swap(nums[max_loc, nums[i]]);
}
}
O(nlogn)复杂度算法
上面3种排序都是复杂度为O(n^2)复杂度的算法,当数据量小的情况下,排序不怎么费时间。但是当数据量大的情况下,上面三种算法就显的太慢了。要想提高效率,就要让计算机少做事情(减少数据之间的相互比较)。
归并排序
把全班同学分成两组,分别排序,那么从每一组中挑选出一个最大的,就能省去一般的相互比较时间。那么:
1.就把整个班级一分为二,分别进行排序,再把两个有序数列合并成一个排好序的序列。相比排序,对有序数列的合并是很快的。
2.对班级分成2个组的排序,再分成4组,分别排序,那么就节省了3/4的时间。
3.4组分8组,8组分16组,直到每组分为2个元素,做一次对比就行。
4.合并有序数组,这样列表越来越大,知道成为一个班级中所有的人。
void Merge(vector<int> &nums, int low, int mid, int high)
{
int i = low;
int j = mid + 1;
vector<int> ltmp;
// 只要左右两个有序序列都有数,开始循环比较
while (i <= mid && j <= high)
{
if (nums[i] < nums[j])
{
ltmp.push_back(nums[i]);
i++;
}
else
{
ltmp.push_back(nums[j]);
j++;
}
}
// 循环结束,两个序列肯定有一个先没有数据
while (i <= mid)
{
ltmp.push_back(nums[i]);
i++;
}
while (j <= high)
{
ltmp.push_back(nums[j]);
j++;
}
for (int k = 0; k < high - low + 1; k++)
{
nums[low + k] = ltmp[k];
}
}
void MergeSort(vector<int> &nums, int low, int high)
{
// 至少有两个元素
if (low < high)
{
int mid = (low + high) / 2;
// 让左右数列有序
MergeSort(nums, low, mid);
MergeSort(nums, mid + 1, high);
// 合并
Merge(nums, low, mid, high);
}
}
快速排序
从班级学生成绩中随机选出来一个,比如我们选的是序号为0的学生,他的是70分。
1.归类:将班级所有学生成绩分成两部分,一部分是大于70分的,令一部分小于70分。
2.从上面得到的两部分学生中,分别采用第一步的方法再选出来一个学生,我们还假设选了两波人中最头部的那个学生,比如分别是50分和80分,并再分拨。
3.用同样的方法,四拨人变八拨。。。变十六拨,这样下去,所有的数字就会排序好。
int Partition(vector<int> &nums, int left, int right)
{
int tmp = nums[left];
while (left < right)
{
// 从右边找比tmp大的数,找到后把大的数放到左边
while (nums[right] <= tmp && left < right)
{
right--;
}
nums[left] = nums[right];
// 从左边找比tmp小的数,找到后放到右边
while (nums[left] >= tmp && left < right)
{
left++;
}
nums[right] = nums[left];
}
// 把tmp归位,此时left = right
nums[left] = tmp;
return left;
}
void QuickSort(vector<int> &nums, int left, int right)
{
if (left < right)
{
int mid = Partition(nums, left, right);
QuickSort(nums, left, mid - 1);
QuickSort(nums, mid + 1, right);
}
}
堆排序
这个涉及到树的知识,笔者就不深入写了。总的过程可以分为一下几部
1.建立堆
2.得到堆顶元素,为最大元素
3.去掉堆顶,将堆最后一个元素放到堆顶,此时可以通过一次调整,让堆重新变得有序。
4.堆顶元素为第二大元素。
5.重复步骤3,直到堆变空。
// 堆的向下调整
void Sift(vector<int> &nums, int low, int high)
{
// low:堆的根节点位置
// high:堆的最后一个元素的位置
// i是父节点,j是左孩子结点
int i = low;
int j = 2 * i + 1;
int tmp = nums[low];
while (j <= high)
{
// 如果有右孩子,且右孩子比左孩子小
if (j + 1 <= high && nums[j+1] < nums[j])
{
// j指向右孩子
j++;
}
if (nums[j] < tmp)
{
nums[i] = nums[j];
// 向下看一层
i = j;
j = 2 * i + 1;
}
// 如果tmp更大,把tmp放在这个根节点位置
else
{
nums[i] = tmp;
break;
}
}
nums[i] = tmp;
}
void HeapSort(vector<int> &nums)
{
int n = nums.size();
// 建立堆
for (int i = (n - 1 - 1) / 2; i >=0; i++)
{
// 建立堆的时候,调整的部分的根的下标
Sift(nums, i , n - 1);
}
for (int i = n - 1; i >= 0; i--)
{
// i指向当前堆的最后一个元素
swap(nums[0], nums[i]);
sift(nums, 0, i - 1);
}
}
总结
排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
冒泡 | O(n^2) | O(1) | 稳定 |
选择 | O(n^2) | O(1) | 不稳定 |
插入 | O(n^2) | O(1) | 稳定 |
快速 | O(nlogn) | O(logn),O(n) | 不稳定 |
归并 | O(nlogn) | O(n) | 稳定 |
堆 | O(nlogn) | O(1) | 不稳定 |
从算法的对比我们可以看出,很容易因为算法不合格,写出的代码浪费掉成千上万的计算机资源。我们平时做事,人和人的水平差一些,很可能做出来事的效率差别十倍。所以我们在智能时代,要提高自己的专业能力,并且要善于把大目标分解成小目标,不断拆解目标任务。