十大内部排序算法的设计思路及Java实现

项目介绍

  • 本项目通过分解各大厂的常见笔面试题,追本溯源至数据结构和算法的底层实现原理,知其然知其所以然;
  • 建立知识结构体系,方便查找,欢迎更多志同道合的朋友加入项目AlgorithmPractice,(欢迎提issue和pull request)。

Part One : 内部排序:

Part Two : 内部排序的应用:

正文开始

1、冒泡排序

  • 代码实现BubbleSort
  • 设计思路
    • 什么是冒泡排序:
      • 1.每次比较相邻的两个元素
      • 2.每次从头比到尾,能确定一个元素的最终位置
      • 3.除了之前最终确定的元素不需要比对外,其他元素重复1和2步骤。
    • 冒泡排序的时间复杂度:
      • O( n 2 n^2 )。
      • (在一般情况下)第一次对比n-1次,第二次对比n-2次,…,第n-1次对比1次,共对比(1+n-1)*n-1=O( n 2 n^2 )。
    • 冒泡排序的稳定性:稳定。
    • 【代码展示】
public class BubbleSort implements SortInterface {

    //冒泡排序    参数:int类型数组,对数组进行排序
    public int[] sortMethod(int[] sortNum) {
        //交换变量:temp
        int temp;
        //当某次内部排序无数据交换发送时,flag不变,则此时数组有序,无序再循环对比
        boolean flag = false;
        //外部循环:循环元素个数减一次,因为最后一次默认有序
        for (int i = 0; i < sortNum.length - 1; i++) {
            //内部循环:外部循环i次,可以确定i个元素已经有序,因此剩余n-i个无序元素,
            //内部循环次数为元素个数n减去外部循环次数i,又因为i和i+1对比,因此防止的对比越界,需再减一
            for (int j = 0; j < sortNum.length - i - 1; j++) {
                if (sortNum[j] > sortNum[j + 1]) {
                    temp = sortNum[j];
                    sortNum[j] = sortNum[j + 1];
                    sortNum[j + 1] = temp;
                    flag = true;
                }
            }
            if (!flag) {
                break;
            }
            flag = false;
        }
        return sortNum;
    }
}
  • 注意事项:内外层循环需要注意区分范围。

2、堆排序

  • 代码实现HeapSort,其实堆排序有两种实现方式,对比看看能不能更深入理解一下,堆排序的两种写法
  • 设计思路
    • 什么是堆排序:
      • 初试化建堆,建完后,堆顶即最大/最小元素。
      • 交换堆顶和数组末尾元素,然后针对剩余的n-1个元素,对堆顶元素进行调整即可。
      • 重复2),直到所有元素有序。
    • 堆排序的时间复杂度:
      • 综合:O(n log 2n)。
      • 整堆的时间复杂度:O(n log 2n)。
      • 建堆的时间复杂度:O(n/2 * log 2n)。
    • 堆排序的稳定性:不稳定。
    • 【代码展示】(整堆的代码请见github)
public int[] sortMethod(int[] heap) {
    int temp;
    //输入检查
    if (heap == null || heap.length <= 1) {
        return heap;
    }
    //初试化建堆
    for (int i = (heap.length - 1) / 2; i >= 0 ; i--) {
        heapify_big(heap, i, heap.length - 1);
    }
    //交换堆顶和数组末尾元素,循环整堆,注意边界值
    for (int i = heap.length - 1; i > 0; i--) {
        temp = heap[0];
        heap[0] = heap[i];
        heap[i] = temp;
        heapify_big(heap, 0, i-1);
    }
    return heap;
}
  • 注意事项:堆的边界不明确,会导致数组越界错误。

