Java超详细的快速排序,堆排序,归并排序算法解析

阅前提示:

    建议把代码复制到本地 ide 里面,方便查看

    以及下面的算法,你只需要印象里知道这个算法的原理,代码要看不懂你来找我

快速排序:

### 解题思路

    新手做题的时候发现,没有java代码的详细的标准快速排序解法

    于是自己写了个快速排序的详解

    解析写在注释里面了

    时间复杂度:

    平均时间复杂度:O(nlogn);最好情况: O(nlogn) ;最坏情况:O(n^2)

快排代码:
 

### 代码

	public int[] sortArray(int[] nums) {
                sort(nums);
	        return nums;
        }

	public static void sort(int[] a) {
	        sort(a, 0, a.length - 1);
	}
	
	public static void sort(int[] a, int lo, int hi) {

		if (hi <= lo) return;
		int j = partition(a, lo, hi);	// 切分
		sort(a, lo, j - 1);	// 将左半部分排序
		sort(a, j + 1, hi);	// 将右半部分排序
	}
	
	private static int partition(int[] a, int lo, int hi) {
		// 将数组切分为a[lo,...,j-1],a[j],a[j+1,...hi]
		// 左右扫描指针, 此处 hi+1是因为,下面算法中从右访问用的是a[--j], 
		// 虽然,从左访问同样是a[++i],但因为我们的标志元素v用的是a[lo],所以没有必要从最左边起自己和自己比一遍。
		int i = lo, j = hi + 1;	// 左右扫描指针
		int v = a[lo];	// 切分元素,标志元素
		
		while (true) {
			// 扫描左右,检查扫描是否结束并交换元素

			// 如果 a[lo+1,....,hi] 全比 v小 ,即 全比 a[lo]小, 就会触发 里面的break条件
			// 这个循环的作用是 让 i(左指针),移动到 >= v(即 >= a[lo]) 的地方
			while (less(a[++i], v)) {
                // 这里的跳出条件是跟边界匹配的,应对极端的直接移动到边界的情况
                // 为了不写更复杂的逻辑,所以直接写成匹配边界
                // 后面的稍微注意一下,就很省事
				if (i == hi) break;
			}

			// 同理,这个循环的作用是让右指针j,移动到 a[j] <= v 的地方
			while (less(v, a[--j])) {
				if (j == lo) break;
			}

			// 判断结束条件,如果 左指针 移到了 右指针 重合甚至更右的地方,就结束本轮
			if (i >= j) break;

//			exch(a, i, j);	 交换的作用
			// 现在是 a[i] > v > a[j]
			// 所以交换 a[i] 和 a[j]
			
			a[j] ^= a[i];
			a[i] ^= a[j];
			a[j] ^= a[i];
		}


//		exch(a, lo, j);	// 将 v = a[j] 放入正确的位置
		// 如果 i >= j , 因为上面大循环内,第一个小循环就确保了: i及i左边的元素全部小于 a[lo],。
		// 所以目前a[j] < a[lo], 那么交换 a[lo] 和 a[j] 就可以完成最终排序
		// 即 标志位 左边全小于&&右边全大于 标志
		// 交换a[lo] 和 a[j]
		
		/* 这里不能用异或,因为,i可能和j重合,自己和自己异或完变零了
		 * 
		a[j] ^= a[lo];
		a[lo] ^= a[j];
		a[j] ^= a[lo];
		*/
		
		// 如果 i >= j , 因为上面大循环内,第一个小循环就确保了: i左边的元素全部小于 a[lo]。
		// 第一个小循环出来有以下可能性:1、左游标当前所指大于等于标志位。
		// 2、左游标指向数组末尾,且该数据小于标志位
		// 3、左游标当前所指为数组末尾,且大于等于标志位。
		
		// 如果左右未相遇,
		// 那么右游标当前所指为任一小于等于标志位的元素。
		// 然后 左游标和右游标所指元素交换,交换后 左游标及左游标左边小于等于标志位
		// 大循环继续

		// 如果左右能相遇,那么退出大循环:
		// 第二个小循环根据第一个小循环有如下情况
		// 如果是 情况1 ,那么右游标 指向 左游标的左一位 或  和 左游标重合
		// 如果是 情况2, 那么右游标 指向 数组末尾,和左游标重合
		// 如果是 情况3, 那么右游标可能指向 数组末尾和左游标重合,也可能指向左游标右一位
		// 如果相遇了,则 左右游标目前并不做交换
		
		// 
		// 左游标:左边小于等于标志位
		// 右游标:右边大于等于标志位
		// 若左右游标重合,则标志位任意和左右游标交换都没有问题
		// 若左游标在右游标右边 即 
		// 右游标及右游标左边小于等于标志位  左游标及左游标右边大于等于标志位
		// 又因为我们的标志位是从最左边取的,说明被交换的元素,需要满足小于等于标志位
		// 故我们需要交换标志位和右游标。
		


		// 要和代表小的那一个交换,因为,标志位是取的数组最左侧
        // 核心在于:哪个游标指的数字应该是 小于等于 标志位的
        // 我们就应该把标志位和哪个游标交换
		// 交换完要满足左边的全比 lo 标志的小, 所以 右游标 和 lo交换, 
        // 交换后  nums[j] 左边全是比 nums[j] 小的

		int temp = a[lo];
		a[lo] = a[j];
		a[j] = temp;
	

		// 所以根据上面的分析,此处返回的下标应该是和标志位交换的那个下标
		// 即 返回 右游标
		return j;	// a[lo,...,j-1] <= a[j] <= a[j+1,....,hi]达成
	}
	
	// 判断a是否小于v
	public static boolean less(int a, int v) {
		return a < v;
	}

