排序
实际开发中,为什么我们更倾向于使用插入排序而不是冒泡排序?
答:从代码实现上来看,冒泡排序的数据交换要比插入排序的数据移动要复杂,冒泡排序需要3个赋值操作,而插入排序只需要1个,所以在对相同数组进行排序时,冒泡排序的运行时间理论上要长于插入排序。
排序算法的执行效率
1.最好情况,最坏情况,平均情况时间复杂度
2.时间复杂度的系数,常数,低阶(在对同一阶时间复杂度的排序性能对比的时候,我们就要把系数,常数,低阶考虑进来)
3.比较次数和交换(移动)次数
排序算法的内存消耗
原地排序,原地排序算法特指空间复杂度为O(1)的排序算法,
排序算法的稳定性
如果待排序的序列中存在值相等的元素,经过排序之后,相等元素之间原有的先后顺序不变,稳定的就是经过排序之后,两个相同元素之间的顺序没有变化。
冒泡排序
冒泡排序只会操作相邻的两个数据,每次冒泡操作都会对相邻的两个元素进行比较,看是否满足大小关系要求,不满足就互换,(想做鱼吐泡泡,排序后的越靠后越大,想象泡泡上浮的过程,越变越大)
冒泡是原地排序算法,是稳定的,时间复杂度最好的是O(n),最差的就是O(n2)
package sort;
public class bubbleSort {
public static int[] bubbleSort(int[] array) {
if (array.length == 0) {
return array;
}
for (int i = 0; i < array.length; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j + 1] < array[j]) {
int tmp = array[j + 1];
array[j + 1] = array[j];
array[j] = tmp;
}
}
}
return array;
}
/*
修改后的冒泡排序
传统的冒泡算法每次排序只确定了最大值,
我们可以在每次循环之中进行正反两次冒泡,分别找到最大值和最小值,如此可使排序的轮数减少一半。*/
public static int[] bubbleSort2(int[] array) {
int low = 0;
int high = array.length - 1;
while (low < high) { //正向冒泡,确定最大值
for (int i = low; i < high; i++) { //如果前一位大于后一位,交换位置
if (array[i] > array[i + 1]) {
int temp = array[i];
array[i] = array[i + 1];
array[i + 1] = temp;
}
}
--high;
for (int j=high;j>low;--j){ //反向冒泡,确定最小值
if(array[j]<array[j-1]){ //如果前一位大于后一位,交换位置
int temp = array[j];
array[j] = array[j-1];
array[j-1] = temp;
}
}
++low;
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
bubbleSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
int [] a = {1,5,4,11,2,20,18};
bubbleSort2(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + ",");
}
}
}
插入排序
思想就是动态的将有序集合中添加数据。
插入排序包含两种操作,一种是元素的比较,一种是元素的移动,
插入排序是原地排序算法,是稳定的排序算法,最好的是时间复杂度为O(n),最差为O(n2);
package sort;
public class insertionSort {
public static int[] insertionSort(int[] array){
if(array.length == 0){
return array;
}
int current;
for(int i=0;i<array.length - 1;i++){
current = array[i+1];
int preIndex = i;
while(preIndex >=0 && current < array[preIndex]){
array[preIndex + 1] = array[preIndex];
preIndex--;
}
array[preIndex + 1] = current;
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
insertionSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
选择排序:
选择排序每次会从未排序区间找到最小的元素,将其放到已排序区间的末尾
选择排序也是一种原地排序算法,时间复杂度都为O(n2);是不稳定的算法
package sort;
public class selectionSort {
public static int[] selectionSort(int[] array){
if(array.length == 0){
return array;
}
for(int i=0;i<array.length;i++){
int minIndex = i;
for(int j=i;j<array.length;j++){
if(array[j] < array[minIndex]) //找到最小的数
minIndex = j;
}
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
selectionSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
归并排序
如果要排序一个数组,我们先把数组从中间分成前后两部分,然后对前后两部分分别排序,然后将排好序的两部分合并在一起,这样整个数组就都有序了
使用的是分治思想,(分治是一种解决问题的处理思想,递归是一种编程技巧)
归并排序是一个稳定的排序算法,时间复杂度是稳定的都为O(nlogn),空间复杂度为O(n)
package sort;
public class MergeSort {
static int number=0;
public static void main(String[] args) {
int[] a = {26, 5, 98, 108, 28, 99, 100, 56, 34, 1 };
printArray("排序前:",a);
MergeSort(a);
printArray("排序后:",a);
}
private static void printArray(String pre,int[] a) {
System.out.print(pre+"\n");
for(int i=0;i<a.length;i++)
System.out.print(a[i]+"\t");
System.out.println();
}
private static void MergeSort(int[] a) {
// TODO Auto-generated method stub
System.out.println("开始排序");
Sort(a, 0, a.length - 1);
}
private static void Sort(int[] a, int left, int right) {
if(left>=right)
return;
int mid = (left + right) / 2;
//二路归并排序里面有两个Sort,多路归并排序里面写多个Sort就可以了
Sort(a, left, mid);
Sort(a, mid + 1, right);
merge(a, left, mid, right);
}
private static void merge(int[] a, int left, int mid, int right) {
int[] tmp = new int[a.length];
int r1 = mid + 1;
int tIndex = left;
int cIndex=left;
// 逐个归并
while(left <=mid && r1 <= right) {
if (a[left] <= a[r1])
tmp[tIndex++] = a[left++];
else
tmp[tIndex++] = a[r1++];
}
// 将左边剩余的归并
while (left <=mid) {
tmp[tIndex++] = a[left++];
}
// 将右边剩余的归并
while ( r1 <= right ) {
tmp[tIndex++] = a[r1++];
}
System.out.println("第"+(++number)+"趟排序:\t");
// TODO Auto-generated method stub
//从临时数组拷贝到原数组
while(cIndex<=right){
a[cIndex]=tmp[cIndex];
//输出中间归并排序结果
System.out.print(a[cIndex]+"\t");
cIndex++;
}
System.out.println();
}
}
快速排序
快排是一种原地的,不稳定的排序算法,时间复杂度为O(nlogn);
package sort;
public class QuickSort {
public static int[] QuickSort(int[] array, int start, int end) {
if (array.length < 1 || start < 0 || end >= array.length || start > end) {
return null;
}
int smallIndex = partition(array, start, end);
if (smallIndex > start)
QuickSort(array, start, smallIndex - 1);
if (smallIndex < end)
QuickSort(array, smallIndex + 1, end);
return array;
}
public static int partition(int[] array,int start,int end){
int pivot = (int) (start + Math.random() *(end - start + 1));
int smallIndex = start - 1;
swap(array,pivot,end);
for(int i=start;i<=end;i++){
if(array[i] <= array[end]){
smallIndex++;
if(i>smallIndex)
swap(array,i,smallIndex);
}
}
return smallIndex;
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
QuickSort(array,0,5);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
归并排序与快速排序的区别
归并和快排用的都是分治思想,递推公式和递归代码也非常相似,那它们的区别在哪里呢?
1.归并排序,是先递归调用,再进行合并,合并的时候进行数据的交换。所以它是自下而上的排序方式。何为自下而上?就是先解决子问题,再解决父问题。
2.快速排序,是先分区,在递归调用,分区的时候进行数据的交换。所以它是自上而下的排序方式。何为自上而下?就是先解决父问题,再解决子问题。
桶排序
将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行快速排序。
桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。
桶排序比较适合用在外部排序中。所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。
应用案例
1)需求描述:
有10GB的订单数据,需按订单金额(假设金额都是正整数)进行排序但内存有限,仅几百MB
2)解决思路:
扫描一遍文件,看订单金额所处数据范围,比如1元-10万元,那么就分100个桶。
第一个桶存储金额1-1000元之内的订单,第二个桶存1001-2000元之内的订单,依次类推。
每个桶对应一个文件,并按照金额范围的大小顺序编号命名(00,01,02,…,99)。
将100个小文件依次放入内存并用快排排序。
所有文件排好序后,只需按照文件编号从小到大依次读取每个小文件并写到大文件中即可。
3)注意点:若单个文件无法全部载入内存,则针对该文件继续按照前面的思路进行处理即可。
package sort;
public class HeapSort {
//声明全局变量,用于记录数组array的长度;
static int len;
public static int[] HeapSort(int[] array) {
len = array.length;
if (len < 1) return array;
//1.构建一个最大堆
buildMaxHeap(array);
//2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
while (len > 0) {
swap(array, 0, len - 1);
len--;
adjustHeap(array, 0);
}
return array;
}
//建立最大堆
public static void buildMaxHeap(int[] array) {
//从最后一个非叶子节点开始向上构造最大堆
for (int i = (len/2 - 1); i >= 0; i--) {
adjustHeap(array, i);
}
}
//调整成为最大堆
public static void adjustHeap(int[] array, int i) {
int maxIndex = i;
//如果有左子树,且左子树大于父节点,则将最大指针指向左子树
if (i * 2 < len && array[i * 2] > array[maxIndex])
maxIndex = i * 2;
//如果有右子树,且右子树大于父节点,则将最大指针指向右子树
if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex])
maxIndex = i * 2 + 1;
//如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
if (maxIndex != i) {
swap(array, maxIndex, i);
adjustHeap(array, maxIndex);
}
}
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
HeapSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
计数排序
计数排序其实是桶排序的一种特殊情况。当要排序的n个数据,所处的范围并不太大的时候,比如最大值是k,我们就可以把数据划分为k个桶,每个桶内的数据值都是相同的,省掉了桶内排序的时间
计数排序只能用在数据范围不大的场景中,如果数据范围k比要排序的数据n大很多,就不适合用计数排序了,而且,计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。
package sort;
import java.util.Arrays;
public class CountingSort {
/**
* 计数排序
*
* @param array
* @return
*/
public static int[] CountingSort(int[] array) {
if (array.length == 0) return array;
int bias, min = array[0], max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
if (array[i] < min)
min = array[i];
}
bias = 0 - min;
int[] bucket = new int[max - min + 1];
Arrays.fill(bucket, 0);
for (int i = 0; i < array.length; i++) {
bucket[array[i] + bias]++;
}
int index = 0, i = 0;
while (index < array.length) {
if (bucket[i] != 0) {
array[index] = i - bias;
bucket[i]--;
index++;
} else
i++;
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
CountingSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
基数排序
基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进关系,如果a数据的高位比b数据大,那剩下的低位就不用比较了,除此之外,每一位的数据范围不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就补发做到O(n)了。
package sort;
import java.util.ArrayList;
public class RadixSort {
/**
* 基数排序
* @param array
* @return
*/
public static int[] RadixSort(int[] array) {
if (array == null || array.length < 2)
return array;
// 1.先算出最大数的位数;
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
public static void main(String[] args) {
int array[] = new int[]{5, 8, 4, 6, 9, 2};
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
RadixSort(array);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + ",");
}
System.out.println();
}
}
排序优化
对于小规模数据进行排序,可以选择时间复杂度为O(n2)的算法,如果对大规模数据进行排序,时间复杂度为O(nlogn)的算法更加高效,(归并排序,快速排序,堆排序等等),java就是采用的堆排序实现排序函数,C语言使用快排实现排序函数
如何优选快速排序
快排最坏的情况是时间复杂度为O(n2),如果数据原来就是有序的或者接近有序的,每次分区点都选择最后一个数据,那块拍就会退化为O(n2),这种时间复杂度就是因为分区点不合理。
最理想的分区点就是,被分区点分开的两个分区中,数据的数量差不多
1.三数取中法
冲区间首尾中各取一个数,取这三个数的中间值作为分区点。
2.随机法
随机法就是每次从要排序的区间中,随机选择一个元素作为分区点,不能保证每次分区点都选的比较好,但是从概率来讲,也不大可能会发生分区点都选得很差的情况。
快排使用递归实现的,递归要警惕堆栈溢出,避免堆栈溢出1.限制递归深度,一旦递归过深,超过了我们实现设定的阀值,就停止递归2.通过在堆上模拟实现一个函数调用栈,手动模拟递归压栈出栈的过程