3、插入排序

  • 代码实现InsertSort
  • 设计思路
    • 什么是插入排序:
      • 将待排序区划分为有序和无序区,初始时有序区只有一个数,即自身有序。
      • 从无序区选取数,按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的纪录插入完为止。
    • 插入排序的时间复杂度:
      • O(n)。
      • (在一般情况下)第一个元素自身有序,第二个元素跟第一个对比,第三个元素跟前两个元素对比。。。共对比(0+1+2+。。。+(n-1))=O(n)。
    • 插入排序的稳定性:稳定。
    • 【代码展示】(指定范围的插入排序请见github)
 public int[] sortMethod(int[] sortNum) {
        int temp = 0;
        //外部循环从1开始,直到最后
        for (int i = 1; i < sortNum.length; i++) {
            //内部循环从外部位置遍历到0
            //====实现1
            int j = i;
            temp = sortNum[i];
            while ((j > 0) && (temp < sortNum[j - 1])) {
                sortNum[j] = sortNum[j - 1];
                j--;
            }
            if (j != i) {
                //因为在循环内j已经被减过。所以此处赋值给j,而不是j-1
                sortNum[j] = temp;
            }
            //====实现2
            /*for (int j = i; j > 0; j--) {
                if(sortNum[j] < sortNum[j - 1]){
                    temp = sortNum[j];
                    sortNum[j] = sortNum[j - 1];
                    sortNum[j - 1] = temp;
                }
            }*/
        }
        return sortNum;
    }
  • 注意事项:插入点的选择

4、选择排序

  • 代码实现SelectSort
  • 设计思路
    • 什么是选择排序:
      • 将待排序区划分为有序和无序区,有序区初始值为0。
      • 每次从待排序的数据元素序列中选出最小(或最大)的一个元素,
      • 存放在序列的起始位置,直到全部待排序的数据元素排完。
    • 选择排序的时间复杂度:
      • O( n 2 n^2 )。
      • (在一般情况下)每次选出一个最大或者最小,那么n个元素需要选择n-1次(外部排序趟数n-1),前 i 趟确定 i 个元素的最终位置(跟冒泡一样),每趟排序的对比次数为 n-i。
    • 选择排序的稳定性:
      • 选择排序是不稳定的排序方法。
      • (比如序列[5, 5, 3]第一次就将第一个[5]与[3]交换,导致第一个5挪动到第二个5后面)。
    • 【代码展示】
方法一:
     for(int i=0;i<A.length-1;i++){//前i趟确定i个元素的最终位置
		for(int j=i+1;j<A.length;j++){//每趟排序从i开始,到A.length结束
				if(A[i]>A[j]){
					int temp=A[j];//交换元素
					A[j]=A[i];
					A[i]=temp;
				}
			}
		}
方法二:
	for(int i=0;i<A.length-1;i++){//前i趟确定i个元素的最终位置
		int flag=i;//标记元素
		for(int j=i+1;j<A.length;j++){//每趟排序从i开始,到A.length结束
			if(A[flag]>A[j]){//如果存在元素比标记元素小,将其序号给标记
				flag=j;
			}
		}
		if(flag!=i){//如果本轮循环的最小元素不是i
			int temp=A[flag];//交换元素
			A[flag]=A[i];
			A[i]=temp;
		}
	}
  • 注意事项:内外层循环的范围以及交换数值的储存方式。

5、桶排序

  • 代码实现BucketSort
  • 设计思路
    • 什么是桶排序:
      • 首先一次遍历获取数组的最大值和最小值,从而得到桶的数量,并新建数据结构。
      • 再次遍历数组,先判断属于哪个桶,再判断桶内的链表位置。
      • 将桶内数据整理为数组并返回。
    • 桶排序的时间复杂度:
      • 第一次遍历获取桶数量: O(n)
      • 找插入点:O(m + n)
    • 桶排序的稳定性:桶排序是稳定的排序方法。
    • 【代码展示】
public int[] bucketsort(int[] array, int bucketsize) {

        //创建桶,注意对象数组初始化一定要创建对象,否则为空
        HashTableNode[] hashTableNodes = new HashTableNode[bucketsize];
        for (int i = 0; i < bucketsize; i++) {
            hashTableNodes[i] = new HashTableNode();
        }
        //找出最大最小值(设置array[0]存数组最大值,array[1]存最小值),并确定桶的间距
        int max = Integer.MIN_VALUE;
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < array.length; i++) {
            max = max > array[i] ? max : array[i];
            min = min < array[i] ? min : array[i];
        }
        //桶的间距: 因为除法只保留商的原因,需要加1,向上取整
        int bucketgap = (max - min) / bucketsize + 1;

        //哈希数据(哈希之后使用插入排序,对桶内进行排序)
        for (int i : array) {
            int head = (i - min) / bucketgap;
            //桶内插入排序
            //新建节点
            HashTableNode point = new HashTableNode(i);
            //插入链表,注意新建的节点value均为Integer.MIN_VALUE
            if (Integer.MIN_VALUE == hashTableNodes[head].value) {
                hashTableNodes[head] = point;
            } else {
                //链表的插入排序
                HashTableNode pro = hashTableNodes[head];
                HashTableNode p = pro;
                while (p != null && p.value < point.value) {
                    pro = p;
                    p = p.next;
                }
                //此处有个头节点的问题
                if (hashTableNodes[head] == pro && point.value < pro.value) {
                    point.next = pro;
                    hashTableNodes[head] = point;
                } else {
                    point.next = pro.next;
                    pro.next = point;
                }
            }
        }
        //整理为数组并返回
        int count = 0;
        for (int i = 0; i < bucketsize; i++) {
            HashTableNode p = hashTableNodes[i];
            while (p != null && p.value != Integer.MIN_VALUE) {
                array[count] = p.value;
                p = p.next;
                count++;
            }
        }
        return array;
    }
  • 注意事项