堆排序:

### 解题思路

    下面的是堆排序。

    这里的堆排序和《算法(第四版)》唯一的区别是做了调整,让其从数组的0下标开始

    

    时间复杂度:

    平均时间复杂度:O(nlogn);最好情况: O(nlogn) ;最坏情况:O(nlogn)

堆排序代码:

### 代码

    public int[] sortArray(int[] nums) {
        sort(nums);
        return nums;
    }

    public static void sort(int[] nums) {
        // 这里需要 - 1 ,因为要访问最右边界的话,需要-1
        int n = nums.length - 1;

        // 从第一个非叶子节点开始,让倒数第二层开始依次往前保证自己是个大顶堆
        // 最终到了根节点后,就全是大顶堆了
        // 此时的大顶堆,不能保证左右孩子有序
        // 数学角度考虑,完全二叉树放到数组后,各层之间的数学关系,下面也是如此
        // 这里实际上是 k = nums.length / 2 - 1
        for (int k = (n - 1) / 2; k >= 0; k--) {
            sink(nums, k, n);
        }

        // 最大的放在末尾依次向前排序
        while (n > 0) {
            exch(nums, 0, n--); // 把目前最大的数放到数组末尾, 最大的沉下去了,肯定是升序数组啊
            sink(nums, 0, n); // 从头部开始,到已经放过的数的前一个,恢复大顶堆,再次找到最大的那个数
        }
        // 上面的循环, 实际上是这个意思
//        for (int i = 0; i < n; i++) {
//            exch(nums, 0, n - i);
//            sink(nums, 0, n - i - 1);
//        }

    }


    // n表示数组最右位的下标,防越界
    public static void sink(int[] a, int k, int n) {
        while (true) {
            int j = 2 * (k + 1) - 1;
            // 如果 j > n 说明当前节点是倒数第一层,所以退出循环。
            if (j > n) {
                break;
            }
            // 保证后面比较且交换的是更大的那个子节点。
            // 并且从这段代码可以猜测,大顶堆从 0 开始建的话,任意节点的子节点都是 2x 和 2x+1
            if (j < n && less(a, j, j + 1)) {
                j++;
            }
            // 大于 大的那个子节点,就可以调出循环,结束sink(),不用沉了。
            if (!less(a, k, j)) {
                break;
            }
            // 如果比子节点中 大的 那个 小,则交换大的到当前位置
            exch(a, k, j);
            // 更新标记游标,再次循环计算是否需要和子节点发生交换
            k = j;
        }

    }




    public static void exch(int[] a, int i, int j) {
        int t = a[i];
        a[i] = a[j];
        a[j] = t;
    }

    public static boolean less(int[] a, int i, int j) {
        return a[i] < a[j];
    }
	/*
	 * // 仅排序的话,用不着他们 // 并且并不知道这里的数学关系调整对了没
        public static void sink(int[] a, int k) {
            while (2 * k + 1 <= a.length) {
                int j = 2 * k + 2;
                if (j < a.length && less(a, j, j + 1)) j++;
                if (!less(a, k, j)) break; // k >= j , 则会触发break
	        exch(a, k, j);
                k = j;
            }
        }

	public static void swim(int[] a, int k) {
            while (k > 0 && less(a,k / 2, k)) {
	        exch(a, k / 2 + 2, k);
                k = k / 2 + 2;
            }
        }
	 */

归并排序:

    思想是分治,先将问题,递归地化解成小问题。这里是将数组递归地拆分成1个长度的数组,然后通过新建临时数组,进行插入排序。

    时间复杂度:

    平均时间复杂度:O(nlogn);最好情况: O(nlogn) ;最坏情况:O(nlogn)

归并排序代码:

public int[] sortArray(int[] nums) {
        mergeSort(nums, 0, nums.length - 1);
        return nums;
    }

    public void mergeSort(int[] nums, int left, int right) {	// 需要左右边界确定排序范围
        if (left >= right) {
            return;
        }
        int mid = (left + right) / 2;

        mergeSort(nums, left, mid);	// 先对左右子数组进行排序
        mergeSort(nums, mid+1, right);

        int[] temp = new int[right - left + 1];	// 临时数组存放合并结果
        int i = left, j = mid+1;
        int cur = 0;
        while (i <= mid && j <= right) {	//开始合并数组
            // 把当前数组人为地看成左右各一半,然后左右各自从头比较,插入。
            // 能这样做的原因是,左右各一半数组都是有序的?
            if (nums[i] <= nums[j]) { 	// 这里这个 = 号 , 保证了算法的稳定性,即: 左侧的,依然在左侧
                temp[cur] = nums[i++];
            } else {
                temp[cur] = nums[j++];
            }
            cur++;
        }

        // 处理剩下来的那个数组,挨个填进去就好
        while (i <= mid) {
            temp[cur++] = nums[i++];
        }
        while (j <= right) {
            temp[cur++] = nums[j++];
        }

        // 把临时数组挨个填进目标数组的应有位置
        // 保留问题,这里可以不使用额外的存储空间吗?
        for (int k = 0; k < temp.length; k++) {
            nums[left + k] = temp[k];
        }
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值