个人觉得会写排序算法并没啥用,如果只是单纯使用排序算法,一般都是直接调用包写好的算法,绝对要比你写的好,而且一般包里面的排序算法都可能数据情况,使用不同的排序算法, 主要是学习各种排序算法中的思想,所以本文除了写各种排序算法,还会写如何用排序算法来解决实际问题。
排序算法总结
先解释下排序的稳定性:
如果排序前的第一个8 和第二个8 在排序后,还是保持这个顺序,就说明排序是稳定,否则就是不稳定的(下面会具体解释每一个算法是否稳定)。那么排序的稳定性有什么用呢?
如果一个类是下面的3个属性
class Student{
private String name; //名称
private int class_id; //班级id
private int score; //分数
}
如果现在已经将对象根据score分数经行排序了,然后又使用class_id班级id进行排序,这个时候相同班级的会被排序到一起来,如果排序算法是稳点的,这个时候每一个班级中的人的分数还是按照有序排列。
各种排序算法
选择排序,冒泡排序
冒泡排序执行流程:
- 从头开始比较每一对相邻元素,如果第1个比第2个大,就交换它们的位置 ,(执行完一轮后,最末尾就是那个元素的最大的元素)
- 忽略1中曾经找到的最大元素,重复执行步骤一,直到全部元素有序
//冒泡排序
public void bubSort(int nums[]){
if(nums==null || nums.length<2)
return;
for(int end=nums.length-1;end>0;end--){
//每一轮循环将没排序的数据中的最大值放在最后一个位置
for(int i=1;i<=end;i++){
if(a[i-1]>a[i]){
//交换这两个数据,将大的数据放在向后面放
swap(nums,i-1,i);
}
}
}
}
冒泡排序优化一:如果序列已经完全有序,可以提前终止冒泡排序
//冒泡排序
public void bubSort(int nums[]){
if(nums==null || nums.length<2)
return;
for(int end=nums.length-1;end>0;end--){
//每一轮循环将没排序的数据中的最大值放在最后一个位置
boolean sorted=true;
for(int i=1;i<=end;i++){
if(a[i-1]>a[i]){
//交换这两个数据,将大的数据放在向后面放
sorted=true;
swap(nums,i-1,i);
}
}
if(sorted)
break;
}
}
冒泡排序优化二:如果序列尾部已经局部有序,可以记录最后1次交换的位置,减少比较次数
//冒泡排序
public void bubSort(int nums[]){
if(nums==null || nums.length<2)
return;
for(int end=nums.length-1;end>0;end--){
//每一轮循环将没排序的数据中的最大值放在最后一个位置
int sortedIndex=1;
for(int i=1;i<=end;i++){
if(a[i-1]>a[i]){
//交换这两个数据,将大的数据放在向后面放
sortedIndex=i;
swap(nums,i-1,i);
}
}
end =sortedIndex;
}
}
时间复杂度: 冒泡排序中,如果某一轮的交换没有交换数据,说明数据是已经排序好了的,最好的情况就是数据本来就是有序的,所以最好的情况就是将数据遍历一遍,时间复杂度也就是O(n);
稳定性:冒泡可以做到稳定性,当然你也可以将排序算法写成不稳定的,也就是当前一个数据后一个数据相等的时候,你也让这两个数据经行交换。
选择排序
- 从序列中找出最小的那个元素,然后与数组最开始元素交换位置(执行完一轮后,第一个开始的那个元素就是最大的元素)
- 忽略1中曾经找到的最小元素,重复执行步骤1
//选择排序
public void SelectSort(int nums[]){
if(nums==null || nums.length<2)
return;
for(int i=0;i<nums.length-1;i++){
int min_index=i;
for(int j=i+1;j<arr.length;j++){
min_index=nums[min_index]>nums[j]?j:min_index;
}
if(i!=min_index)
swap(nums,min_index,i);
}
}
- 选择排序的交换次数要远远少于冒泡排序,平均性能优于冒泡排序
- 选择排序,不可能会稳定
所以选择排序不管怎么做,都不能满足稳定性
插入排序
插入排序,一般也被称为直接插入排序。对于少量元素的排序,它是一个有效的算法 。插入排序是一种最简单的排序方法,它的基本思想是将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动 。
执行流程:
- 在执行过程中,插入排序会将序列分为2部分(头部是排好序的,尾部是待排序的)
- 从头开始扫描每一个元素(每当扫描到一个元素,就将它插入到合适的位置,使得头部数据依然保持有序)
插入排序也比较简单,但是不代表并不使用它,一般当数据个数小于60的时候,使用它和使用O(nlgn)的区别并不大,而且它的常数项比较低,而且是稳定的
public void insertSort(int [] nums){
if(nums==null || nums.length<2)
return;
for(int i=1;i<nums.length;i++){
int temp=nums[i];
for(int j=i-1;j>=0 && temp<nums[i];j--){
nums[j+1]=nums[j];
}
nums[j+1]=temp;
}
}
- 插入排序的时间复杂度与逆序对的数量成正比关系
逆序对的数量越多,插入排序的时间复杂度越高
插入排序 可以使用二分搜索优化 :使用二分搜索后,只是减少了比较次数,插入排序的时间复杂度依然不变
private static void insertionSort(int []arr){
for(int i=1;i<arr.length;i++){
insert(arr,i,search(arr,i));
}
}
private static void insert(int [] arr,int i,int search){
int temp=arr[i];
for(int j=i;j>search;j--){
arr[i]=arr[i-1];
}
arr[search]=temp;
}
//返回第一个大于等于 arr[i]的位置的坐标
private static int search(int []arr,int i){
int begin=0;
int end=i;
while(begin<end){
int mid=begin+(end-begin)>>1;
if(arr[i]<arr[mid]){
end=mid;
}else{
begin=mid+1;
}
}
return begin;
}
时间复杂度:O(n^2) ,如果数据本来就是有序排列的,也就是第二层的循环不会进行,这个时候最好的时间的复杂度是O(n)
稳定的,和冒牌排序一个道理。
归并排序
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
「归并排序」是分治思想的典型应用,它包含这样三个步骤:
分解: 待排序的区间为 [l, r] ,令 m =(l + r)/2 ,我们把 [l, r]分成 [l, m] 和 [m + 1, r]
解决: 使用归并排序递归地排序两个子序列
合并: 把两个已经排好序的子序列 [l, m] 和 [m + 1, r]合并起来
public void void mergeSort(int [] arr){
if(arr==null || arr.length<2)
return;
//排序数组
sortProcess(arr,0,arr.length-1);
}
public void sortProcess(int [] arr,int L,int R){
if(L==R)
return;
int mid=L+((R-L)>>1);
//将数组左边排序
sortProcess(arr,L,mid);
//将数组右边排序
sortProcess(arr,mid+1,R);
//通过排序好的左边,右边,将整个数组排序好
merge(arr,L,mid,R);
}
private void merge(int [] arr,int l,int mid,int r){
int help[]=new int[r-l+1];
int i=0;
int p1=l;
int p2=mid+1;
//将数组中左右部分中较小的部分赋值给Help
while(p1<=mid && p2<=r){
help[i++]=arr[p1]<=arr[p2]?arr[p1++]:arr[p2++];
}
//如果左边部分没有遍历完,将左边部分中剩下的给Help就好了
while(p1<=mid){
help[i++]=arr[p1++];
}
while(p2<=r){
help[i++]=arr[p2++];
}
for(int j=0;j<help.length;j++)
arr[l+j]=help[j];
}
时间复杂度为 O(nlgn) ,这个使用主定理可以算出
空间复杂度为O(n),因为使用外部排序,因为归并排序需要用到一个临时数组。
稳定性:完全在排序的时候做到稳定性
先来个简单题目题目leetcode 88 合并两个有序数组
这个题目,可以使用归并排序中的merge的过程。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int l=0;
int r=0;
int i=0;
int [] help=new int[m+n];
while(l<m && r<n){
help[i++]=nums1[l] <= nums2[r]? nums1[l++] :nums2[r++];
}
while(l<m){
help[i++]=nums1[l++];
}
while(r<n){
help[i++]=nums2[r++];
}
for( i=0;i<help.length;i++)
nums1[i]=help[i];
}
}
思路:在归并排序中merge的过程中,会比较数组左边部分和右边部分的大小,所以可以计算出逆序对
同理可以得到,以后的逆序对。
class Solution {
private int count=0;
public int reversePairs(int[] nums) {
if(nums==null || nums.length<2)
return 0;
process(nums,0,nums.length-1);
return count;
}
private void process(int[] nums, int L, int R) {
if(L==R)
return;
int mid=L+((R-L)>>1);
//对左边部分进行排序
process(nums,L,mid);
//对右边部分经行排序
process(nums,mid+1,R);
merge(nums,L,mid,R);
}
//合并左边和右边的部分
private void merge(int[] nums, int l, int mid, int r) {
int []helper=new int[r-l+1];
int p1=l;
int p2=mid+1;
int i=0;
while(p1<=mid && p2<= r){
if(nums[p1]<=nums[p2]){
helper[i++]=nums[p1++];
}else{
helper[i++]=nums[p2++];
//当找到右边的值小于左边的值时候,看左边有多少数字是大于这个数字的
count+=(mid-p1+1);
}
}
while(p1<=mid){
helper[i++]=nums[p1++];
}
while(p2<=r){
helper[i++]=nums[p2++];
}
for (int j = 0; j <helper.length ; j++) {
nums[l+j]=helper[j];
}
}
}
堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序堆是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
堆排序的基本思想:将待排序的数组构成一个大顶堆.这个时候数组的最大值就是大顶堆的根节点。将它和末尾的元素经行交换,这个时候末尾就是最大值了,然后将剩下的n-1个元素重新构造成为一个大顶堆,这样会得到n-1个元素中最的最大值,如此就能得到一个有序数组了。
数组28, 95, 14, 8, 17, 53, 5, 26, 87, 48对应的大顶堆,小顶堆如下
也就是满足大顶堆: arr[i]>= arr[2i+1] && arr[i]>=arr[2i+2]
小顶堆:arr[i]<=arr[2i+1] &&arr[i] <=arr[2i+2]
如数组 4 ,2,10,5,8的排序过程
- 首先建立大顶堆,下面的二叉树是逻辑中的,实际上在物理中是使用数组来存储和交换的
- 最开始只有一个节点4
- 插入2,2比4小,所有是4的子树
- 插入10,10比4大,交换4和10,这个时候数组是
10,2,4,5,8
4. 插入5 ,5比2大,交换5和2
现在数组是 10,5,4,2,8
5. 插入8 ,8比5大,交换8和5
现在数组是10,8,4,2,5
可以知道现在数组的最大值在堆顶,而且满足父节点不小于任何一个子节点
然后将数组第一个位置和最后一个位置的数字交换,数变成了[5,8,4,2,10] ,最大值放在组后面,对应的逻辑堆图变成了如下,所以需要将树5,8,4,2堆化,也就是将5和左子树和右子树中最大的值比较,如果有比5大的子树,即交换位置
结果为
其他的同理这个过程,最后所有的数组会变得有序化
import java.util.Arrays;
/**
* Created by chengxiao on 2016/12/17.
* 堆排序demo
*/
public class HeapSort {
public static void main(String []args){
int []arr = {9,8,7,6,5,4,3,2,1};
sort(arr);
System.out.println(Arrays.toString(arr));
}
public static void sort(int []arr){
//1.构建大顶堆
for(int i=arr.length/2-1;i>=0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,arr.length);
}
//2.调整堆结构+交换堆顶元素与末尾元素
for(int j=arr.length-1;j>0;j--){
swap(arr,0,j);//将堆顶元素与末尾元素进行交换
adjustHeap(arr,0,j);//重新对堆进行调整
}
}
/**
* 调整大顶堆(仅是调整过程,建立在大顶堆已构建的基础上)
* @param arr
* @param i
* @param length
*/
public static void adjustHeap(int []arr,int i,int length){
int temp = arr[i];//先取出当前元素i
for(int k=i*2+1;k<length;k=k*2+1){//从i结点的左子结点开始,也就是2i+1处开始
if(k+1<length && arr[k]<arr[k+1]){//如果左子结点小于右子结点,k指向右子结点
k++;
}
if(arr[k] >temp){//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
arr[i] = arr[k];
i = k;
}else{
break;
}
}
arr[i] = temp;//将temp值放到最终的位置
}
/**
* 交换元素
* @param arr
* @param a
* @param b
*/
public static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}
时间复杂度为 O(nlgn) ,
空间复杂度为O(1),
稳定性:堆排序应该建堆和堆化的过程,无法保证稳定性。
堆排序相关的题目,求数组中的第k个最大元素
可以使用堆排序来解决这个问题——建立一个大根堆,做 k−1 次删除操作后堆顶元素就是我们要找的答案
class Solution {
public int findKthLargest(int[] nums, int k) {
//建立大顶堆
for (int i = nums.length/2-1; i >= 0; i--) {
heapify(nums,i,nums.length);
}
//堆化
int heaSize=nums.length;
while(--k>0){
swap(nums,0,--heaSize); //交换数据
heapify(nums,0,heaSize);
}
return nums[0];
}
private void heapify(int[] nums, int i, int heaSize) {
int left=i*2+1;
int temp=nums[i];
while(left<heaSize){
int large_index=left+1<heaSize&& nums[left+1]>nums[left]
?left+1:left;
if(nums[large_index]>temp){
nums[i]=nums[large_index];
}else{
break;
}
i=large_index;
left=i*2+1;
}
nums[i]=temp;
}
private static void swap(int []arr,int a ,int b){
int temp=arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
}