八种常见的排序算法

1. 算法复杂度计算

2.1 首先看时间复杂度

想要了解时间复杂度,就需要先了解时间频度。一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。

接下来就引入了时间复杂度的概念。看一下比较官方的定义吧:一般情况下,算法中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。是不是比较难以理解,说白了时间复杂度就是描述时间的规模,比如说时间频度是T(n),时间复杂度就是O(n)。时间频度是T(n+n)的时候,时间复杂度还是O(n)。也就是他的时间规模就是n这个层次了。

常见的算法的时间 复杂度之间的关系为:

O(1) n)

为什么时间复杂度用logn来描述,而不用log2n来描述?

假如有logaB(a为底数),由换底公式可得:
在这里插入图片描述
logcA(c为底数)为常数,由O的运算规则"O(C×f(N))=O(f(N)),其中C是一个正的常数"得O(logaB)=O(logcB)可知算法的时间复杂度与不同底数只有常数的关系,均可以省略自然可以用logN代替。

2.2 空间复杂度

空间复杂度就比较容易理解了,空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个空间规模,我们用 S(n) 来定义。

空间复杂度比较常用的有:O(1)、O(n)、O(n²)

2.常见排序算法的复杂度

八种常见的排序算法_第1张图片
什么是稳定排序和不稳定排序?
通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同,则称之为稳定排序。也就是排序前后两个相等元素的相对位置不发生改变。比如A[i]=A[j],排序前i在j前面,排序后i还是在j之前。如果排序算法稳定,对基于比较的排序算法而言,元素交换的次数可能会少一些。

哪些是稳定排序和不稳定排序?
通常前后相邻两两元素比较的排序是稳定的。

稳定的排序:冒泡排序、插入排序、归并排序、基数排序

不稳定的排序:选择排序、快速排序、希尔排序、堆排序

3. 冒泡排序

3.1 算法原理

S1:从待排序序列的起始位置开始,从前往后依次比较各个位置和其后一位置的大小并执行S2。

S2:如果当前位置的值大于其后一位置的值,就把他俩的值交换(完成一次全序列比较后,序列最后位置的值即此序列最大值,所以其不需要再参与冒泡)。

S3:将序列的最后位置从待排序序列中移除。若移除后的待排序序列不为空则继续执行S1,否则冒泡结束。

3.2 算法实现

 public void sort(int[] nums) {
  for (int i = 0; i < nums.length; i++) {
   for (int j = 0; j < nums.length-1-i; j++) {
	    if(nums[j] > nums[j+1]) {
	     int temp = nums[j];
	     nums[j] = nums[j+1];
	     nums[j+1] = temp;
	    }
   }
  }
 }

3.3 算法优化

若某一趟排序中未进行一次交换,则排序结束。因为在一趟排序中没有交换的话,说明前一个数一直小于后一个数。

 public void sort(int[] nums) {
  for (int i = 0; i < nums.length; i++) {
   boolean flag = false;
   for (int j = 0; j < nums.length-1-i; j++) {
    if(nums[j] > nums[j+1]) {
     int temp = nums[j];
     nums[j] = nums[j+1];
     nums[j+1] = temp;
     flag = true;
    }
   }
   if(flag==false)
    break;
  }
 }

4.快速排序

4.1 算法原理

快速排序是对冒泡排序的一种改进
基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此实现整个数据变成有序序列。

算法步骤

  1. 假设我们对数组{7, 1, 3, 5, 13, 9, 3, 6,11}进行快速排序。

  2. 首先在这个序列中找一个数作为基准数,为了方便可以取第一个数。

  3. 遍历数组,将小于基准数的放置于基准数左边,大于基准数的放置于基准数右边。

  4. 此时得到类似于这种排序的数组{3, 1, 3, 5, 6, 7, 9, 13, 11}。

  5. 在初始状态下7是第一个位置,现在需要把7挪到中间的某个位置k,也即k位置是两边数的分界点。

  6. 那如何做到把小于和大于基准数7的值分别放置于两边呢,我们采用双指针法,从数组的两端分别进行比对。

  7. 先从最右位置往左开始找直到找到一个小于基准数的值,记录下该值的位置(记作 i)。

  8. 再从最左位置往右找直到找到一个大于基准数的值,记录下该值的位置(记作 j)。
    注意:一定要先从右往左找到小于基准值target的值,再从左往右找到小于target的值,因为如果先从左往右找到大于target的值,此时nums[i]>target的,再早小于的target的时候,如果i==j,那么就会将nums[i]和target交换位置,此时target的左边就会出现一个大于它的数,不符合快速排序的要求。

  9. 如果位置i

  10. 如果执行到i==j,表示本次比对已经结束,将最后i的位置的值与基准数做交换,此时基准数就找到了临界点的位置k,位置k两边的数组都比当前位置k上的基准值或都更小或都更大。

  11. 上一次的基准值7已经把数组分为了两半,基准值7算是已归位(找到排序后的位置)。

  12. 通过相同的排序思想,分别对7两边的数组进行快速排序,左边对[left, k-1]子数组排序,右边则是[k+1, right]子数组排序。

  13. 利用递归算法,对分治后的子数组进行排序。

