Leetcode刷题912. 排序数组

目录

冒泡排序

一般冒泡排序

冒泡排序优化

选择排序 

插入排序

直接插入排序

二分插入排序

归并排序

快速排序

刨坑法

交换指针元素法

 快速排序优化


给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入:nums = [5,2,3,1]
输出:[1,2,3,5]


示例 2:

输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
 

提示:

1 <= nums.length <= 5 * 104
-5 * 104 <= nums[i] <= 5 * 104

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/sort-an-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

冒泡排序

基本思路:

1.从头开始让相邻的两个元素进行比较,符合条件就交换位置,这样就把最大值或者最小值放到数组的最后面了;

2.接着再从头开始两两比较交换,直到把最大值或者最小值放到数组的倒数第二位。(即不需要与最后一位数进行对比).....以此类推,直到排序完成。

时间复杂度O(N^2),空间复杂度O(1)

一般冒泡排序

    public void bubbleSort(int[] nums) {
        int len = nums.length;
        //外层循环进行n-1趟冒泡排序
        for (int i = 0; i < len - 1; i++) {
            //内层循环从第一个元素开始每两个进行比较,得出最大/最小的元素
            //这里减去 i 是因为每次外层循环都会确定一个最值(已经排好序了)
            for (int j = 0; j < len - i - 1; j++) {
                if (nums[j] > nums[j + 1]) {
                    swap(nums, j, j + 1);
                }
            }
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

冒泡排序优化

在遍历的过程中,提前检测到数组是有序的,可以结束排序,这样如果输入数据是有序的,可以避免排序过程。

    public void bubbleSortWithFlag(int[] nums) {
        int len = nums.length;
        for (int i = 0; i < len - 1; i++) {
            //先默认数组有序的,只要发生一次交换,就必须进行下一轮比较
            //如果在内层循环中,都没有执行一次交换操作,说明此时数组已经是升序数组
            boolean sorted = true;
            for (int j = 0; j < len - i - 1; j++) {
                if (nums[j] > nums[j + 1]) {
                    swap(nums, j, j + 1);
                    sorted = false;
                }
            }
            if (sorted) {
                break;
            }
        }
    }

选择排序 

基本思路:

1.首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置。

2.接着再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾。

3.以此类推,直到所有元素均排序完毕。

时间复杂度O(N^2),空间复杂度O(1)

    public void selectSort(int[] nums) {
        int len = nums.length;
        //把数组分为有序和无序两部分,[0,i)区间是有序的
        for (int i = 0; i < len - 1; i++) {
            int minIndex = i;
            //找到区间[i+1,len-1]里最小的元素索引
            for (int j = i + 1; j < len; j++) {
                if (nums[j] < nums[minIndex]) {
                    minIndex = j;
                }
            }
            //每一轮选择最小元素交换到未排定部分的开头
            swap(nums, i, minIndex);
        }
    }

插入排序

基本思路:

1.将一个记录插入到已经排好序的有序表中,从而得到一个新的,记录数增加1的有序表。

2.实现时,使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,进行移动。

3.类似于生活中扑克牌游戏,会习惯性地把手上的牌按照从小到大排好,每次拿到新牌时就是插入排序的过程。

时间复杂度O(N^2),空间复杂度O(1)

直接插入排序

    public void insertionSort(int[] nums) {
        //外层循环从数组中第二个元素开始遍历
        for (int i = 1; i < nums.length; i++) {
            //先保存这个元素
            int temp = nums[i];
            int j = i;
            //如果前一个元素大于temp,将该元素之前的逐个后移,留出空位
            while (j > 0 && nums[j - 1] > temp) {
                nums[j] = nums[j - 1];
                j--;
            }
            //将temp放到合适的位置
            nums[j] = temp;
        }
    }

二分插入排序

因为左边数组是有序的,所以当元素数量比较多时,可以采用二分定位新元素插入位置。

    public void binaryInsertionSort(int[] nums) {
        for (int i = 1; i < nums.length; i++) {
            int temp = nums[i];
            int left = 0, right = i - 1;
            while (left <= right) {
                int mid = left + (right - left) / 2;
                if (nums[mid] > temp) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            }
            int j = i;
            //左边索引右边的数据 到 要排序的数据之前的数据 都往后移动一位
            while (j > left) {
                nums[j] = nums[j - 1];
                j--;
            }
            //循环结束的条件是j<=left,要插入排序的数据插入到左边索引的位置
            nums[j] = temp;
        }
    }

归并排序

感谢Leetcode大神liweiwei1419的详细解法,传送门复习基础排序算法(Java)

感谢 labuladong的详细解法,传送门归并排序的正确理解方式及运用

基本思路:先把左半边数组排好序,再把右半边数组排好序,然后把两边数组合并。

时间复杂度O(NlogN),空间复杂度O(N)

    public void mergeSort(int[] nums, int left, int right, int[] temp) {
        //小区间使用插入排序
        if (right - left <= INSERTION_SORT_THRESHOLD) {
            insertionSort(nums, left, right);
            return;
        }
        int mid = left + (right - left) / 2;
        mergeSort(nums, left, mid, temp);
        mergeSort(nums, mid + 1, right, temp);
        //如果数组的这个子区间本身有序,无需合并
        if (nums[mid] <= nums[mid + 1]) {
            return;
        }
        merge(nums, left, mid, right, temp);
    }

    private void merge(int[] nums, int left, int mid, int right, int[] temp) {
        //先把nums[left,right]copy到temp数组中,以便合并后的结果能够直接存入nums
        for (int i = left; i <= right; i++) {
            temp[i] = nums[i];
        }
        int i = left, j = mid + 1;
        for (int k = left; k <= right; k++) {
            if (i == mid + 1) {
                //左边数组已经排好序
                nums[k] = temp[j++];
            } else if (j == right + 1) {
                //右边数组已经排好序
                nums[k] = temp[i++];
            } else if (temp[i] <= temp[j]) {
                //注意写成 < 就丢失了稳定性(相同元素原来靠前的排序以后依然靠前)
                nums[k] = temp[i++];
            } else {
                nums[k] = temp[j++];
            }
        }
    }

快速排序

快速排序的思想:

1.从数组中任意选取一元素作为pivot中心轴。

2.将大于pivot的数字放在pivot的右边,将小于pivot的数字放在pivot的左边。

3.分别对左右子序列重复做前两步操作,直到各区间只有一个数。

时间复杂度O(NlogN),空间复杂度logN

刨坑法

感谢xiehongfeng的详细解释,传送门 “《算法导论》之‘排序’”:快速排序_51CTO博客_各种排序算法

    public void quickSort(int[] nums, int left, int right) {
		if (left >= right) {
			return;
		}
		int p = partition(nums, left, right);
		quickSort(nums, left, p - 1);
		quickSort(nums, p + 1, right);
	}

    //pivot在排序之前就拿出来,空一个位置
    //然后左右指针交替扫描,遇到不符合情况的就把那个不符合的数字放到空位置,直至左右指针重合
    //在排序过程中pivot是一直在序列之外的,在一趟排序之后再把pivot放回唯一的空位置就行
	private int partition(int[] nums, int left, int right) {
		//即nums[left]就是第一个坑,因为pivot选取的是左边第一个数,所以先从右往左开始移动
		int pivot = nums[left];
		int i = left, j = right;
		while (i < j) {
			//从右往左移动,跳过大于pivot的元素
			while (i < j && nums[j] >= pivot) {
				j--;
			}
			//遇到小于pivot的元素,将其放到左边空出的坑位中,并移动i
			if (i < j) {
				nums[i] = nums[j];
                i++;
			}
			//从左往右移动,跳过小于pivot的元素
			while (i < j && nums[i] <= pivot) {
				i++;
			}
			//遇到大于pivot的元素,将其放到右边空出的坑位中,并移动j
			if (i < j) {
				nums[j] = nums[i];
                j--;
			}
		}
		//退出循环时,i=j,此时将pivot指向的元素放到该坑位中
		nums[j] = pivot;
        return j;
	}

交换指针元素法

1.先从数组中选取一个数当做基准数,一般选择最左边的数当做基准数,然后从两边进行检索。

2.先从右边检索比基准数小的,再从左边检索比基准数大的。如果检索到了,就停下,然后交换这两个元素。然后再继续检索。

3.直到左右指针相遇,停止检索,把基准数和相遇位置的元素交换,此时第一轮交换结束。基准数左边都比它小,右边都比它大。

4.以后先排基准数左边,排完以后再排基准数右边,方式和第一轮一样。

    public void quickSort(int[] nums, int left, int right) {
        if (left >= right) {
            return;
        }
        int p = partition(nums, left, right);
        quickSort(nums, left, p - 1);
        quickSort(nums, p + 1, right);
    }

    private int partition(int[] nums, int left, int right) {
        int pivot = nums[left];
        int i = left, j = right;
        while (i < j) {
            //因为基准数是左边第一个数,所以先从右边开始,跳过大于pivot的元素
            while (i < j && nums[j] >= pivot) {
                j--;
            }
            //从左边开始,跳过小于pivot的元素
            while (i < j && nums[i] <= pivot) {
                i++;
            }
            //此时i指向的元素大于pivot,j指向的元素小于pivot,交换这两个指针指向的元素
            if (i < j) {
                swap(nums, i , j);
            }
        }
        //循环结束,此时i==j,将相遇位置的元素和pivot交换
        //此时第一轮交换结束。基准数左边都比它小,右边都比它大
        nums[left] = nums[i];
        nums[i] = pivot;
        return i;
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

 快速排序优化

上面两种方法都是选取最左边元素作为pivot,如果出现顺序数组或者逆序数组,快速排序会变得非常慢,因此一定要随机化选择切分元素(pivot),同时对于小区间内的数组,使用插入排序效率更高。

    private static final int INSERTION_SORT_THRESHOLD = 47;
    public final Random RANDOM = new Random();

    private void quickSort(int[] nums, int left, int right) {
        //小区间内排序,使用插入排序
        if (right - left <= INSERTION_SORT_THRESHOLD) {
            insertionSort(nums, left, right);
            return;
        }
        int p = partition(nums, left, right);
        quickSort(nums, left, p - 1);
        quickSort(nums, p + 1, right);
    }

    private int partition(int[] nums, int left, int right) {
        //随机生成[left, right + 1)区间内的数据
        int randomIndex = left + RANDOM.nextInt(right - left + 1);
        swap(nums, randomIndex, left);
        //避免出现耗时的极端情况,随机取值pivot
        int pivot = nums[left];
        //定义[left, i) <= pivot, (j,right] > pivot,遍历时要正确维护区间
        int i = left + 1, j = right;
        while (true) {
            //跳过小于pivot的元素
            while (i <= right && nums[i] < pivot) {
                i++;
            }
            //跳过大于pivot的元素
            while (j > left && nums[j] > pivot) {
                j--;
            }
            //此时满足条件[left, i) <= pivot && (j, right] > pivot,直接退出循环
            if (i >= j) {
                break;
            }
            //否则,出现[left, i) > pivot || (j, right] <= pivot,进行元素值交换
            swap(nums, i, j);
            i++;
            j--;
        }
         //跳出循环,pivot需要放置在这样一个位置,即该位置左侧的所有元素都不大于中枢元素,右侧的所有元素都不小于中枢元素
        //此时索引i 会停在第一个不小于中枢元素的位置,索引 j 指向的是最后一个不大于中枢元素的位置
        //j 位置的元素是中枢值应该放置的位置,因为它是从右向左遇到的最后一个不大于中枢值的元素。
        //i 位置的元素可能大于中枢值,所以不能与中枢值交换。
        swap(nums, left, j);
        return j;
    }

    private void insertionSort(int[] nums, int left, int right) {
        for (int i = left + 1; i <= right; i++) {
            int temp = nums[i];
            int j = i;
            while (j > left && nums[j - 1] > temp) {
                nums[j] = nums[j - 1];
                j--;
            }
           nums[j] = temp;
        }
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值