介绍一些排序基础算法
相关
一、排序链表
-
排序链表,输入:head = [4,2,1,3] 输出:[1,2,3,4]
-
很容易想到将链表转为集合(比如List),然后排序,这不符合学习算法的目的,为了理解掌握而不是只为了通过。
-
此时会联想常用的排序算法,快排和堆排是O(nlogn),但为原址排序,考虑归并算法天然的合适。如果对于归并不太熟悉的,可以查看上一篇 Java排序算法(一)。
-
程序入口
public static void main(String[] args) {
ListNode head = new ListNode(4);
ListNode curNode = head;
curNode.next = new ListNode(2);
curNode = curNode.next;
curNode.next = new ListNode(1);
curNode = curNode.next;
curNode.next = new ListNode(3);
ListNodeHelper.show("head", head);
ListNode sortedHead = sortList(head);
ListNodeHelper.show("sortedHead", sortedHead);
}
- 1.2 链表归并代码
/*
归并
4->2->1->3->null
*/
public static ListNode sortList(ListNode head) {
if (head == null) {
return null;
}
return mergeSort(head);
}
/*
目的:处理到只剩余一个节点
4->2->1->3->null
4->2->null
4->null ----
|--> 2->4->null ------
2->null ---- |
|----> 1->2->3->4->null
1-3->null |
1->null ---- |
|--> 1->3->null ------
3->null ----
*/
private static ListNode mergeSort(ListNode head) {
if (head == null || head.next == null) {
return head;
}
// 1->null leftNode不为空 mid一定不为空
ListNode mid = findMidListNode(head, null);
ListNode tmp = mid.next;
mid.next = null; // 断开
ListNode left = mergeSort(head);
ListNode right = mergeSort(tmp);
return merge(left, right);
}
// 合并两个有序链表
public static ListNode merge(ListNode leftNode, ListNode rightNode) {
ListNode dummy = new ListNode(0);
ListNode curNode = dummy;
while (leftNode != null && rightNode != null) {
if (leftNode.val < rightNode.val) {
curNode.next = leftNode;
leftNode = leftNode.next;
} else {
curNode.next = rightNode;
rightNode = rightNode.next;
}
curNode = curNode.next;
}
if (leftNode != null) {
curNode.next = leftNode;
}
if (rightNode != null) {
curNode.next = rightNode;
}
return dummy.next;
}
private static ListNode findMidListNode(ListNode left, ListNode right) {
ListNode slow = left;
// 4->2->null // 这种寻找中间节点是错误的方式,会陷入死循环 => left=4->2->null right=null
// ListNode fast = left;
ListNode fast = left.next;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
二、数组中的第K个最大元素
- 给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
- 输入: [3,2,1,5,6,4] 和 k = 2 输出: 5
- 只为了寻找第K个最大元素, 其实并不需要全局有序 。
- 快速排序会利用划分元将数组分为左右两部分,左子部分小于等于划分元,右子部分大于划分元。通过比较划分元和目标顺位的位置关系。可以更快的找到结果。
- 堆排序将原始堆划分为有序和无序两个部分,寻找第K个最大元素,也就是发生了K次堆顶元素的取用。
2.1 快排优化方式
- 代码实现
public int findKthLargest(int[] nums, int k) {
int len = nums.length;
quickSort(nums, 0, len - 1, len - k);
return nums[len - k];
}
private void quickSort(int[] nums, int left, int right, int target) {
if (left < right) {
int p = partition(nums, left, right);
// 调试
// ArrayHelper.show(nums);
if (p == target) {
return;
} else if (p > target) { // 划分元在目标下标值右侧
quickSort(nums, left, p - 1, target);
} else {
quickSort(nums, p + 1, right, target);
}
}
}
private int partition(int[] nums, int left, int right) {
int i = left - 1;
randomPickPartitionNum(nums, left, right);
int x = nums[right];
for (int j = left; j < right; j++) {
if (nums[j] <= x) {
i++;
swap(nums, i, j);
}
}
// 交换划分元位置
swap(nums, i + 1, right);
return i + 1;
}
/* 随机划分元
[3 2 1 5 6 4]
randPos = 1
[3 4 1 5 6 2]
*/
private void randomPickPartitionNum(int[] nums, int left, int right) {
int range = right - left + 1;
// 随机产生一个在[left,right]下标范围内的整数
int randPos = left + random.nextInt(range);
// System.out.println(String.format("[randPos:%s][randNum:%s]", randPos, nums[randPos]));
swap(nums, randPos, right);
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
- 程序入口
public static void main(String[] args) {
int[] nums = new int[]{3, 2, 1, 5, 6, 4};
// int[] nums = new int[]{3,1,2,4};
int k = 2;
// ArrayHelper.show("输入数组", nums);
FindKthLargest dis = new FindKthLargest();
int res = dis.findKthLargest(nums, k);
System.out.println(String.format("第k个最大元素为: " + res));
}
-
结果预览
输入数组: 3 2 1 5 6 4 划分元: 6 ==> 划分后的数组: 3 2 1 5 4 6 划分元: 5 ==> 划分后的数组: 3 2 1 4 5 6 第k个最大元素为: 5
2.2 堆排序方式
- 代码实现
// 堆排序 大根堆
public int findKthLargest(int[] nums, int k) {
int len = nums.length;
// 初始化时指定无序堆最后一个元素下标
int heapSize = len - 1;
buildHeap(nums, heapSize);
// 区k次堆顶元素,表示第k大的数
for (int i = 1; i <= k; i++) {
// ArrayHelper.show("第" + i + "次交换堆顶元素前", nums);
swap(nums, 0, heapSize);
// ArrayHelper.show("第" + i + "次交换堆顶元素后", nums);
heapSize--;
adjustHeap(nums, heapSize, 0);
}
return nums[len - k];
}
// 建堆 heapSize标识无序堆的之后一个元素下标
private void buildHeap(int[] nums, int heapSize) {
int parent = getParent(heapSize); // 最后一个非叶子节点
for (int p = parent; p >= 0; p--) {
adjustHeap(nums, heapSize, p);
}
}
// 整堆
private void adjustHeap(int[] nums, int heapSize, int idx) {
int left = getLeftChild(idx);
int right = getRightChild(idx);
int largest = idx;
if (left <= heapSize && nums[left] > nums[largest]) {
largest = left;
}
if (right <= heapSize && nums[right] > nums[largest]) {
largest = right;
}
if (largest != idx) {
swap(nums, largest, idx);
adjustHeap(nums, heapSize, largest);
}
}
private int getLeftChild(int i) {
return 2 * i + 1;
}
private int getRightChild(int i) {
return 2 * i + 2;
}
private int getParent(int i) {
return (i - 1) / 2;
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
- 程序入口
public static void main(String[] args) {
int[] nums = new int[]{3, 2, 1, 5, 6, 4};
int k = 2;
// ArrayHelper.show("输入数组", nums);
FindKthLargest02 dis = new FindKthLargest02();
int res = dis.findKthLargest(nums, k);
System.out.println(String.format("第k个最大元素为: " + res));
}
-
结果预览
输入数组: 3 2 1 5 6 4 第1次交换堆顶元素前: 6 5 4 3 2 1 第1次交换堆顶元素后: 1 5 4 3 2 6 第2次交换堆顶元素前: 5 3 4 1 2 6 第2次交换堆顶元素后: 2 3 4 1 5 6 第k个最大元素为: 5