快速排序之所以比较快,是因为相比冒泡排序,每次的交换都是跳跃式的,每次设置一个基准值,将小于基准值的都交换到左边,大于基准值的都交换到右边,这样不会像冒泡一样每次都只交换相邻的两个数,因此比较和交换的此数都变少了,速度自然更高。

4.2 算法实现

	public void sort(int[] nums,int left,int right) {
		if(left>=right)
			return ;
		int target = nums[left];
		int i = left;
		int j = right;
		while(i < j) {
			//从右往左找到小于target的值
			while(nums[j] >= target && i<j) {
				j--;
			}
			//从左往右找到大于target的值
			while(nums[i] <= target && i<j) {
				i++;
			}

			if(i < j){
					int temp = nums[i];
					nums[i] = nums[j];
					nums[j] = temp;
					i++;
					j--;
			}
		}
		//将基准数归位
		nums[left] = nums[i];
		nums[i] = target;
		sort(nums,left,i-1);
		sort(nums,i+1,right);
	}
}

为什么说快速排序是对冒泡排序的一种改进?
因为冒泡排序是将大的数往右移动,小的数往做移动。而快速排序有两个序列,分别存储大的数和小的数,大于target的数放在大数序列,小于target的数放在小数序列。

4.3 算法改进

改进思路:
(1)分而治之时候,分到了最后,数组已经很小,这时候采用插入排序代替快速排序。
(2)基准值的选取,我们随机取出来3个数,取中间大小的为基准值。
(3)取三个变量切分数组,将数组分为大于,等于,小于基准元素三部分,这样在递归时就可以剔除相等的元素,减小比较的次数

private static void sort(Comparable[] a,int low,int height){  
     //改进处1:由插入排序替换
     if(height <= low + M){//M取5-15
           InsertSort.sort(a,lo,hi);
           return;  
    } 
    //改进处3:三向切分
    int lt=low,i=low+1,gt=height;  //三个变量,
    //改进处2:基准元素的选取
    int i=medianOf3(a,low,low+(height-low)/2, height);

    while(i<=gt){
        int cmp = a[i].compareTo(a[low]);
        if(cmp<0) 
            exch(a,lt++,i++);  
        else if(cmp>0)
            exch(a,i,gt--);   
        else 
            i++;    
        }
    sort(a,low,lt-1); 
    sort(a,lt+1,height);
}  

这里面有俩函数,medianOf3和exch。medianOf3函数是找到三个数的中间值,exch是交换两个数的位置

5.直接插入排序

5.1 算法原理

插入排序的基本方法是:每步将一个待排序序列按数据大小插到前面已经排序的序列中的适当位置,直到全部数据插入完毕为止。

(1) 将这个序列的第一个元素视为一个有序序列;

(2)默认从第二个数据开始比较。

(3)如果第二个数据比第一个小,则交换。然后在用第三个数据比较,如果比前面小,则插入(狡猾)。否则,退出循环

(4)说明:默认将第一数据看成有序列表,后面无序的列表循环每一个数据,如果比前面的数据小则插入(交换),否则退出。

5.2 算法实现

	public void sort(int[] nums) {
		for (int i = 1; i < nums.length; i++) {
			for (int j = i; j > 0; j--) {
				if(nums[j] >= nums[j-1])
					break;
				else {
					int temp = nums[j];
					nums[j] = nums[j-1];
					nums[j-1] = temp;
				}
			}
		}
	}

5.3 算法改进

每次找插入位置的时候我们都要从头到尾一个一个比较。当数据量大的时候我们肯定不允许。于是我们换一种想法。使用二分法查找的思想,使用二分法查找应该插入的位置

	//使用二分查找进行插入排序
	public void sort(int[] nums) {
		for (int i = 1; i < nums.length; i++) {
			//从已经排好序的数组0---i-1中找到nums[i]插入的位置
			int temp = nums[i];
			int left = 0;
			int right = i-1;
			int mid = (left+right)/2;
			//找到待插入的位置right+1
			while(left <= right) {
				if(nums[i] <= nums[mid]) {
					right = mid-1;
				}
				else {
					left = mid+1;
				}
				mid = (left+right)/2;
			}
			//将待插入位置后面所有元素往后移动一位
			for (int j = i; j > right+1; j--) {
				nums[j] = nums[j-1];
			}
			nums[right+1] = temp;
			System.out.println(Arrays.toString(nums));
		}
		
	}

