目录
简单排序算法
选择排序
排序规则:第一次现在[0:n-1]中找最小的元素,将其与第0个位置数进行交换,然后再从[1:n-1]中找最小元素,将其与第1个位置的数进行交换...
一共需要找n次,每次都要遍历数组,时间复杂度为O(n^2),空间复杂度为O(1)
public static void selectSort(int[] nums){
if (nums == null || nums.length < 2)
return;
for (int i = 0; i < nums.length - 1; i++){
int index = i;
for (int j = i + 1; j < nums.length; j++) {
index = nums[j] < nums[index] ? j : index; //如果当前元素小于之前最小的元素
}
swap2(nums, i, index);
}
}
public static void swap2(int[] nums, int i, int j){
int t = nums[i];
nums[i] = nums[j];
nums[j] = t;
}
冒泡排序
排序规则:第一次冒泡:从数组第0个位置开始,遍历到数组末尾,如果nums[i]>nums[i+1],就交换两个位置的元素;第二次冒泡:从数组第1个位置开始,规则一样...
一共需要进行冒泡n次,每次冒泡要比较n次,因此时间复杂度为O(n),空间复杂度是O(1)
通过异或运算可以不需要借助额外的辅助变量就完成两个元素的交换,前提是交换的两个元素下标不同,如果下标相同的话,会将他们全都清为0!
public static void bubbleSort(int[] nums){
if (nums == null || nums.length < 2)
return;
for (int i = nums.length-1; i > 0; i--){
for (int j = 0; j < i; j++) {
if (nums[j] > nums[j+1])
swap(nums, j, j+1);
}
}
}
public static void swap(int[] nums, int i, int j){
nums[i] = nums[i] ^ nums[j];
nums[j] = nums[i] ^ nums[j];
nums[i] = nums[i] ^ nums[j];
}
^异或运算
异或运算规则:相同为0,不同为1,1^1=0, 1^0=1, 0^1=1, 0^0=0
异或运算也已看作是无进位相加,比如10111 ^ 01101 = 11010
如果两个相同的数进行异或,其结果为0,任意一个数与0进行异或都等于其本身
异或运算满足交换律和结合律
题目:如果一个数组中,只有一个数有奇数个,其他所有数都有偶数个,如何找出这个数?要求时间复杂度是O(n),空间复杂度是O(1)
这道题目要求空间复杂度O(1),因此我们不能用HashMap去存
我们可以用异或运算,定义一个变量初始化为0,将它和数组中的每个数异或,最后的结果就是要找的数
public static int findOddOne(int[] nums){
int tmp = 0;
for (int num : nums)
tmp ^= num;
return tmp;
}
题目:如果一个数组中,只有两个数有奇数个,其他所有数都有偶数个,如何找出这个数?要求时间复杂度是O(n),空间复杂度是O(1)
同样,用异或运算,假设数组中奇数个的数为a和b,用0去异或数组中的每个数,最后的结果一定是a^b的值,并且由于a不等于b,a^b一定不等于0,那么他的32位二进制中一定有一位是1,假设a^b在第8位上是1,那么在第8位上,a和b一定不相等,假设a的32位二进制在第8位是0,a的32位二进制在在第8位是1,我们就可以将第8位是否为0将数组分为两个部分
public static int[] findOddTwo(int[] nums){
int eor = 0;
int[] ans = new int[2];
for (int num : nums)
eor ^= num;
int m = eor & (~eor + 1); //通过将eor与上eor取反加1来求到eor最右边为1的位置
int tmp = 0;
for (int num : nums){
if ((num & m)== 0) //如果num在该位置上是0
tmp ^= num;
}
ans[0] = tmp;
ans[1] = tmp ^ eor;
return ans;
}
也可以通过循环找a^b最右边为1的位置
int m = 1;
while ((eor & m) == 0)
m <<= 1;
插入排序
排序规则:从第1个位置开始(第0个位置天然有序),从右往左看,如果nums[j] > nums[j+1],那么交换,直到nums[j] <= nums[j+1]
时间复杂度O(n^2),当数组是有序的时候,插入排序的时间复杂度是O(n)
public static void insertSort(int[] nums){
for (int i = 1; i < nums.length; i++){
for (int j = i-1; j >= 0 && nums[j] > nums[j+1]; j--){
swap(nums, j, j+1);
}
}
}
二分法
在一个有序的数组上,找满足>=target的最左位置
public static int bigNearestIndex(int[] nums, int target){
int left = 0, right = nums.length - 1;
int index = -1;
while (left <= right){
int mid = left + ((right - left) >> 1);
if (nums[mid] >= target){ //如果mid大于等于target,则直接抛弃右边的,将right置为mid-1,并用index记录下mid
index = mid;
right = mid - 1;
}else
left = mid + 1;
}
return index;
}
public static int test(int[] nums, int target){
for (int i = 0; i < nums.length; i++) {
if (nums[i] >= target)
return i;
}
return -1;
}
public static int[] generateRandomArray(int maxSize, int maxValue){
int[] arr = new int[(int)Math.random() * (maxSize + 1)];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int)((maxSize + 1) * Math.random()) - (int)(maxValue * Math.random());
}
return arr;
}
public static void printArray(int[] arr){
if (arr==null){
return;
}
for (int i=0;i<arr.length;i++){
System.out.print(arr[i]+" ");
}
System.out.println();
}
public static void main(String[] args) {
int testTime=500000;
int maxSize=100;
int maxValue=100;
boolean succeed=true; //完成与否标记
for(int i=0;i<testTime;i++){
//1 生成自定义大小的随机数组
int [] arr=generateRandomArray(maxSize,maxValue);
//2 利用系统的Sort方法排序成标准的有序数组
Arrays.sort(arr);
//3 生成一个随机数,进行测试
int value=(int)((maxSize+1)*Math.random())-(int)(maxValue*Math.random());
if (test(arr,value)!=bigNearestIndex(arr,value)){
printArray(arr);
System.out.println("目标值: "+value);
System.out.println("暴力完的下标: "+test(arr,value));
System.out.println("二分法的下标: "+bigNearestIndex(arr,value));
succeed=false;
break;
}
}
System.out.println(succeed?"Nice!漂亮!":"Fucking fucked!继续努力!");
}
查找第一个小于等于给定值的元素
public static int lowerNearestIndex(int[] nums, int target){
int left = 0, right = nums.length - 1;
int index = -1;
while (left <= right){
int mid = left + ((right - left) >> 1);
if (nums[mid] <= target){ //如果mid小于等于target,则直接抛弃右边的,将right置为mid-1,并用index记录下mid
index = mid;
left = mid + 1;
}else
right = mid - 1;
}
return index;
}
二分找局部最小值(与Leecode 162寻找峰值类似)
题目:无序数组arr,一定保证任意位置i和i+1不相等
若[0]<[1],0位置算是局部最小
若[N-1]<[N-2],N-1位置算是局部最小
其他的,所[i-1]>[i]且[i]<[i+1],则i位置是局部最小值。
解析:例如arr=3 1 2 1 2 3 4 1 2
(1)首先看0 1位置,显然3>1,3不会是局部最小
(2)再看N-2 N-1位置,显然,1<2,2不会是局部最小
(3)在L=1,R=N-2中二分查找
mid=0+8/2=4,mid处的2并不满足[i-1]>[i]且[i]<[i+1],故mid不是局部最小
——此时,随意判断,因为我们刚刚(1)看到了有一个[0]>[1],故如果此时[mid-1]<[mid]的话,令R=mid-1,这样子的话L–之间必然有一个是局部最小,大不了就是1位置。
——同理,刚刚我们看到了(2)[N-2]<[N-1],故此时再有[mid]<[mid+1]的话,令L=mid+1,这样子的话,L–R之间也必然有一个是局部最小,大不了就是N-2位置。
(4)一旦找到一个位置是局部最小,break即可,不找了。返回mid;
//复习手撕代码:优化方法:二分法
public static int findLocalSmallest2Review(int[] arr){
if (arr == null || arr.length < 2) return -1;
int N = arr.length;
//看边界
if (arr[0] < arr[1]) return 0;
if (arr[N - 1] < arr[N - 2]) return N - 1;
int L = 1;//0和N-1不是不管了
int R = N - 2;
int index = -1;//结果,找不到就是-1
while (L <= R){
int mid = L + ((R - L) >> 1);
if (arr[mid - 1] < arr[mid]){
//必然mid不是局部最小值,条件都不满足,那左边也必然有一个最小值
R = mid - 1;
}else if (arr[mid] > arr[mid + 1]){
//也不然mid不是局部最小,右边必然,有一个是局部最小值
L = mid + 1;
}else {
//上面俩条件都不满足,必然是arr[mid - 1] > arr[mid]且arr[mid] < arr[mid + 1]
//这可不就是局部最小的定义吗
index = mid;
break;//找到了出去返回结果
}
}
return index;
}
认识O(nlogn)的排序
递归求一个无序数组上的最大值
下面是递归栈结构
public static int process(int[] arr, int L, int R){
if (L == R)
return arr[L];
int mid = L + ((R - L) >> 1);
int leftMax = process(arr, L, mid);
int rightMax = process(arr, mid + 1, R);
return Math.max(leftMax, rightMax);
}
Master公式求解时间复杂度
归并排序
规则:先将左半数组有序,然后再将右半数组有序,通过一个辅助数组,将两个子数组中较小的那个纸放入辅助数组中,指针右移,最后将还没比较完的子数组加入辅助数组中,再将辅助数组中的值置入原数组中
时间复杂度是O(nlogn),空间复杂度O(n)
public static void mergeSort(int[] arr, int L, int R){
if (L == R)
return;
int mid = L + ((R - L) >> 1);
mergeSort(arr, L, mid);
mergeSort(arr, mid + 1, R);
merge(arr, L, mid, R);
}
public static void merge(int[] arr, int L, int M, int R){
int[] help = new int[R - L + 1]; //辅助数组
int p1 = L, p2 = M + 1, i = 0; //遍历指针
while (p1 <= M && p2 <= R){
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= M)
help[i++] = arr[p1++];
while (p2 <= R)
help[i++] = arr[p2++];
for (int j = 0; j < help.length; j++){
arr[L + j] = help[j];
}
}
小和问题(用归并做)
思路转换:求左边小的数的和,可以反过来求右边有多少个数比当前值大,那么就应该产生右边大于的数量*当前数的小和,在归并merge的时候,可以判断左半数组的当前值是否小于有半数组的当前值,如果小于,那就得产生小和
代码如下:相较于归并排序,主要是增加了一个求右边有多少个数比当前值大(那么就会产生多少个当前值的小和)的操作。
public static int lowSum(int[] arr, int L, int R){
if (L == R)
return 0;
int mid = L + ((R - L) >> 1);
return lowSum(arr, L, mid) + lowSum(arr, mid + 1, R) + merge2(arr, L, mid, R);
}
public static int merge2(int[] arr, int L, int M, int R){
int ans = 0;
int[] help = new int[R - L + 1];
int p1 = L, p2 = M + 1, i = 0;
while (p1 <= M && p2 <= R){
ans += arr[p1] < arr[p2] ? arr[p1] * (R - p2 + 1) : 0;
help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
}
while (p1 <= M)
help[i++] = arr[p1++];
while (p2 <= R)
help[i++] = arr[p2++];
for (int j = 0; j < help.length; j++){
arr[L + j] = help[j];
}
return ans;
}
逆序对问题
可以使用归并排序来做,在merge 的过程中,我们从左半数组和右半数组的最右边开始遍历,指针分别为p1和p2,如果arr[p1] > arr[p2],对于arr[p1]来说,他会产生p2-m个逆序对
//逆序对
public static int reversePair(int[] arr, int l, int r){
if (l == r)
return 0;
int mid = l + ((r - l) >> 1);
return reversePair(arr, l, mid) + reversePair(arr, mid + 1, r) + mergeReverse(arr, l, mid, r);
}
public static int mergeReverse(int[] arr, int L, int m, int r){
int[] help = new int[r - L + 1];
int i = help.length - 1;
int p1 = m;
int p2 = r;
int res = 0;
while (p1 >= L && p2 > m) {
res += arr[p1] > arr[p2] ? (p2 - m) : 0;
help[i--] = arr[p1] > arr[p2] ? arr[p1--] : arr[p2--];
}
while (p1 >= L) {
help[i--] = arr[p1--];
}
while (p2 > m) {
help[i--] = arr[p2--];
}
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
return res;
}
荷兰国旗问题(有点类似快速排序)
思想:用一个指针lowIndex来指向小于区,一个指针bigIndex指向大于区,初始时lowIndex=-1,bigIndex=arr.length
然后我们从前往后遍历整个数组,直到遍历到了遍历指针与bigIndex重合
- 当arr[i]<target,我们就将arr[i]与lowIndex的下一个元素进行交换,i++;
- 当arr[i]>target,我们就将arr[i]与bigIndex的前一个元素进行交换,这时候i不能加加,因为我们并没有判断bigIndex前一个元素的大小
- 如果arr[i]=target,i++
代码如下:
//荷兰国旗问题
public static void lowEqualBig(int[] arr, int target){
int lowIndex = -1, bigIndex = arr.length;
int i = 0;
while (i != bigIndex){
if (arr[i] < target){ //如果当前值小于target,就扩充<区,并将arr[i]与小于区下一个元素进行交换,i++
swap(arr,lowIndex+1, i);
i++;
lowIndex++;
}else if(arr[i] > target){ //如果当前值大区target,扩充>区,并将arr[i]与大于区的前一个元素进行交换,
//这时候i不能++,因为大于区前一个元素并没有进行判断
swap(arr, bigIndex - 1, i);
bigIndex--;
}else { //如果当前值等于target,直接i++
i++;
}
}
}
public static void swap(int[] arr, int i, int j){
int t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
快排1.0(<=放左边,>放右边)
快排2.0 (利用荷兰国旗问题)时间复杂度O(n^2)
快排2.0比1.0稍快一些,因为他每次都一次搞定了等于某个值的所有元素(小于放左边,等于放中间,大于放右边)
//快排2.0
public static void quickSort(int[] arr, int l, int r){
if(l < r){
int[] p = partition(arr, l, r);
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[1] + 1,r);
}
}
public static int[] partition(int[] arr, int l, int r){
int lowIndex = l - 1;
int bigIndex = r;
while (l < bigIndex){
if (arr[l] < arr[r]){
swap(arr, ++lowIndex, l++);
}else if(arr[l] > arr[r]){
swap(arr, --bigIndex, l);
}else {
l++;
}
}
swap(arr, bigIndex, r);
return new int[]{lowIndex,bigIndex};
}
快排3.0
思想:在2.0的基础上进行改进,先从数组中随机取一个数,然后将他与数组最后一个数进行交换,然后再将最后一个数(现在是那个随机跳出来的数)作为比较值,其他操作与2.0一致
由于有一个随机选值的操作,最坏和最好的情况都是等概率出现的,因此快排3.0的时间复杂度为O(nlogn)
在parition之前有一个随机选值并与数组最后一个元素交换的操作
//快排3.0
public static void quickSort(int[] arr, int l, int r){
if(l < r){
swap(arr, l + (int)(Math.random() * (r - l + 1)), r);
int[] p = partition(arr, l, r);
quickSort(arr, l, p[0] - 1);
quickSort(arr, p[1] + 1,r);
}
}
堆
堆是一种完全二叉树(结点i的左子结点下标为2*i+1,右子结点为2*i+2,父结点(i-1)/2),分为大根堆和小根堆
大根堆
每一颗子树的头节点都是该子树的最大值
大根堆构造过程堆排序【带图演示】_画出构建大根堆的过程_十分之九加九分之一的博客-CSDN博客
大根堆排序算法时间复杂度为O(logn),空间复杂度为O(1)
/**
* 大根堆排序过程
* @param arr
*/
public static void heapSort(int[] arr){
//数组为空或者数组中只有一个元素,天然就是大根堆
if (arr == null || arr.length < 2)
return;
//依次将数组中的元素插入到堆中,得到一个原始的堆
for (int i = 0; i < arr.length; i++) { //O(n)
heapInsert(arr, i); //O(logn)
}
int heapsize = arr.length;
swap(arr, 0 , --heapsize); //将堆中的最后一个元素与根节点交换
while (heapsize > 0){
heapify(arr, 0, heapsize); //交换后需要向下调整堆,使其形成一个新的堆,O(logn)
swap(arr, 0, --heapsize); //调整完后接着交换最后一个元素与根节点
}
}
/**
* 堆插入的过程
* @param arr
* @param index
*/
public static void heapInsert(int[] arr, int index){
//判断index与其父节点大小
while (arr[index] > arr[(index - 1) / 2]){
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
/**
* 堆向下调整的过程
* @param arr 数组
* @param index 需要调整的下标
* @param heapsize 堆的大小
*/
public static void heapify(int[] arr, int index, int heapsize){
int left = index * 2 + 1; //找到index的左子结点
while (left < heapsize){ //如果存在孩子
//两个孩子结点,谁的值大,就将下标赋给largest
int largest = (left + 1) < heapsize && arr[left + 1] > arr[left] ? left + 1 : left;
//父结点和孩子结点,谁的值更大,就赋给largest
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 t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
优化:将插入堆的过程换为从数组末尾开始,依次对数组中的所有元素进行heapify向下调整,就可以得到一个原始堆,这个过程的时间复杂度为O(n),比依次插入堆的时间复杂度O(nlogn)优,但是堆排序的时间复杂度仍是O(nlogn)
for (int i = arr.length - 1; i > 0; i--){
heapify(arr, i, arr.length);
}
堆排序拓展题
已知一个几乎有序的数组,几乎有序是指,如果把数组排好序的话,每个元素的移动距离可以不超过k,并且k相较于数组长度来说比较小,请选择一个合适的排序算法针对这个数组进行排序。
思路:比如k为6,可以先将数组中前7个元素放入小根堆中,然后将根节点弹出,弹出的这个元素就是数组中最小的元素(ps:每个元素移动距离不超过k),然后将第8个元素放入小根堆,再弹出根节点,这个元素是数组中第2小的元素,依次加入一个元素,就弹出一个元素,最后剩余的元素再依次弹出。
public static void sortedArrDistanceLessK(int[] arr, int k){
//用优先队列,优先队列就是小根堆
PriorityQueue<Integer> heap = new PriorityQueue<>();
int index = 0;
//先将前k个元素放到小根堆中
for (; index < Math.min(arr.length, k); index++){
heap.add(arr[index]);
}
int i = 0;
//每添加一个元素,就将堆顶元素弹出,放到arr数组中
for (; index < arr.length; i++, index++){
heap.add(arr[index]);
arr[i] = heap.poll();
}
//将最后k个元素依次放入arr
while (!heap.isEmpty())
arr[i++] = heap.poll();
}
比较器
对于所有的比较器:
- 返回负数的时候,第一个参数排前面;
- 返回整数的时候,第二个参数排前面;
- 返回0的时候,顺序不变
比如按照Student类的id进行排序
public class ConpartorTest {
public static void main(String[] args) {
Student student1 = new Student(2, "A", 12);
Student student2 = new Student(3, "C", 18);
Student student3 = new Student(1, "D", 20);
Student[] students = new Student[]{student1, student2, student3};
Arrays.sort(students, new IdAscendingComparator());
for (Student student : students)
System.out.println(student);
}
}
class Student{
public int id;
public String name;
public int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
class IdAscendingComparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
//返回负数的时候,第一个参数排前面;
//返回整数的时候,第二个参数排前面;
//返回0的时候,顺序不变
return o1.id - o2.id;
}
}
PeriorityQueue实现大根堆
PeriorityQueue默认是小根堆,如果想要用PeriorityQueue实现大根堆,需要用比较器
过程如下:
public static void main(String[] args) {
PriorityQueue<Integer> heap = new PriorityQueue<>(new Comp());
heap.add(6);
heap.add(10);
heap.add(5);
heap.add(7);
while (!heap.isEmpty())
System.out.println(heap.poll());
}
class Comp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1;
}
}
不基于比较的排序(桶排序)
计数排序
准备一个int [max(arr) - min(arr)]大小的数组,统计arr数组中每个数值的个数,然后在恢复到arr中
缺点:对数据要求比较高,使用面窄,比如对员工年龄进行排序(年龄一般大于16岁,小于200岁,可以准备一个(200-16)大小的数组来统计年龄个数),时间复杂度是O(n),用空间换时间
基数排序
基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。
public class RadixSort {
/**
* 基数排序
* @param arr
*/
public static void radixSort(int[] arr){
if (arr == null || arr.length < 2)
return;
radixSort(arr, 0, arr.length - 1, maxBits(arr));
}
/**
* 统计数组中的最大值一共有多少位
* @param arr
* @return
*/
public static int maxBits(int[] arr){
int max = arr[0];
//得到数组中的最大值
for (int i = 1; i < arr.length; i++) {
max = arr[i] > max ? arr[i] : max;
}
int digit = 0; //统计最大值位数
while (max != 0){
digit++;
max /= 10;
}
return digit;
}
/**
* 基数排序算法
* @param arr 待排序数组
* @param l 从数组中那个位置开始排序
* @param r 到哪个位置结束
* @param digit 最大值有多少位
*/
public static void radixSort(int[] arr, int l, int r, int digit){
final int radix = 10;
int[] bucket = new int[r - l + 1]; //辅助数组
for (int d = 1; d <= digit; d++){ //有多少位就得进桶多少次
//一共10个空间
//count[0] 表示当前位(d位)是0的数字有多少个
//count[1] 表示当前位(d位)是1的数字有多少个
int[] count = new int[radix]; //用于统计当前位数的值的个数/词频
for (int i = l; i <= r; i++) { //统计词频
count[getDigit(arr[i], d)]++; //getDigit(arr[i], d)用于取出arr[i]第d位的数
}
//统计词频数组的前缀和,统计完后的arr[i]中的值表示数组中有arr[i]个数,第d位不小于i
for (int i = 1; i < radix; i++) {
count[i] += count[i - 1];
}
//从arr最右边开始遍历,根据其第d位的值来得到i,将其放入辅助数组bucket放入bucket[count[i]]
for (int i = r; i >= l ; i--) {
int j = getDigit(arr[i], d);
bucket[count[j] - 1] = arr[i];
count[j]--;
}
//将辅助数组中的数让如arr
for (int i = l, j = 0; i <= r; i++, j++) {
arr[i] = bucket[j];
}
}
}
/**
* 取出num第d位的值
* @param num
* @param d
* @return
*/
public static int getDigit(int num, int d){
return (num / ((int)(Math.pow(10, d - 1))) % 10);
}
public static void main(String[] args) {
int[] arr = {12,33,5,78,62,59};
radixSort(arr);
for (int ar: arr)
System.out.print(ar + " ");
}
}
排序算法总结
稳定性:如果两个相同的值a和b,刚开始他们在数组中的位置a在b前面,排完序之后a与b的相对顺序不变,即a还在b前面,那么说这个排序算法是稳定的。