一.冒泡排序
1.算法思想
冒泡排序,顾名思义,就是在每一趟中将最大的数沉到水底,也就是将最大的数移动到末尾位置,一共进行n-1次
冒泡排序的整体思想是:
1.左右两两比较,大的向后移动,小的向前移动
2.每一趟都会使当前所比较过的元素中最大的那个移动到最后位置
3.一共n-1趟即可,因为对于一个数组来说,如果已经确定好了n-1个数字,又因为一个数组只有n个数字,所以剩下的那一个数字的位置也就唯一确定了
2.代码实现
void BubbleSort(int* a, int n) { int i = 0; for (i = 0; i < n - 1; i++)//一共冒泡n-1趟(i控制趟数) { int flag = 1;//假设有序 int j = 0; for (j = 0; j < n - 1 - i; j++)//(j控制每一趟比较的次数) //第一趟比较n-1次,第二趟比较n-2次(因为第二趟就不需要比较最后一个数字了,因为最后一个数字已经是最大的了)....,归纳为n-1-i次 { if (a[j] > a[j + 1]) { Swap(&a[j], &a[j + 1]); flag = 0;//发生交换,改为无序 } } if (flag == 1)//有序,所以无需再进行排序,直接break { break; } } }
这是冒泡排序的一种优化版本,使用flag标志数组是否已经有序,如果有序,则跳出循环,排序成功.
3.冒泡排序的时间复杂度与稳定性
冒泡排序的时间复杂度是O(N^2),具有稳定性
至于为什么冒泡排序具有稳定性呢?
是因为冒泡排序在每一趟的比较过程中只有当左边元素大于右边元素时才会发生交换,而当左边元素等于右边元素时并不会发生交换,所以冒泡排序具有稳定性
4.冒泡排序,插入排序,选择排序的优劣介绍
插入排序比冒泡排序好,冒泡排序比选择排序好
前面提到过,当数组是有序或者部分有序时,插入排序有奇效(时间复杂度能达到O(N)!!!)
可是我们刚才进行的冒泡排序的优化也可以减少一些不必要的比较啊,那么它们两个到底谁更好呢?
下面给大家举个例子.
总结:优化版本冒泡排序
时间复杂度:O(N^2)
最好排序:O(N)
最差:O(N^2)
1.跟直接插入排序相比来说,直接插入更好
因为:
插入排序的任意性更强,对有序,接近有序,局部有序的适用性更强,所以说直接插入排序是在O(N^2)中的排序算法中非常好的一个算法了
2.很直接选择排序相比来说,冒泡排序更好,因为直接选择排序无论数组是否有序,都进行N^2次,而冒泡排序针对有序或部分有序有一些优化,避免了一些没有任何必要的比较
3.但是数组无序的情况下,冒泡排序还是效率比较差的
二.快速排序
1.算法思想
快速排序是一个非常强大的排序算法,能够很快地解决大多数排序问题,但是要理解起来不是很容易,我们先来看一下快排的单步思想
那么我们该如何实现让6的左边都比6小,6的右边都比6大呢?
下面我们介绍一下"挖坑"法,pivot:英文单词"坑"
我们来看这几张图片
这就是单趟快排的示意图.
2.单趟快排的代码实现
接下来,我们先写一下单趟快排的代码
void QuickSort(int* a, int n) { int begin = 0; int end = n - 1; int pivot = begin; int key = a[begin]; while (begin < end) { //左边有坑,右边找小,放到左边 while (a[end] >= key && begin < end) { end--; } if (begin < end) { //小的放到左边的坑里,自己形成了新的坑位 a[pivot] = a[end]; pivot = end; } //右边有坑,左边找小,放到右边 while (a[begin] <= key && begin < end) { begin++; } if (begin < end) { //大的放到右边的坑里,自己形成了新的坑位 a[pivot] = a[begin]; pivot = begin; } } pivot = begin; a[pivot] = key; }
可见,单趟排序的时间复杂度是O(N),因为begin和end要相遇,且每轮循环只能有一个"指针"在变动,这里所说的"指针"不是C语言所说的指针,而是一种标记而已
3.快排整体思想和代码实现
这时,我们成功让6的左边都小于6,6的右边都大于6,
此时我们想:
如果能让6的左边变为有序,右边也变为有序,那么不就整体有序了吗?
所以在这里我们呢考虑使用分治的思想,也就是递归的方法,也就是缩短区间(大事化小,将一个大问题拆解为若干个子问题,然后逐个击破)
因为当区间缩小到只剩一个值的时候,它一定有序
下面我们来看一张图片
我们发现,只需要对第一次分割出来的左区间和右区间分别进行多次挖坑法,就可以实现整体有序,
其实我们发现,这个快排的递归思想就像是二叉树的深度优先遍历.
下面我们用代码实现一下,如果感觉理解不了下面代码中的这种递归的方式,请仔细看一下上面这张图片,上面把递归调用的整个过程划分的很详细
void QuickSort(int* a, int left,int right)//这里为了进行函数递归,把形参改为左右"指针",方便进行递归 { if (left >= right)//当区间长度小于等于1时,无需进行排序了,如果不加这一条,函数将进入死递归 { return; } int begin = left; int end = right; int pivot = begin; int key = a[begin]; while (begin < end) { //左边有坑,右边找小,放到左边 while (a[end] >= key && begin < end) { end--; } if (begin < end) { //小的放到左边的坑里,自己形成了新的坑位 a[pivot] = a[end]; pivot = end; } //右边有坑,左边找小,放到右边 while (a[begin] <= key && begin < end) { begin++; } if (begin < end) { //大的放到右边的坑里,自己形成了新的坑位 a[pivot] = a[begin]; pivot = begin; } } pivot = begin; a[pivot] = key; //这里我们将[left,right]这个区间分为这么三个部分 //[left,pivot-1] pivot [pivot+1,right] QuickSort(a, left, pivot - 1); QuickSort(a, pivot + 1, right); //快排的递归思想就像是二叉树的深度优先遍历. //根 左区间 右区间 根 左区间 右区间 根 左区间 右区间 根 左区间 右区间 ........., //单个区间就像是叶子节点 }
上面就是快排的基础代码(还未经过优化的)