6. 希尔排序

希尔排序是插入排序的变种版。希尔排序是一种插入排序算法,又称作缩小增量排序,是一种分组插入排序的方法。

6.1 算法原理

(1)将数组nums分成若干个子数组,子数组元素之间的步长为i,i的初始值为nums.length/2,i最小值为1,并且每次循环后i变成i/2。

(2)对每个子数组进行插入排序,首先控制子数组开始的位置j,j < nums.length-i

(3)对每个子数组元素从第二个元素开始进行判断,如果大于前一个元素,直接退出该层循环,如果小于前一个元素则交换

6.2 算法实现

	public static void sort(int[] nums) {
		int len = nums.length;
		//希尔排序的步长,最长为len/2,最短为1
		for (int i = len/2; i > 0; i/=2) {
			//插入排序i个子数组的开始位置j
			for (int j = 0; j <= i; j++) {
				//每个子数组插入排序交换元素
				for (int k = j+i; k < nums.length;) {
					if(nums[k] < nums[k-i]) {
						int temp = nums[k-i];
						nums[k-i] = nums[k];
						nums[k] = temp;
					}
					//找到了插入位置,退出循环
					else {
						break;
					}
					k = j+i;
				}
			}
		}
	}

6.3 算法改进

(1)对增量序列优化:使用更加复杂的方式。

(2)对每一趟的插入排序进行优化:在内循环中,总是将较大的元素向右移动。

7. 直接选择排序

7.1 算法原理

直接选择排序是一种简单的排序方法,它的基本思想是:
第一次从R[0]~R[n-1]中选取最小值,与R[0]交换
第二次从R[1]~R[n-1]中选取最小值,与R[1]交换,
….,
第i次从R[i-1]~R[n-1]中选取最小值,与R[i-1]交换,
……,
第n-1次从R[n-2]~R[n-1]中选取最小值,与R[n-2]交换,
共通过n-1次,得到一个从小到大排列的有序序列。

7.2 算法实现

	public void sort(int[] nums) {
		for (int i = 0; i < nums.length-1; i++) {
			for (int j = i+1; j < nums.length; j++) {
				if(nums[i] > nums[j]) {
					int temp = nums[i];
					nums[i] = nums[j];
					nums[j] = temp;
				}
			}
		}
	}

7.3 算法优化

之前的算法中,每跑一趟,只能选出来一个最大的元素或者是一个最小的元素,但是归根结底是一个元素。这样的话有N个元素,就要跑N-1趟。现在我们换一种思路。

每跑一趟不仅仅记录最大的元素还可以记录最小的元素,这不是一举两得嘛。这时候只需要跑N/2趟即可。省时省力。

    //选择排序改进版
       public static void selectSort(int[] arr){
           int minPoint;  //存储最小元素的小标
           int maxPoint;  //存储最大元素的小标
           int len = arr.length;
           int temp;
           //只需要跑N/2趟即可
           for(int i=0;i<len/2;i++){   
              minPoint= i;
              maxPoint= i;
              for(int j=i+1;j<=len-1-i;j++){ 
                  //每一趟的最小值
                  if(arr[j]<arr[minPoint]){  
                     minPoint= j;
                     continue;
                  }
                  //每一趟的最小值
                  else if(arr[j]>arr[maxPoint]){ 
                     maxPoint= j;
                  }
              }
             //将最小值与前面的值交换
              if(minPoint!=i){  
                  temp= arr[i];
                  arr[i]= arr[minPoint];
                  arr[minPoint]= temp;
                  if(maxPoint== i){
                     maxPoint= minPoint;
                  }

              }
             //将最大值与后面的值交换
              if(maxPoint!=len-1-i){  
                  temp= arr[len-1-i];
                  arr[len-1-i]= arr[maxPoint];
                  arr[maxPoint]= temp;
              }         
           }
       }

8. 堆排序

参考自堆排序算法(图解详细流程)

8.1 算法原理

堆排序(Heapsort)是指利用堆这种数据结构(后面的【图解数据结构】内容会讲解分析)所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

堆排序的平均时间复杂度为 Ο(nlogn)。

1.父结点索引:(i-1)/2(这里计算机中的除以2,省略掉小数)

2.左孩子索引:2*i+1

3.右孩子索引:2*i+2

堆的定义性质:

大根堆:arr(i)>arr(2i+1) && arr(i)>arr(2i+2)

小根堆:arr(i)i+1) && arr(i)i+2)

8.2 算法步骤

  1. 首先将待排序的数组构造成一个大根堆,此时,整个数组的最大值就是堆结构的顶端

  2. 将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1

  3. 将剩余的n-1个数再构造成大根堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组

