不稳定: 选择,快排,堆排
稳定: 插排,冒泡, 归并
选择排序从数组中选择最小的元素,将它与第一个元素交换位置,再从数组剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置,不断进行这样的操作,直到将整个数组排序。
选择排序需要 N^2 / 2次比较 以及 N次交换,它的运行时间和输入无关,即使是排好序的数组也需要这么多的比较和交换操作。
public static void selectSort(int[] arr) {
if (arr == null || arr.length < 2) return ;
for (int i = 0; i < arr.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
swap(arr, i, minIndex);
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
从左到右不断交换相邻逆序的元素,在议论循环之后,可以将未排序的最大元素上浮到最右侧,在一轮循环中,那么说明数组已经是有序的,此时可以直接退出。
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length < 2) return;
boolean isSorted = false;
for (int i = arr.length - 1; i > 0 && !isSorted; i--) {
isSorted = true;
for (int j = 0; j < i ; j++) {
if (arr[j] > arr[j + 1]) {
isSorted = false;
swap(arr, j , j + 1);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
每次都将当前元素插入到左侧已经排好序的数组中(在左侧排好序的数组中寻找一个合适的位置插入),使得插入之后左侧的数组依然有序。
插入排序的时间复杂度取决于数组的数组的初始顺序,如果数组已经部分有序,那么逆序比较少,需要的交换次数也比较少,时间复杂度也比较低
public static void insertSort(int[] arr) {
if (arr == null || arr.length < 2) return;
for (int i = 1; i < arr.length; i++) {
for (int j = i - 1; j >= 0; j--) {
if (arr[j] > arr[j + 1]) {
swap(arr, j, j + 1);
}
}
}
}
public static void swap(int[] arr, int i, int j) {
arr[i] = arr[i] ^ arr[j];
arr[j] = arr[i] ^ arr[j];
arr[i] = arr[i] ^ arr[j];
}
147. 对链表进行插入排序
public ListNode insertionSortList(ListNode head) {
if (head == null || head.next == null)return head;
ListNode dummyHead = new ListNode(-1);
dummyHead.next = head;
ListNode insert = dummyHead;
ListNode cur = head;
while (cur != null && cur.next != null) {
if (cur.val < cur.next.val) { //cur.next < cur 退出 1 2 4 5 (cur) 3 (cur.next) 产生逆序
cur = cur.next;
continue;
}
insert = dummyHead;
while (insert.next.val < cur.next.val) { //在排好序的insert 到 cur之间寻找合适的插入位置 ,插入到insert之后。cur.next是待插入的节点。
insert = insert.next; //退出循环时 insert.next > cur.next 2.next = 4 > 5.next = 3
}
ListNode temp = cur.next;
cur.next = temp.next;
temp.next = insert.next;
insert.next = temp;
}
return dummyHead.next;
}
java实现希尔排序(思路与实现)
【排序算法】希尔排序原理及Java实现
希尔排序的基本思想是:将数组列在一个表中并对列分别进行插入排序,重复组合格过程,不过每次用更少的列,最后整个表就只有一列,将数组转换至表是为了更好的理解这个算法,算法本事还是使用数组进行排序的。
public static void shellSort(int[] nums) {
if (nums == null || nums.length < 2) return;
int len = nums.length;
int gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len; i+= gap) {
if (nums[i] < nums[i - gap]) { //在每个gap上进行插入排序。
int temp = nums[i];
int j = i - gap;
while (j >= 0 && nums[j] > temp) {
nums[j + gap] = nums[j];
j = j - gap;
}
nums[j + gap] = temp;
}
}
gap /= 2;
}
}
归并排序的思想就是将数组分为两部分,分别对左部分和右部分进行排序,然后归并起来。
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2)return;
mergeSort(arr, 0 , arr.length - 1);
}
private static void mergeSort(int[] arr, int left, int right) {
if (left == right)return;
int mid = left + (right - left) / 2;
mergeSort(arr,left, mid);
mergeSort(arr,mid + 1, right);
merge(arr, left, mid ,right);
}
private static void merge(int[] arr, int left, int mid, int right) {
int[] help = new int[right - left + 1];
int index = 0;
int p1 = left;
int p2 = mid + 1;
while (p1 <= mid && p2 <= right) {
help[index++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= mid) {
help[index++] = arr[p1++];
}
while (p2 <= right) {
help[index++] = arr[p2++];
}
for (int i = 0; i < help.length; i++) {
arr[left + i] = help[i];
}
}
88. 合并两个有序数组
public void merge(int[] nums1, int m, int[] nums2, int n) {
int[] nums3 = new int[nums1.length];
int p1 = 0;
int p2 = 0;
int index = 0;
while (p1 < m && p2 < n) {
nums3[index++] = nums1[p1] < nums2[p2] ? nums1[p1++] : nums2[p2++];
}
while (p1 < m) {
nums3[index++] = nums1[p1++];
}
while (p2 < n) {
nums3[index++] = nums2[p2++];
}
for (int i = 0; i < nums1.length; i++) {
nums1[i] = nums3[i];
}
}
从后往前遍历,省去nums3的空间
public void merge(int[] nums1, int m, int[] nums2, int n) {
int p1 = m - 1;
int p2 = n - 1;
int index = nums1.length - 1;
while (p1 >= 0 && p2 >= 0) {
nums1[index--] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--];
}
while (p2 >= 0) {
nums1[index--] = nums2[p2--];
}
}
21. 合并两个有序链表
递归
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null)return l2;
if (l2 == null)return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
迭代
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummyHead = new ListNode(-1);
ListNode cur = dummyHead;
while (l1 != null && l2!= null) {
if (l1.val < l2.val) {
cur.next = l1;
l1 = l1.next;
}else {
cur.next = l2;
l2 = l2.next;
}
cur = cur.next;
}
if (l1 != null)cur.next = l1;
else cur.next = l2;
return dummyHead.next;
}
21. 合并K个排序链表
直接对链表数组进行分治算法,然后在进行双链表的归并
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0)return null;
return mergeKLists( lists, 0 , lists.length - 1);
}
private ListNode mergeKLists(ListNode[] lists, int left ,int right) {
if (left == right) return lists[left];
int mid = left + (right - left) / 2;
ListNode l1 = mergeKLists(lists, left , mid);
ListNode l2 = mergeKLists(lists, mid + 1, right);
return mergeTwoLists(l1, l2);
}
private ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
}else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
148. 排序链表
对单链表进行归并
public ListNode sortList(ListNode head) {
return head == null ? head : mergeSort(head);
}
private ListNode mergeSort(ListNode head) {
if (head == null)return null;
if (head.next == null)return head;
ListNode midPre = getMidPre(head);
ListNode mid = midPre.next;
midPre.next = null;
ListNode left = mergeSort(head);
ListNode right = mergeSort(mid);
return mergeTwoSortedList(left, right);
}
private ListNode mergeTwoSortedList(ListNode left, ListNode right) {
if (left == null)return right;
if (right == null) return left;
if (left.val < right.val) {
left.next = mergeTwoSortedList(left.next, right);
return left;
}else {
right.next = mergeTwoSortedList(left, right.next);
return right;
}
}
private ListNode getMidPre(ListNode head) {
ListNode fast = head.next;
ListNode slow = head;
ListNode pre = slow;
while (fast != null && fast.next != null) {
pre = slow;
slow = slow.next;
fast = fast.next.next;
}
return pre;
}
public static void quickSort(int[] arr) {
if (arr == null || arr.length < 2)return;
quickSort(arr, 0 , arr.length - 1);
}
private static void quickSort(int[] arr, int left, int right) {
if (left < right) {
//随机快排,避免由于原数据的特殊性(已排好序),将时间复杂度变为O(n^2)
swap(arr, right, left + (int)((right - left + 1) * Math.random()));
int[] p = partition(arr, left, right);
quickSort(arr, left, p[0] - 1);
quickSort(arr, p[1] + 1, right);
}
}
private static int[] partition(int[] arr, int left, int right) {
int less = left - 1;
int more = right + 1;
int value = arr[right]; //以数组的右边界值作为划分值
int cur = left;
while (cur < more) {
if (arr[cur] < value) {
swap(arr, ++less, cur++); //和小于区域的下一个位置交换
}else if (arr[cur] > value) {
swap(arr, --more, cur); //和大于区域的前一个位置交换
}else {
cur++;
}
}
return new int[]{less + 1, more - 1}; //返回等于区域的开始位置和结束位置
}
荷兰国旗问题
75. 颜色分类
1.第一种方法就是计数排序,统计0 , 1, 2出现的次数。但是需要扫描两遍,不符合题目的要求
public void sortColors(int[] nums) {
int[] count = new int[3];
for (int i = 0; i < nums.length; i++) {
count[nums[i]]++;
}
int index = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < count[i]; j++) { //count[0] 0出现的次数
nums[index++] = i;
}
}
}
快速排序的思想,以1作为划分值,小于1的放在左边 大于1的放在右边
public void sortColors(int[] nums) {
int value = 1;
int less = -1;
int more = nums.length;
int cur = 0;
while (cur < more) {
if (nums[cur] < value) {
swap(nums,++less,cur++);
}else if (nums[cur] > value) {
swap(nums, --more, cur);
}else {
cur++;
}
}
}
private void swap(int[] nums, int i , int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
堆 完全二叉树
大根堆 在一颗完全二叉树中,任何一颗子树的最大值都是这颗子树的头部
堆中某个节点的值总是大于等于其子节点的值,并且堆是一颗完全二叉树
堆可以用数组来表示,这是因为堆是完全二叉树,而完全二叉树很容易就存储在数组中,位置k的节点的父节点的位置为k - 1 / 2,它的两个子节点的位置分别为2k和 2k + 1
2.上浮和下沉
上浮: heapInsert构建堆的过程中,当新加入的节点其幅节点要大的时候,就需要交换着两个节点,交换后还可能比它新的父节点还要大,因此需要不断的进行比较和交换操作
下沉:(heapify)当一个节点比子节点,就需要不断下沉的过程,把这种操作称为下沉,一个节点如果有两个子节点,应当与两个子节点中最大的那个节点进行交换
3。从数组顶端删除最大的元素,与数组最后一个元素进行交换,并让这个元素下沉到合适的位置。
1.构建堆
2. 交换堆顶元素与最后一个元素,并将堆的大小减1
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) return;
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int right = left + 1;
int largest = right < size && arr[right] > arr[left] ? right : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
215. 数组中的第K个最大元素
堆排序的思想
public int findKthLargest(int[] nums, int k) {
Queue<Integer> heap = new PriorityQueue<>();
for (int x : nums) {
if (heap.size() < k) {
heap.add(x);
}else {
if (x > heap.peek()) {
heap.poll();
heap.offer(x);
}
}
}
return heap.peek();
}
桶排序的基本思路是:
假设数据是均匀分布的,则每个桶的元素的平均个数为n / k,假设选择用快速排序对每个桶内的元素进行排序的话,那么每次排序的时间复杂度就是O(n/k log(n /k) )总的时间复杂度 为 O(n)+O(m)O(n/klog(n/k)) = O(n+nlog(n/k)) = O(n+nlogn-nlogk 。当k接近于n的嘶吼,桶排序的时间复杂度就可以近似为O(n)的,即桶越多,时间效率就越高,而桶越多时间复杂对就越大。
41. 缺失的第一个正数
题解:
桶的思想 + 抽屉原理
public int firstMissingPositive(int[] nums) {
bucketSort(nums);
int len = nums.length;
for (int i = 0; i < len; i++) {
if (nums[i] != i + 1) { //[1, - 1, 3, 4]
return i + 1;
}
}
return len + 1; //都正确则返回len + 1
}
private static void bucketSort(int[] nums) {
if (nums == null || nums.length < 1)return ;
int n = nums.length;
for (int i = 0; i < n; i++) {
//满足在指定的范围内,并且没有放在正确的位置上,才交换
//3应该放在2位置上
while (nums[i] > 0 && nums[i] < n && nums[nums[i] - 1] != nums[i]) {
swap(nums, i , nums[i] - 1);
}
}
}
private static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
164. 最大间距
public int maximumGap(int[] nums) {
if (nums == null || nums.length < 2) return 0;
int len = nums.length;
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for (int i = 0; i < nums.length; i++) { //找到数组的最大值与最小值
max = Math.max(max, nums[i]);
min = Math.min(min, nums[i]);
}
if (min == max) return 0;
boolean[] hasNum = new boolean[len + 1]; //准备 len + 1 个桶
int[] maxs = new int[len + 1]; //用于存放每个桶中的最大值
int[] mins = new int[len + 1]; //用于存放每个桶中的最小值
int bid = 0;//记录元素在桶中的位置
for (int i = 0; i < len; i++) {
bid = bucket(nums[i], len , min, max);
mins[bid] = hasNum[bid] ? Math.min(mins[bid], nums[i]) : nums[i];
maxs[bid] = hasNum[bid] ? Math.max(maxs[bid], nums[i]) : nums[i];
hasNum[bid] = true;
}
//开始求最大的间隙
int res = 0;
int lastMax = maxs[0];
for (int i = 1; i <= len; i++) {
if (hasNum[i]) {
res = Math.max(mins[i] - lastMax, res);
lastMax = maxs[i];
}
}
return res;
}
private static int bucket(long num, int len, int min, int max) {
return (int)((num - min) * len / (max - min)); //元素在同种的下标
}
计数排序(Counting Sort)是一种O(n)的排序算法,其思路是开一个长度为 然
后开一个maxValue-minValue+1的数组,
举个例子, nums=[2, 1, 3, 1, 5] , 首先扫描一遍获取最小值和最大值, 于是开一个长度为5的计数器数组 counter ,
maxValue=5 ,
minValue=1 ,
分配。统计每个元素出现的频率,得到 counter=[2, 1, 1, 0, 1] ,例如 counter[0] 值表示 0+minValue=1 出现了2次。
收集。 counter[0]=2 表示 1 出现了两次,那就向原始数组写入两个1, counter[1]=1表示2出现了1次,那就向原始数组写入一个2,依次类推,最终原始数组变为[1,1,2,3,5],排序好了。
计数排序本质上是一种特殊的桶排序,当桶的个数最大的时候,就是计数排序。
public static void bucketSort(int[] arr) {
if (arr == null || arr.length < 2)return;;
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int[] bucket = new int[max + 1];
for (int i = 0; i < arr.length; i++) {
bucket[arr[i]]++;
}
int index = 0;
for (int j = 0; j < bucket.length; j++) {
while (bucket[j]-- > 0) {
arr[index++] = j;
}
}
}
274. H指数
想象一个直方图,其中x表示文章数,y中表示每篇文章的引用次数。如果将这些文章按照引用次数,降序排序并在直方图上表示,那么直方图上的最大的正方形边长h,就是我们要求的h
public int hIndex(int[] citations) {
Arrays.sort(citations);
reverse(citations);
for (int i = 0; i < citations.length; i++) {
if (i + 1 == citations[i])return i + 1;
if (i + 1 > citations[i]) return i;
}
return citations.length;
}
private static void reverse(int[] nums) {
int left = 0;
int right = nums.length - 1;
while (left < right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
left++;
right--;
}
}
思路二: 计数排序,不过排序算法换成了计数排序。有一个小技巧,因为H-Index最大不可能超过论文 综述,所以我们只需要开一个长度为 n+1 的数组,如果某篇论文的引用数超过了 n ,就将其当做 n 。
public int hIndex(int[] citations) {
int n = citations.length + 1;
int[] papers = new int[n + 1];
for (int x :citations) {
papers[Math.min(x, n)]++;
}
int sum = 0;//当前的文章数
for (int i = n; i > 0; i--) {
sum += papers[i];
if (sum >= i) {
return i;
}
}
return 0;
}
基数排序是一种费比较的排序算法,时间复杂度是O(n),它的主要思路是
举个例子,有一个整数序列 0 , 123, 45, 386, 106 先面试排序的过程:
第一次排序个位:
000, 123, 045, 386, 106
第二次排序十位:
000,106,123,045,386
第三次排序百位:
000, 045, 106, 123,386 排序完成。
**为什么同一数位的排序子程序要用稳定排序?**因为稳定排序能将上一次排序的成果保留下来,例如十位数的排序过程能保留个位数的排序成果,百位数的排序过程能够保留十位数的排序成果。
public static void radixSort(int[] arr) {
if (arr == null || arr.length < 2) return;
int max = Integer.MIN_VALUE;
for (int x : arr) {
max = Math.max(x, max); //找到数组中最大的数
}
int maxBit = 0; //计算最大的数的位数
while (max != 0) {
maxBit++;
max /= 10;
}
Queue<Integer>[] queues = new LinkedList[10];
for (int i = 0, mod = 1; i < maxBit; i++, mod *= 10) {
for (int x : arr) {
int index = (x / mod) % 10;
queues[index].add(x);
}
int count = 0;
for (Queue<Integer> queue: queues) {
while (queue.size() > 0) {
arr[count++] = queue.poll();
}
}
}
}
小结:
快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为他总是顺序的访问数据,它的运行时间近似为~cNlogN,这里的c比其他线性对数级别的排序算法都小
使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而且他排序算法仍然需要线性对数时间。
Java主要的排序方式为Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序(因为归并排序具有稳定性)
参考:
CS-NOTES