文章目录
01 | 补上节知识点
1. 排序算法的稳定性及其汇总
-
稳定性:排序前后,值相等的元素相对次序不改变
-
意义:业务需要。实际应用场景中很多情况下需要保留原始次序;例如,对一张考试成绩表做排序,该表包含姓名、班级、成绩三列,先对成绩字段做一次排序,即将全年级成绩从高到低排完序,之后要将所有人的成绩再按班级归类,即要再对班级字段进行排序,此时不希望排好后,班内的成绩次序又被打乱,所以需要排序算法具有稳定性。
-
总结:
-
冒泡(相等时不交换)、插入(相等时不交换)、归并(相等时先拷贝左边数组的值)可以做到稳定性
-
选择、堆排序无法做到稳定性
-
快排可以做到稳定但困难(“01 stable sort”),所以一般认为不稳定
-
tips:有一道题目,是奇数放在数组左边,偶数放在数组右边,还要求原始的相对次序不变,这个问题就是要求快排做到稳定性,很难,不要求掌握。
-
2. 介绍工程中的排序算法
-
工程中的综合排序算法
- 样本量很小(小于60):直接用插排(插入排序的时间复杂度为O(N²)的劣势体现不出来,反而插入排序常数项很低,导致在小样本情况下,插入排序极快。)
- 数组中是基础类型数据(如:int,long,short等):快排(因为基础类型都相等不用区分原始顺序,稳定性的原因)
- 数组中是自己定义的数据类型 : 归并(因为需要做到稳定性)
3. 比较器
- 流程:当数据需要按照你的想法进行排序时可重写Comparator方法下的compare方法,该方法传入两个参数,如student1,与student2,当return一个负数时,表示1<2, 1排在前面; 当return一个正数时,表示1>2,2排在前面; 当return 0时,表示1==2。
public static class IdAscendingComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.id - o2.id; } }
- 应用广泛:优先级队列(堆)、treemap(红黑树)都能用比较器。
//堆,id小的放在头部 PriorityQueue<Student> heap = new PriorityQueue<>(new IdAscendingComparator()) heap.add(student3); heap.add(student2); heap.add(student1); while(!heap.isEmpty()){ Student student = heap.poll(); System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age); } ===================================================== //红黑树 TreeMap<Student> treeMap = new TreeMap<>(new IdAscendingComparator()); .......
- 举个栗子:
public class Code_09_Comparator { public static class Student { public String name; public int id; public int age; public Student(String name, int id, int age) { this.name = name; this.id = id; this.age = age; } } public static class IdAscendingComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.id - o2.id; } } public static class IdDescendingComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o2.id - o1.id; } } public static class AgeAscendingComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.age - o2.age; } } public static class AgeDescendingComparator implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o2.age - o1.age; } } public static void printStudents(Student[] students) { for (Student student : students) { System.out.println("Name : " + student.name + ", Id : " + student.id + ", Age : " + student.age); } System.out.println("==========================="); } public static void main(String[] args) { Student student1 = new Student("A", 1, 23); Student student2 = new Student("B", 2, 21); Student student3 = new Student("C", 3, 22); Student[] students = new Student[] { student3, student2, student1 }; printStudents(students); Arrays.sort(students, new IdAscendingComparator()); printStudents(students); Arrays.sort(students, new IdDescendingComparator()); printStudents(students); Arrays.sort(students, new AgeAscendingComparator()); printStudents(students); Arrays.sort(students, new AgeDescendingComparator()); printStudents(students); } }
4. 桶排序、计数排序、基数排序
-
介绍:桶排序(Bucket sort)、计数排序(Counting sort)和基数排序(Radix sort)
- 非基于比较的排序,与被排序的样本的实际数据状况很有关系,所 以实际中并不经常使用
- 时间复杂度O(N),额外空间复杂度O(N)
- 稳定的排序
-
桶排序
- 桶相当于安装数据状况设计的容器,然后把元素依次放进属于他的桶中
- 计数排序是桶排序的一个具体体现
- 基数排序用的桶比较少
简单介绍:
- 计数排序:
- 基数排序
5. 补充问题(年年考)
-
给定一个数组,求如果排序之后,相邻两数的最大差值,要求时间复杂度O(N)且要求不能用非基于比较的排序
-
思路:
- 若是数组有N个数,则准备N+1个桶,然后找到数组中的最小值min与最大值max分别放入0号桶和N号桶,将min与max中间的值等分(N-1)份作为中间(N-1)个桶的范围;
- 由于一共N个数,有N+1和桶,故可以断定相邻数最大差值必定不在同一个桶内;
- 每个桶记录桶里的最大值与最小值,并给一个标志位flag来标记桶内是否有数值;
- 则可推断,相邻数最大差值必定为所有非空桶的最小值与前一非空桶的最大值的差值中的最大值。
-
举个栗子:
- 假设原数组有9个元素,那么则设置9+1个桶,每个桶内装指定范围内的数。试想,因为只有9个数,但却有10个桶,所以必定有一个桶是空桶。
- 有空桶则说明这个序列的最大差值大于桶的跨度!
- 因此,最大差值不可能出现在同一个桶内,只可能出现在后一个桶的min-前一个桶的max
注:不一定隔着空桶的两个桶差值最大。
-
代码实现最大差值:
public class Code_11_MaxGap {
public static int maxGap(int[] nums) {
if (nums == null || nums.length < 2) {
return 0;
}
int len = nums.length;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int i = 0; i < len; i++) {
min = Math.min(min, nums[i]);
max = Math.max(max, nums[i]);
}
if (min == max) {
return 0;
}
boolean[] hasNum = new boolean[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);
//该桶的最小值与最大值发生更新,只有当该桶有数字时,才为true,将当前述与min、max进行对比,当该桶没有数字是,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];
int i = 1;
for (; i <= len; i++) {
if (hasNum[i]) {
res = Math.max(res, mins[i] - lastMax);
lastMax = maxs[i];
}
}
return res;
}
//确定当前数来自几号桶
public static int bucket(long num, long len, long min, long max) {
return (int) ((num - min) * len / (max - min));
}
// for test 对数器
public static int comparator(int[] nums) {
if (nums == null || nums.length < 2) {
return 0;
}
Arrays.sort(nums);
int gap = Integer.MIN_VALUE;
for (int i = 1; i < nums.length; i++) {
gap = Math.max(nums[i] - nums[i - 1], gap);
}
return gap;
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static int[] copyArray(int[] arr) {
if (arr == null) {
return null;
}
int[] res = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
res[i] = arr[i];
}
return res;
}
// for test
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++) {
int[] arr1 = generateRandomArray(maxSize, maxValue);
int[] arr2 = copyArray(arr1);
if (maxGap(arr1) != comparator(arr2)) {
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
}
}
02 | 第三节课
1. 用数组结构实现大小固定的栈(难度:大厂一面)
- 解题流程:
- 设置一个指针size表示数组的大小
- 当size大于等于数组长度或者小于0时则报出异常,size初始化为0
- 进栈操作时,先将数据填入size位置,在将size+1,指向下一个数据进入的位置
- 出栈操作时,先将size-1,指向栈顶位置的数据再将栈顶位置数据返回,至于栈顶位置数据是否需要抹除则无所谓,因为下一个数据进栈的时候会将其覆盖。
- 代码实现
public class Code_01_Array_To_Stack { public static class ArrayStack { private Integer[] arr; private Integer size; public ArrayStack(int initSize) { if (initSize < 0) { throw new IllegalArgumentException("The init size is less than 0"); } arr = new Integer[initSize]; size = 0; } public Integer peek() { if (size == 0) { return null; } return arr[size - 1]; } public void push(int obj) { if (size == arr.length) { throw new ArrayIndexOutOfBoundsException("The queue is full"); } arr[size++] = obj; } public Integer pop() { if (size == 0) { throw new ArrayIndexOutOfBoundsException("The queue is empty"); } return arr[--size]; } } }
2. 用数组结构实现大小固定的队列(难度:大厂一面)
- 解题流程:
- 代码实现:
public class Code_01_Array_To_Queue {
public static class ArrayQueue {
private Integer[] arr;
private Integer size;
private Integer first;
private Integer last;
public ArrayQueue(int initSize) {
if (initSize < 0) {
throw new IllegalArgumentException("The init size is less than 0");
}
arr = new Integer[initSize];
size = 0;
first = 0;
last = 0;
}
public Integer peek() {
if (size == 0) {
return null;
}
return arr[first];
}
public void push(int obj) {
if (size == arr.length) {
throw new ArrayIndexOutOfBoundsException("The queue is full");
}
size++;
arr[last] = obj;
last = last == arr.length - 1 ? 0 : last + 1;
}
public Integer poll() {
if (size == 0) {
throw new ArrayIndexOutOfBoundsException("The queue is empty");
}
size--;
int tmp = first;
first = first == arr.length - 1 ? 0 : first + 1;
return arr[tmp];
}
}
public static void main(String[] args) {
.........
}
}
3. 实现返回栈中最小元素的操作(难度:大厂一面)
- 实现一个特殊的栈,在实现栈的基本功能的基础上,再实现返 回栈中最小元素的操作。
【要求】:
1.pop、push、getMin操作的时间复杂度都是O(1)。
2.设计的栈类型可以使用现成的栈结构 - 解题思路:
- 代码实现:
public static class MyStack2 { private Stack<Integer> stackData; private Stack<Integer> stackMin; public MyStack2() { this.stackData = new Stack<Integer>(); this.stackMin = new Stack<Integer>(); } public void push(int newNum) { if (this.stackMin.isEmpty()) { this.stackMin.push(newNum); } else if (newNum < this.getmin()) { this.stackMin.push(newNum); } else { int newMin = this.stackMin.peek(); this.stackMin.push(newMin); } this.stackData.push(newNum); } public int pop() { if (this.stackData.isEmpty()) { throw new RuntimeException("Your stack is empty."); } this.stackMin.pop(); return this.stackData.pop(); } public int getmin() { if (this.stackMin.isEmpty()) { throw new RuntimeException("Your stack is empty."); } return this.stackMin.peek(); } }
4. 如何仅用队列结构实现栈?(难度:大厂一面)
-
tip:
- 如何仅用队列结构实现图的深度遍历?
- 图的深度遍历需要用栈结构来实现,因此,我们可以用两个队列结构来实现栈结构,再用形成的栈结构来实现图的深度遍历。
- Queue<> queue1 = new LinkedList<>();
为啥是new的LinkedList呢?
因为LinkedList实现了List, Deque, Cloneable, Serializable这四个接口
其中,(双端队列)Deque继承了Queue
因此可以把LinkedList当成Queue来用。
另外,Queue是接口,抽象的,不能被实例化。
-
解题流程:
-
代码实现:
public static class TwoQueuesStack { private Queue<Integer> queue; private Queue<Integer> help; public TwoQueuesStack() { queue = new LinkedList<Integer>(); help = new LinkedList<Integer>(); } public void push(int pushInt) { queue.add(pushInt); } public int peek() { if (queue.isEmpty()) { throw new RuntimeException("Stack is empty!"); } while (queue.size() != 1) { help.add(queue.poll()); } int res = queue.poll(); help.add(res); swap(); return res; } public int pop() { if (queue.isEmpty()) { throw new RuntimeException("Stack is empty!"); } //只要栈中大于一个数就将栈中的数据放到help栈中 while (queue.size() > 1) { help.add(queue.poll()); } int res = queue.poll(); //queue栈变help栈,help栈变queue栈 swap(); return res; } private void swap() { Queue<Integer> tmp = help; help = queue; queue = tmp; } }
5. 如何仅用栈结构实现队列?(难度:大厂一面)
-
解题思路:
-
从push栈将数据倒如pop栈的原则:
- 要一次将数据全部倒完
- 如果pop栈中有数据,push栈则不可倒入数据。
-
代码实现:
public static class TwoStacksQueue { private Stack<Integer> stackPush; private Stack<Integer> stackPop; public TwoStacksQueue() { stackPush = new Stack<Integer>(); stackPop = new Stack<Integer>(); } public void push(int pushInt) { stackPush.push(pushInt); } public int poll() { if (stackPop.empty() && stackPush.empty()) { throw new RuntimeException("Queue is empty!"); } else if (stackPop.empty()) { while (!stackPush.empty()) { stackPop.push(stackPush.pop()); } } return stackPop.pop(); } public int peek() { if (stackPop.empty() && stackPush.empty()) { throw new RuntimeException("Queue is empty!"); } else if (stackPop.empty()) { while (!stackPush.empty()) { stackPop.push(stackPush.pop()); } } return stackPop.peek(); } }