6、计数排序

  • 代码实现CountSort
  • 设计思路
    • 什么是计数排序:
      • 是一种牺牲空间换取时间的做法,
      • 首先找到数值区间,然后根据每个数对应的位置加一。
    • 计数排序的时间复杂度:O(n+k)
    • 计数排序的稳定性:稳定
    • 【代码展示】
public  int[] sortMethod(int[] arrayA) {
		//安全性检测
		if (arrayA == null || arrayA.length <= 1) {
			return arrayA;
		}
		
		//1、找出原始数组的最大最小值
		int max = arrayA[0];
		int min = arrayA[0];
		for(int i: arrayA){
			max = max > i ? max : i;
			min = min < i ? min : i;
		}
		//2、定义新的数组大小  min到max之间,闭区间,有(max-min+1)个数字
		int[] arrayB = new int[max - min + 1];
		
		//3、一次循环统计数据——优化掉最小值之前的无用存储
		for (int value : arrayA) {
			arrayB[value - min]++;
		}
		/*for (int i = 0; i < arrayA.length; i++) {
			arrayB[arrayA[i]-min]++;
		}*/
		//4、返回新数组
		int[] arrayC = new int[arrayA.length];
		
		//优化前方案——两次循环,需判空,浪费循环次数
		/*int count = 0;
		for (int i = 0; i < arrayB.length; i++) {
			while(arrayB[i] != 0){
				arrayC[count++] = i + min;
				arrayB[i]--;
			}
		}*/
		
		/*优化后方案:1、找出对应位有多少累加的统计数据;
		          2、根据累积数据填充b数组。
		 *注意事项:1、i必须到0,否则arrayA[0]无法遍历到
		         2、b数组,必须先--,考虑到arrayB中最小的统计位是1,表示有一个这样的数
		           这个数对应整个数组的最小值(即0位),所以必须先--*/
		for (int i = 1; i < arrayB.length; i++) {
			arrayB[i] += arrayB[i-1];
		}
		
		for (int i = arrayA.length - 1; i >= 0; i--) {
			arrayC[--arrayB[arrayA[i] - min]] = arrayA[i];
		}
		//原址排序
		for (int i = 0; i < arrayA.length; i++) {
			arrayA[i] = arrayC[i];
		}
		return arrayA;
	}
  • 注意事项

7、归并排序

  • 代码实现MergeSort
  • 设计思路
    • 什么是归并排序:
      • 把两个数组合并为一个数组,
      • 合并的过程中,按照大小排序,那么新数组自然是有序的。
    • 归并排序的时间复杂度:
    • 归并排序的稳定性:
    • 【代码展示】
public int[] mergesort(int[] array, int left, int right, int[] temp){
		
		if(left < right){
			int mid = (left+right)/2;
			mergesort(array, left, mid, temp);
			mergesort(array, mid+1, right, temp);
			sort(array, left, mid, right, temp);
		}
		return array;
	}
	
	//闭区间合并
	public void sort(int[] array, int left, int mid, int right, int[] temp) {

		int l = left;
		int r = mid + 1;
		int j = left;
		while(l <= mid && r <= right){
			if(array[l] > array[r]){
				temp[j++] = array[r++];
			}else{
				temp[j++] = array[l++];
			}
		}
		while(l <= mid){
			temp[j++] = array[l++];
		}
		while(r <= right){
			temp[j++] = array[r++];
		}
		//灌装数组
		for (int i = left; i <= right; i++) {
			array[i] = temp[i];
		}
	}
  • 注意事项