8.3 算法实现

    public static void sort(int[] nums){
    	//1.构建大顶堆
    	heapInsert(nums);
    	int size = nums.length;
    	while(size > 1) {
    		//将堆顶元素放到末尾
    		swap(nums,0,size-1);
    		System.out.println(Arrays.toString(nums));
    		size--;
    		//将剩余元素重新构造大顶堆
    		heapify(nums,0,size);
    	}
    	
    }
    
    //构造大顶堆,通过新插入的数上升
    public static  void heapInsert(int[] nums) {
    	for (int i = 0; i < nums.length; i++) {
			//当前插入的索引
    		int currentIndex = i;
    		//父节点
    		int fatherIndex = (currentIndex-1)/2;
    		//如果当前值大于父节点值则交换,并且将索引指向父节点
    		while(nums[currentIndex] > nums[fatherIndex]) {
    			swap(nums, currentIndex, fatherIndex);
    			currentIndex = fatherIndex;
    			fatherIndex = (currentIndex-1)/2;
    			
    		}
		}
    }
    
    //剩余的树构造大顶堆
    public static void heapify(int[] nums,int index,int size) {
    	//左节点
    	int left = 2*index+1;
    	//右节点
    	int right = 2*index+2;
    	while(left < size) {
    		int maxIndex;
    		//选择子节点较大的那一个
    		if(nums[left] < nums[right] && right < size)
    			maxIndex = right;
    		else
    			maxIndex = left;
    		if(nums[index] >= nums[maxIndex]) {
    			break;
    		}
    		else {
    			swap(nums, index, maxIndex);
    			index = maxIndex;
    			left = 2*index+1;
    	    	right = 2*index+2;
    		}
    	}
    }
    
    public static void swap(int[] nums,int i,int j) {
    	int temp = nums[i];
    	nums[i] = nums[j];
    	nums[j] = temp;
    }

9. 归并排序

9.1 算法原理

归并排序的算法我们通常用递归实现,先把待排序区间[s,t]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s,t]。

S1: 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
S2: 设定两个指针,最初位置分别为两个已经排序序列的起始位置
S3: 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
S4: 重复S3,直到某一指针超出序列尾
S5: 将另一序列剩下的所有元素直接复制到合并序列尾

9.2 算法实现

	public void mergeSort(int[] nums,int low,int high) {
		int mid = (low+high)/2;
		if(low<high) {
			mergeSort(nums, low, mid);
			mergeSort(nums, mid+1, high);
			merge(nums, low, mid, high);
		}
	}
	public void merge(int[] nums,int low,int mid,int high) {
		int[] tempArr = new int[high-low+1];
		int i = low;
		int j = mid+1;
		int index = 0;
		while(i <= mid && j <= high) {
			if(nums[i] < nums[j]) {
				tempArr[index++] = nums[i++];
			}
			else {
				tempArr[index++] = nums[j++];
			}
		}
		while(i<=mid) {
			tempArr[index++] = nums[i++];
		}
		while(j<=high) {
			tempArr[index++] = nums[j++];
		}
		for (int k = low; k <= high; k++) {
			nums[k] = tempArr[k-low];
		}
	}

9.3 算法改进

(1)如果子数组较小,改用插入排序;

(2) 两个子数组若已经有序,则直接复制进数组

(3) 通过交换参数来避免每次都要复制到辅助数组。

    private static void sort(Comparable[] a, Comparable[] aux,int low, int height){
        int mid = low + (height -low) / 2;  
        //改进1:若子序列比较短,则直接使用插入排序
        if(height <= low + 7 - 1){  
            insertionSort(a, lo, hi);
            return;
        }       
        sort(aux,a,low,mid);     
        sort(aux,a,mid+1,height);  
        //less函数比较a[mid+1]和a[mid]的大小
        //改进2:如果数组已经有序则直接复制,不再merge
        if(!less(a[mid+1],a[mid])){ 
            System.arraycopy(aux, low, a, low, height-low+1);
            return;
        }
        merge(a,aux,low,mid,height); 
    }
	private void merge(Comparable[] a,Comparable[] aux, int lo, int mid, int hi){
	    //左半部分
	    for(int k = low; k <= mid; k++)
	        aux[k] = a[k];
	    //右半部分
	    for(int k = mid+1; k <= height; k++)
	        aux[k] = a[height+mid+1-k];
	    int i = low, j = height; 
	    //整个序列
	    for(int k=low;k<=height; k++ ){
	        if(less(aux[j],aux[i]))
	            a[k] = aux[j--];
	        else 
	            a[k] = aux[i++]; 
	    }           
	}

10.基数排序

你可能感兴趣的:(数据结构分析)