目录
快速排序链接: http://t.csdnimg.cn/MvZoV
默认从小到大排列
基本概念:
稳定性:就是在一组数据中, 相同数字在排序之后的相对位置是否发生变化.发生变化不稳定, 反之稳定
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
直接插入排序
基本思想: 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到 一个新的有序序列 。实际中我们玩扑克牌时,就用了插入排序的思想。
举个例子:
给一个无序数组.
先选择前两个进行比较, 设置一个交换规则
规则:将1下标的数据挖走存到变量 tmp 中, 然后 0 下标的值和 tmp 比较大小.如果 0 下标值大就到坑中, 这时 0 下标就留下一个坑,最后将 tmp 放入坑中, 这是基本思路, 以后循环这个思路即可.
再来比较第二次,看一下结果如何:
选择下标为1和2的数据进行比较:
然后选择下标为2和3的数据进行比较,以此类推.
我们来看一下代码
/**
* 直接插入排序
* 时间复杂度最坏:O(N^2)
* 最好:O(N) 数据越有序,越快
* 空间复杂度O(1)
* 稳定
* @param array
*/
public static void insertSort(int[] array) {
for(int i = 1; i < array.length; i++) {
int j = i-1;
int tmp = array[i];
for (;j>=0;j--) {
if(tmp < array[j]) {
array[j+1] = array[j];
}else {
break;
}
}
array[j+1] =tmp;
}
}
关注点:
1> 每次让 i 的值+1后, j的值总是 (i-1)
2> 判断tmp大于array[j] 直接跳出循环, 因为这次比较的值没有tmp大的话 (j--) 之后对应下标的值不会比tmp大, 因为越往前数组的值越小, 前面的都是排好序的
3> 最后要把剩余的坑给补上, array[j+1] = tmp; 为什么是j+1呢?因为是j+1;
4> 数据越有序,越快, 接下来会用到.
希尔排序(也叫缩小增量排序)
基本思想: 给定一个数组, 将数组分成若干组, 每组用直接插入排序的方法进行排序.
排序之后我们将分组减少直至一组就是全部数据进行排序.
刚开始分组很多,但是每组数据很少, 最后分组很少, 但是数据会更有序, 由直接插入排序4>的特点得出排序的速度也不会很慢.
举个例子:
给出一个无序数组,
1>总共八个数据.分为4组
然后开始对每一组进行排序
关注点:
为什么要让第1个数据和第5个数据进行比较呢?
我们关注最坏的情况,这样的话可以尽量让小的数据往前面靠
排序完之后减少分组,我们使用每次减少组数为上次的二分之一
到最后变为一组之后排序完成.
2>分为2组
排完序之后结果
3>分为一组进行直接插入排序.排序完成.
我们来看一下代码:
/**
* 希尔排序(缩小增量排序)
* 空间复杂度O(1)
* 时间复杂度O(N^1.3)
* 不稳定
* @param array
*/
public static void shellSort(int[] array) {
int gap = array.length;
while(gap > 1) {
gap /=2;
shell(array, gap);
}
}
/**
* 对每组进行插入排序
* @param array
* @param gap
*/
private static void shell(int[] array, int gap) {
for(int i = gap; i < array.length; i++) {
int j = i-gap;
int tmp = array[i];
for (;j>=0;j-=gap) {
if(tmp < array[j]) {
array[j+gap] = array[j];
}else {
break;
}
}
array[j+gap] =tmp;
}
}
关注点:
1> 分组的组数依次变为1/2;最初为0.5*数组的长度.当长度为1的时候在进行最后一次排序然后结束循环.
2> shell函数实现每一组的直接插入排序, 在最外层循环的时候 i 的值依次+1, 在 内部循环的时候通过j = i - gap来依此实现每个分组的数据的排序.其他地方和直接插入排序原理一样.
选择排序
基本思想: 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元 素排完 。
选择排序原理很简单, 我将提供两种实现的方法,
一种是: 遍历数组选出最小的数据放到数组的最前面, 然后进行将剩余的数据重复复同样的操作.
另一种是: 遍历数组选出最大的数据放到数组的最后面, 选出最小的数据放到数组的最前面, 然后将剩余的数据重复同样的操作. (有点小坑)
代码如下:
/**
* 选择排序:最小放前面
* 时间复杂度: O(N^2)
* 空间复杂度:O(1)
* 不稳定
* @param array
*/
public static void selectSort(int[] array) {
for(int i = 0; i < array.length; i++) {
int minIndex = i;
for(int j = i+1; j < array.length; j++) {
if(array[j] < array[minIndex]) {
minIndex = j;
}
}
swap(array, i, minIndex);
}
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
非常简单,不多赘述.
第二个代码:
/**
* 选择排序
* 两边同时
* @param array
*/
public static void selectSort2(int[] array) {
int left = 0;
int right = array.length-1;
while(left < right) {
int maxIndex = left;
int minIndex = left;
for(int i = left + 1; i <= right; i++) {
if(array[i] < array[minIndex]) {
minIndex = i;
}
if(array[i] > array[maxIndex]) {
maxIndex = i;
}
}
swap(array, left, minIndex);
//防止第一个是最大值被换到了其他位置
if(maxIndex == left) {
maxIndex = minIndex;
}
swap(array, right, maxIndex);
left++;
right--;
}
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
关注点:
讲一下其中的小坑,
我们在交换完数据之后会出现一种情况
举个例子:
本组数据最大值下标maxIndex为0, 最小值下标minIndex为1, 进行最小值的交换之后, 会出现这种情况
我们的最大值下标变为了1, 因为换跑了.
因此需要先进行一个判断操作,如果换的值的下标为最大值的下标的话, 我们将最大值下标重新赋值.
其他很容易理解.
冒泡排序:
非常简单, 直接上代码:
/**
* 冒泡排序
* 时间复杂度: O(N^2)
* 空间复杂度: O(1)
* 稳定
* @param array
*/
public static void bubbleSort(int[] array) {
for(int i = 0; i < array.length-1; i++) {
boolean flg = true;
for(int j = 0; j < array.length-i-1; j++) {
if(array[j] > array[j+1]) {
swap(array,j,j+1);
flg = false;
}
}
if(flg) {
break;
}
}
}
关注点:
1> n个数据需要进行排n-1次
2> 每一次排序后排序的次数少1次
3> 当我们一次排序(不管哪一次)后如果没有任何数据进行交换, 说明所有数据全部排列完毕,直接跳出循环.
堆排序
定义: 堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆 来进行选择数据。
简单来说就是 : 创建一个大根堆(从上到下数据依次减小, 同一层的不做要求), 大根堆创建之后将最大的数据也就是在 0 下标的数据与最后一个数据换一下, 然后重新向下调整, 直到end==0的那时候, 数组就但从小到大的方式排列完毕了.
代码实现:
/**
* 堆排序
* 先创建一个大根堆
* 向下调整
* 给出父亲从上往下一次比较大小
* 先让子树比较找出较大的
* 再让子树和父亲比较
* 循环给出父亲节点
* 时间复杂度:O(N*logN)
* 空间复杂度:O(1)
* @param array
*/
public static void heapSort(int[] array) {
createHeap(array);
int end = array.length-1;
while(end > 0) {
swap(array,end,0);
siftDown(array,0,end);
end--;
}
}
private static void siftDown(int[] array, int parent, int length) {
int child = 2*parent + 1;
while(child < length) {
if(child+1 < length && array[child] < array[child+1]) {
child++;
}
if(array[child] > array[parent]) {
swap(array,child,parent);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
/**
* 从最后一棵子树开始
* @param array
*/
private static void createHeap(int[] array) {
for(int parent = (array.length-1-1)/2; parent >= 0; parent--) {
siftDown(array, parent, array.length);
}
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
代码详解:
private static void createHeap(int[] array) {
for(int parent = (array.length-1-1)/2; parent >= 0; parent--) {
siftDown(array, parent, array.length);
}
}
首先时怎么创建一个大根堆?
在上面的代码中, 为什么parent 从(array.length-1-1)/2开始?
如图:我们有最后一个数据的下标为array.length-1,
由此得出它的父亲节点下标为 (array.length-1-1)/2
然后进行向下调整:
private static void siftDown(int[] array, int parent, int length) {
int child = 2*parent + 1;
while(child < length) {
if(child+1 < length && array[child] < array[child+1]) {
child++;
}
if(array[child] > array[parent]) {
swap(array,child,parent);
parent = child;
child = 2*parent+1;
}else {
break;
}
}
}
我们由父亲节点得到儿子节点, 进行循环
if()里面的判断语句 (child +1 < length)
当儿子节点小于数组的长度的时候 才能进行排序, 排序过程中, 需要先判断这个树是否有 右节点, 如果有右节点, 选择更大的儿子节点和父亲节点作比较, 判断完毕之后, 如果儿子节点大, 交换他俩的位置, 然后把儿子节点作为父亲节点重复进行上述操作
反之, break
直接跳出循环, 因为父亲节点更大的话, 那么一定比下面已经排好的节点更大
创建大根堆完毕, 通过向下调整的方式进行堆排序.
public static void heapSort(int[] array) {
createHeap(array); //此处创建一个大根堆.
int end = array.length-1;
while(end > 0) {
swap(array,end,0);
siftDown(array,0,end);
end--;
}
}
end == array.length-1
选择最后一个数据下标
swap (array, end, 0);
循环将最后一个数据 0 下表数据进行交换, 这样最大的数据就到最后面了.
end--
最后一个数据不再进行堆排序, 会一直停在最后面
先进行 end-- , 还是先进行 siftDown() 操作呢?
先进行 siftDown 操作, 因为传过去的应该是数组的长度, 所以要填 5 , 而总数组的长度为 6
第一次传过去 5 的话, 最多下标到 4.
最后当end == 0时停止排列, 就剩这一个了.
归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使 子序列段间有序。若将两个有序表合并成一个有序表,成为二路归并.
简单理解为: 使用递归的方法, 将一个数组平均分为两组, 两组, 两组....最后将每一组排序后合并
给定一个数组:
一层层分解后合并
对于每一组的数据每次分解后选择相应的start, end, mid进行拆分, 什么时候停止呢? 当start >= end时停止开始递归中的" 归 "的操作, 这个操作中需要对数组进行排列.如何进行排列呢? 通过数组的下表进行排列
就以上图的第三行数据到第二行数据的左半部分为例进行说明:当" 归 "到第二行数据如图:
第三层完成递归后,将第三层右边的数和第三层左边的数作比较, 两边的数据放到一个大小为4个数据的新数组中.
让s1和s2作比较, 谁小先入数组中, 如图s2小放入后, s2++, 再让s1和e2比较,s1小放入后s1++, 再让e1和e2比较, e2小放入数组中, s2++因为s2大于e2, 停止比较, e1的值放入数组中.
代码展示:
/**
* 归并排序
* 时间复杂度:O(N*logN)
* 空间复杂度:O(N)
* 3个稳定的排序:直接插入,冒泡,归并
* 稳定的
* @param array
*/
public static void mergeSort(int[] array) {
mergeSortFun(array,0,array.length-1);
}
private static void mergeSortFun(int[] array, int start, int end) {
if(start >= end) {
return;
}
int mid = (start+end)/2;
mergeSortFun(array, start, mid);
mergeSortFun(array, mid+1, end);
//合并
merge(array,start,mid,end);
}
private static void merge(int[] array, int left, int mid, int right) {
int s1 = left;
int e1 = mid;
int s2 = mid+1;
int e2 = right;
int[] tmpArr = new int[right-left+1];
int k = 0;
while(s1<=e1 && s2<=e2) {
if(array[s1] <= array[s2]) {
tmpArr[k++] = array[s1++];
}else {
tmpArr[k++] = array[s2++];
}
}
while(s1<=e1) {
tmpArr[k++] = array[s1++];
}
while(s2<=e2) {
tmpArr[k++] = array[s2++];
}
for(int i = 0; i < tmpArr.length; i++) {
array[i+left] = tmpArr[i];
}
}
/**
* 非递归实现归并排序
* @param array
*/
public static void mergeSortNor(int[] array) {
int gap = 1; //每组几个数据
while (gap < array.length) {
for(int i = 0; i < array.length; i=i+gap*2) {
int left = i;
int mid = left + gap - 1;
int right = mid + gap;
if(mid >= array.length) {
mid = array.length-1;
}
if(right >= array.length) {
right = array.length-1;
}
merge(array,left,mid,right);
}
gap*=2;
}
}
注意: 当进行右边的数据的传输时, tmpArr的下标从0开始, 而array的下标, 从0+start开始.
非基于比较排序(计数排序)
给一组数据:
2 3 4 5 6 2 2 1 1 1 4 3 6 7 9 8 3 3 , 这组数据无序
首先给出的数据都是0 - 9之间的.可以申请一个数组长度为10, 下标为 0 - 9
这组数据中的数是几就放到那个下标下面, 放一次就这个下标存的数据就加一, 默认刚开始为1
步骤:
1> 申请一个数组, 10个大小
2> 遍历数组, 把数字对象的 count 数组的下标进行++
3> 遍历计数数组, 写回到原来数组 array.
注意如果这个数在 90- 99之间呢?
91 93 94 93 91 92
这时不用申请 长度为 100 的数组, 而是找到min = 90, max = 99, 申请大小为 (max - min+1)
怎么放下标呢?
使用这样的写法: count[array[i] - min].
代码实现:
/**
* 计数排序的场景:
* 指定范围内的数据
* 时间复杂度:O(MAX(N,范围))
* 空间复杂度:O(范围)
* 稳定性:稳定
* @param array
*/
public static void countSort(int[] array) {
int minVal = array[0];
int maxVal = array[0];
for(int i= 1; i < array.length; i++) {
if(array[i] < minVal) {
minVal = array[i];
}
if(array[i] > maxVal) {
maxVal = array[i];
}
}
int len = maxVal-minVal+1;
int[] count = new int[len];
for(int i = 0; i < array.length; i++) {
count[array[i]-minVal]++;
}
int index = 0;
for(int i = 0; i < count.length; i++) {
while (count[i] > 0) {
array[index] = i+minVal;
index++;
count[i]--;
}
}
}
小结
排序算法分类:
1> 分治 归并算法, 快速排序
2> 基于插入 希尔排序, 直接插入排序
3>基于选择 堆排序, 直接选择排序
关于几个排序的特点:
直接插入排序: 元素越有序效率越高, 优化的时候也可能会使用到
希尔排序: 对直接插入排序的优化, 时间复杂度不固定
直接选择排序: 很好理解, 但不常用
堆排序: 使用对来选数, 效率高了很多
冒泡排序: 容易理解
快速排序: 整体的综合性能 和 使用场景都比较好
归并排序: 缺点在于需要 O(N) 的空间复杂度, 归并排序思考更多的是解决在磁盘外中的外排序问题.
计数排序: 数据范围集中时效率很高, 但是使用范围有限.