8、基数排序

  • 代码实现RadixSort
  • 设计思路
    • 什么是基数排序:
    • 基数排序的时间复杂度:
    • 基数排序的稳定性:
    • 【代码展示】
public int[] sortMethod(int[] array) {
		//边界判空
		if (array == null || array.length <= 1) {
			return array;
		}
		//找出最大位数
		int max = Integer.MIN_VALUE;
		int temp = 0;
		for (int i = 0; i < array.length; i++) {
			temp = array[i] > 0 ? array[i] : -array[i];
			if(max < temp){
				max = temp;
			}
		}
		int d = String.valueOf(max).length();
		//基数排序位数d 和 radix
		radixsort(array, d, 10);
		return array;
	}
	
	public int[] radixsort(int[] array, int d, int radix) {

		//当前循环到的位数,从第一位到第 d 位
		int radixtemp = 1;
		//二维数组用于基数排序、radixpoint是二维桶内指针,每个桶一个,所以其本身也是一个数组
		int[][] radixbucket = new int[radix][array.length];
		int[] radixpoint = new int[radix];
		int temp = 0;

		//循环控制位 loopcount
		for(int loopcount = 0; loopcount <= d; loopcount++){
			//基数排序(可以使用数组加链表的哈希结构或者二维数组,利弊均有)
			for (int i = 0; i < array.length; i++) {
				//取出当前尾数,用于排序
				temp = (array[i] / radixtemp) % radix;
				radixbucket[temp][radixpoint[temp]] = array[i];
				radixpoint[temp]++;
			}
			//基数统计合并,通过桶内指针判断桶内是否还有元素
			int bucketcount = 0;
			for (int i = 0; i < radix; i++) {
				int j = 0;
				while(radixpoint[i] != 0){
					array[bucketcount++] = radixbucket[i][j++];
					radixpoint[i]--;
				}
			}
			//修改循环变量
			radixtemp *= radix;
		}
		return array;
	}
  • 注意事项

9、希尔排序

  • 代码实现ShellSort
  • 设计思路
    • 什么是希尔排序:
      • 将原本的一个数组,分成若干个数组,独自排序
      • 第一次分成n个数组,第二次分成n/2个数组,第三次分成n/4个…
      • 每次排序才有插入排序来调整序列
    • 希尔排序的时间复杂度:
    • 希尔排序的稳定性:
      • 不稳定,因为分队排序的时候,可能会导致排序的先后顺序改变
    • 【代码展示】
public class ShellSort implements SortInterface {
    @Override
    public void sortMethod(int[] array) {
        int length = array.length;
        //间隔gap ,直到gap为1结束
        int gap = length / 2;
        for (; gap >= 1; gap /= 2) {//注意此处的gap等于1,就相当于全排序,是需要的,“=”不能少
            for (int i = 0; i < gap; i++) {
                //内部使用插入排序
                for (int j = i + gap; j < length; j += gap) {
                    int temp = array[j];
                    int k = j;
                    while ((k - gap) >= 0 && (array[k - gap] > temp)) {//k-gap 要判断越界
                        array[k] = array[k - gap];
                        k -= gap;
                    }
                    if (k != j) {
                        array[k] = temp;
                    }
                }
            }
        }
    }
}
  • 注意事项
    • gap的大小设置,
    • 注意到代码中含有四次嵌套的循环,每次的起止需要注意
    • 注意一下越界问题
    • 插入排序的写法需要注意

10、快排及其改进

这篇文章针对快排的单项、双向、改进分别做了详细解释,
请移步:快排及其优化

11.1、解压字符串并且排序输出

  • 代码实现Decompress
  • 设计思路
    • 解题步骤
    • 代码展示
在这里插入代码片
  • 注意事项

11.2、找出超过半数的那个数字

  • 代码实现HalfPastNum
  • 设计思路
    • 解题步骤
    • 代码展示
在这里插入代码片
  • 注意事项

11.3、返回数组中最小的k个数

  • 代码实现ReturnKMin
  • 设计思路
    • 解题步骤
    • 代码展示
在这里插入代码片
  • 注意事项

11.4、返回数组中最小的第k个数

  • 代码实现ReturnKthMin
  • 设计思路
    • 解题步骤
    • 代码展示
在这里插入代码片
  • 注意事项

11.5、统计字符串中首先出现三次的英文字符

在这里插入代码片
  • 注意事项

猜你喜欢

转载自blog.csdn.net/ljfirst/article/details/102762758