本章是java路径课程基础,数据结构与算法系列课程。
给定一个乱序数组,如何使其变得有序?
public class Main {
public static void main(String[] args) {
var nums = new int[] { 5, 1, 3, 2, 4, 6 };
for (var i = 0; i < nums.length; i++)
for (var j = 0; j < nums.length - i - 1; j++)
if (nums[j] > nums[j + 1])
nums[j] = nums[j + 1] ^ nums[j] ^ (nums[j + 1] = nums[j]);
System.out.println(java.util.Arrays.toString(nums)); // [1, 2, 3, 4, 5, 6]
}
}
通过不断交换相邻元素的方式,就可以将无序的线性表有序化,是不是很神奇。数据结构与算法是程序的灵魂,本课程次从这里出发,之后则是设计模式提升,最后就是企业化开发可以包括桌面应用开发和网页服务开发。
课程的目的是要求大家掌握数据结构的特性和常用的算法,并不涉及工程化的实践,工程化的实践将在第二章设计模式中体现,所以这门课并不会使用工程化的代码。
不知道大家发现没有,排序设计到的知识点很多,很多书或者课程喜欢把排序放到第一章来讲,其实有点过早。这个课程会把算法分散在数据结构的过程中去将,比如插入选择排序会在串中讲(线性表),堆排会在树中说明,而快排和归并会在分治算法中讲。
串就是线性表,最常见的结构就是字符串。串数据结构很简单,总体上可以分为数组结构和链表结构。
数组长度是固定的,如果不知道长度使用起来十分不方便,如何实现一个动态数组呢?
class Array {
int[] data;
int capacity;
int len;
public Array(int capacity) {
this.capacity = capacity;
this.data = new int[capacity];
}
public int get(int idx) {
if (java.util.Objects.checkIndex(idx, len) >= 0)
return data[idx];
throw new java.lang.IndexOutOfBoundsException();
}
public void set(int idx, int val) {
if (len <= capacity && java.util.Objects.checkIndex(idx, len) >= 0)
data[idx] = val;
if (idx >= 0 && idx < len)
throw new java.lang.IndexOutOfBoundsException();
}
public void add(int val) {
if (len < capacity) {
data[len] = val;
len += 1;
return;
}
capacity += capacity >> 1;
data = java.util.Arrays.copyOf(data, capacity);
add(val);
}
}
数组串的处理方式多种多样。首先最简单也是必会的就是最值。
public class Main {
public static void main(String[] args) {
var nums = new int[] { 5, 1, 3, 2, 4, 6 };
// code herr
var minidx = 0;
for (var j = 0; j < nums.length; j++)
if (nums[j] < nums[minidx])
minidx = j;
System.out.println(nums[minidx]); // 1
}
}
这段处理其实比较简单,遍历一遍数组,如果遍历元素比最小元素还小,此时将最小下标更新。
这里要注意的就是我们使用的是串的下标,而不是一个广义上的极大值Integer.MAX_VALUE
,没有经验的同学可能会使用这种方式来定义,实际上使用下标是更明智的选择。
紧接着我们可以加几行代码实现选择排序。
public class Main {
public static void main(String[] args) {
var nums = new int[] { 5, 1, 3, 2, 4, 6 };
// code here
for (int i = 0, minidx = 0; i < nums.length; i += 1, minidx = i) {
for (var j = i; j < nums.length; j += 1)
if (nums[j] < nums[minidx])
minidx = j;
nums[i] = nums[minidx] ^ nums[i] ^ (nums[minidx] = nums[i]);
}
System.out.println(java.util.Arrays.toString(nums)); // [1, 2, 3, 4, 5, 6]
}
}
这个算法即使从i到len(不做特殊说明,之后统一定义都是左闭又开)找到最小值和i号元素交换使其前i号有序。
讲完了处理ii到len的方式,来试一下前序处理。典型的例子就是插入排序。
public class Main {
public static void main(String... args) {
var nums = new int[] { 5, 1, 3, 2, 4, 6 };
// code here
for (int i = 0; i < nums.length; i += 1) {
int j = i - 1, t = nums[j + 1];
for (; j >= 0 && t < nums[j]; j -= 1)
nums[j + 1] = nums[j];
nums[j + 1] = t;
}
System.out.println(java.util.Arrays.toString(nums));
}
}
经常会有同学写插入排序的时候控制不好边界。处理的时候有时会写错,最后这里的nums[j + 1]
就经常会被写成nums[j]
。这种问题首先是你的习惯不好,在你上面定义的时候可能已经定义成了nums[i]
,这样就导致一个问题前后不对应。所以这里的一个处理的方式就是先定义j,之后所有的处理都和i没关系了。这样顺眼多了,也不容易写错。
插入的本质是后移,那么极端的情况下,[2,3,4,5,1]
最后一个没拍好,后移了整个数组,导致时间复杂度指向极端。实际上可以做如下优化。
public class Main {
public static void main(String... args) {
var nums = new int[] { 5, 1, 3, 2, 4, 6 };
// code herr
for (var step = nums.length / 2; step > 0; step /= 2)
for (var offset = 0; offset < step; offset += 1)
sort(nums, offset, step);
System.out.println(java.util.Arrays.toString(nums));
}
public static void sort(int[] nums, int offset, int step) {
for (int i = offset; i < nums.length; i += step) {
int j = i - step, t = nums[j + step];
for (; j >= offset && t < nums[j]; j -= step)
nums[j + step] = nums[j];
nums[j + step] = t;
}
}
}
把插入排序部分查出来,全部的0替换成offset,全部的1替换成step,这样就可以提前将大块的排序元素抽走。防止每次后移过多导致时间复杂度过高。
有同学会说,这使用了四层循环,怎么可能会更快呢?为了证明有效性,我们写了如下的验证代码。
public class Main {
public static void main(String... args) {
var nums = new int[] { 5, 1, 3, 2, 4, 6 };
// code herr
for (var step = nums.length / 2; step > 0; step /= 2)
for (var offset = 0; offset < step; offset += 1)
sort(nums, offset, step);
nums = new int[] { 5, 1, 3, 2, 4, 6 };
sort(nums, 0, 1);
}
public static void sort(int[] nums, int offset, int step) {
var cnt = 0;
for (int i = offset; i < nums.length; i += step) {
int j = i - step, t = nums[j + step];
for (; j >= offset && t < nums[j]; j -= step) {
nums[j + step] = nums[j];
cnt++;
}
nums[j + step] = t;
}
System.out.println(cnt);
}
}
这里会将后移的次数记录下来,比较下使用前后的后移次数。前者后移了1+0+0+2
共3次,而普通的插入排序后移了5
次。所以减少后移次数,算法会更快。
除了数组正序处理,逆序处理,还有一种创建的处理方法块处理,步长是内层循环决定的。此时有两个数组的两个部分{ 1, 3, 5 }
和{ 2, 4, 6 }
有序,如何将整个数组使其有序呢,不使用辅助空间。
public class Main {
public static void main(String... args) {
var nums = new int[] { 1, 3, 5, 2, 4, 6 };
// code here
merge(nums, 0, nums.length - 1);
System.out.println(java.util.Arrays.toString(nums)); // [1, 2, 3, 4, 5, 6]
}
public static void merge(int[] nums, int left, int right) {
var mid = (left + right) / 2;
int i = left, j = mid + 1;
for (; i < j && j <= right;) {
for (; i < j && j <= right && nums[i] < nums[j]; i++)
continue;
var idx = j;
for (; i < j && j <= right && nums[j] < nums[i]; j++)
continue;
for (int p = i, q = idx - 1; p < q; p += 1, q -= 1)
nums[p] = nums[q] ^ nums[p] ^ (nums[q] = nums[p]);
for (int p = idx, q = j - 1; p < q; p += 1, q -= 1)
nums[p] = nums[q] ^ nums[p] ^ (nums[q] = nums[p]);
for (int p = i, q = j - 1; p < q; p += 1, q -= 1)
nums[p] = nums[q] ^ nums[p] ^ (nums[q] = nums[p]);
i += j - 1 - idx + 1;
}
}
}
从范式上讲,链表存在两种表达。第一种表达方式是duumy表达(realnull)强调返回值,第二种是replace表达(fakenull)。
以链表插入为例子,duumy派主张找到单链表前驱节点并返回的做法。
public class Main {
public static void main(String... args) {
// var node = (java.util.Map) new java.util.HashMap
// Object>();
// var val = (Integer) node.get("val");
// var next = (java.util.Map) node.get("next");
// code here
var head = insert(null, Integer.MIN_VALUE, 0);
head = insert(head, Integer.MAX_VALUE, 2);
head = insert(head, 1, 1);
System.out.println(head); // {val=0, next={val=1, next={val=2, next=null}}}
}
public static java.util.Map<String, Object> insert(java.util.Map<String, Object> head, int idx, int val) {
var dummy = (java.util.Map<String, Object>) new java.util.HashMap<String, Object>();
dummy.put("next", head);
var node = dummy;
for (int i = 0; i < idx && java.util.Objects.nonNull(node.get("next")); i += 1)
node = (java.util.Map<String, Object>) node.get("next");
var n = (java.util.Map<String, Object>) new java.util.HashMap<String, Object>();
n.put("val", val);
n.put("next", node.get("next"));
node.put("next", n);
return (java.util.Map<String, Object>) dummy.get("next");
}
}
replace派更关注当下。
public class Main {
public static void main(String... args) {
// var node = (java.util.Map) new java.util.HashMap
// Object>();
// var val = (Integer) node.get("val");
// var next = (java.util.Map) node.get("next");
// code here
java.util.Map<String, Object> head = new java.util.HashMap<>();
head.put("next", new java.util.HashMap<>());
insert(head, Integer.MIN_VALUE, 0);
insert(head, Integer.MAX_VALUE, 2);
insert(head, 1, 1);
System.out.println(head); // {val=0, next={val=1, next={val=2, next={next={}}}}}
}
public static void insert(java.util.Map<String, Object> head, int idx, int val) {
var node = (java.util.HashMap<String, Object>) head;
for (int i = 0; i < idx && node.size() > 1; i += 1)
node = (java.util.HashMap<String, Object>) node.get("next");
var next = node.clone();
var n = (java.util.Map<String, Object>) new java.util.HashMap<String, Object>();
n.put("val", val);
n.put("next", next);
node.clear();
node.putAll(n);
}
}
这种取巧的思路来自于无法前驱节点时的删除思路,写法比较优雅,但是判空会很难受,但是如果是python就会爽很多。
目前主流算法都是dummy实现,replace限制比较多,而且必须提供clone或者copy方法才能使用。给出dummy派的插入删除和遍历。插入和删除时,idx仅仅做辅助定位,链表遍历其实不应该关注idx,而是关注node和node的next的关系。
class ListNode {
int val;
ListNode next = null;
static ListNode insertTail(ListNode head, int val) {
return insert(head, Integer.MAX_VALUE, val);
}
static ListNode insertHead(ListNode head, int val) {
return insert(head, Integer.MIN_VALUE, val);
}
static ListNode insert(ListNode head, int idx, int val) {
ListNode dummy = new ListNode(0, head), node = dummy;
for (; idx > 0 && node.next != null; node = node.next, idx -= 1)
continue;
var newnode = new ListNode(val, node.next);
node.next = newnode;
return dummy.next;
}
static ListNode removeTail(ListNode head) {
return remove(head, Integer.MAX_VALUE);
}
static ListNode removeHead(ListNode head) {
return remove(head, Integer.MIN_VALUE);
}
static ListNode remove(ListNode head, int idx) {
ListNode dummy = new ListNode(0, head), node = dummy;
for (; idx > 0 && node.next != null && node.next.next != null; node = node.next, idx -= 1)
continue;
if (node.next != null)
node.next = node.next.next;
return dummy.next;
}
static void traverse(ListNode head) {
for (var node = head; node != null; node = node.next)
System.out.print(node.val + "->");
System.out.println("null");
}
ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
改了下写法过了leetcode,应该没啥问题。dummy写法过leetcode
链表遍历判断链表是否存在环。
public class Main {
public static void main(String... args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
// code here
var cycle = false;
for (ListNode i = head, j = head; j != null && j.next != null;) {
i = i.next;
j = j.next.next;
if (i == j) {
cycle = true;
break;
}
}
System.out.println(cycle);
}
}
这是一个非常简单的代码但是要注意这里的循环只是再遍历,在发现无法遍历到结尾时退出。而一些解法在遍历时检查,就要注意初始化要初始化j
为head.next
。
链表反转。
public class Main {
public static void main(String... args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
// code here
ListNode prev = null, node = head, post = head.next;
for (; node != null; prev = node, node = post) {
post = node.next;
node.next = prev;
}
ListNode.traverse(prev);
}
}
链表合并。
public class Main {
public static void main(String... args) {
ListNode head1 = new ListNode(1);
head1.next = new ListNode(3);
head1.next.next = new ListNode(4);
ListNode.traverse(head1); // 1->3->4->null
ListNode head2 = new ListNode(2);
head2.next = new ListNode(6);
head2.next.next = new ListNode(7);
ListNode.traverse(head2); // 2->6->7->null
ListNode dummy = new ListNode(0, null), n = dummy;
ListNode n1 = head1, n2 = head2;
for (; n1 != null && n2 != null; n = n.next) {
if (n1.val > n2.val) {
n.next = n2;
n2 = n2.next;
} else {
n.next = n1;
n1 = n1.next;
}
}
for (; n1 != null; n1 = n1.next, n = n.next)
n.next = n1;
for (; n2 != null; n2 = n2.next, n = n.next)
n.next = n2;
ListNode.traverse(dummy.next); // 1->2->3->4->6->7->null
}
}
线性结构串到这里接近尾声了,相信大家已经掌握了串的基本操作。串是一切问题的基础,一切问题也围绕这串去服务,所以对于基础的串操作要掌握,接下来是栈。
package algo;
import java.util.Arrays;
import java.util.Stack;
public class Temperature {
public static void main(String[] args) {
Temperature.temperature(new int[] { 63, 54, 76, 56, 37, 89, 23, 74 });
}
public static void temperature(int[] tempera) {
var destack = new Stack<Integer>();
var distance = new int[tempera.length];
for (var i = 0; i < tempera.length; i++) { // 一轮i从0到len对每个不破坏栈内单调性的元素
while (!destack.isEmpty() && tempera[destack.peek()] < tempera[i]) { // 栈内单调性被破坏
int pop = destack.pop();
distance[pop] = i - pop;
}
destack.push(i);
}
System.out.println(Arrays.toString(distance));
}
}
package algo;
import java.util.Deque;
import java.util.LinkedList;
import struct.TreeNode;
public class TreeOrder {
public static void main(String[] args) {
TreeOrder.layer(TreeNode.random());
}
public static void layer(TreeNode root) {
var queue = (Deque<TreeNode>) new LinkedList<TreeNode>();
queue.add(withdepth(root, 0)); // 队列
var depth = 0;
while (!queue.isEmpty()) { // 直到队列为空队列中的元素
var r = queue.poll();
if (r != null) {
if (depth != (int) r.ext.get("depth")) {
depth = (int) r.ext.get("depth");
System.out.println();
}
System.out.print(r.val);
queue.add(withdepth(r.left, (int) r.ext.get("depth") + 1));
queue.add(withdepth(r.right, (int) r.ext.get("depth") + 1));
}
}
System.out.println();
}
public static TreeNode withdepth(TreeNode root, int depth) {
if (root != null) {
root.ext.put("depth", depth);
}
return root;
}
}
还记的在数组块处理中的不借助辅助空间排序的例子么?归并这里就是用到这个函数,一起来看下。归并按照中间划分,划分到不能再划分就开始合并。合并函数我们已经实现,那现在归并就很简单了。
public class Main {
public static void main(String[] args) {
var nums = new int[] { 5, 1, 3, 2, 4, 6 };
// code here
sort(nums, 0, nums.length - 1);
System.out.println(java.util.Arrays.toString(nums)); // [1, 2, 3, 4, 5, 6]
}
public static void sort(int[] nums, int left, int right) {
if (left >= right)
return;
sort(nums, left, (left + right) / 2);
sort(nums, (left + right) / 2 + 1, right);
merge(nums, left, right);
}
public static void merge(int[] nums, int left, int right) { // 数组中已经实现
var mid = (left + right) / 2;
int i = left, j = mid + 1;
for (; i < j && j <= right;) {
for (; i < j && j <= right && nums[i] < nums[j]; i++)
continue;
var idx = j;
for (; i < j && j <= right && nums[j] < nums[i]; j++)
continue;
for (int p = i, q = idx - 1; p < q; p += 1, q -= 1)
nums[p] = nums[q] ^ nums[p] ^ (nums[q] = nums[p]);
for (int p = idx, q = j - 1; p < q; p += 1, q -= 1)
nums[p] = nums[q] ^ nums[p] ^ (nums[q] = nums[p]);
for (int p = i, q = j - 1; p < q; p += 1, q -= 1)
nums[p] = nums[q] ^ nums[p] ^ (nums[q] = nums[p]);
i += j - 1 - idx + 1;
}
}
}
快速排序,主要还是串的处理手法,将串分解成两个部分,使其左边小于某个数,右边大于这个数。
public class Main {
public static void main(String[] args) {
var nums = new int[] { 5, 1, 3, 2, 4, 6 };
// code here
sort(nums, 0, nums.length - 1);
System.out.println(java.util.Arrays.toString(nums)); // [1, 2, 3, 4, 5, 6]
}
public static void sort(int[] nums, int left, int right) {
if (left >= right)
return;
int i = left, j = right;
var base = nums[i];
for (; i < j;) {
for (; i < j && nums[j] >= base; j -= 1)
continue;
nums[i] = nums[j];
for (; i < j && nums[i] <= base; i += 1)
continue;
nums[j] = nums[i];
}
nums[i] = base;
sort(nums, left, i);
sort(nums, i + 1, right);
}
}
写起来也是十分清爽干净,函数参数是数组左右边界,相似的常见处理手法还有偏移加限制,像是之后的堆排序就用到这种处理手法。接下来看下堆排。
阶乘。
public class Main {
public static void main(String[] args) {
Factorial.get(4);
}
}
class Factorial {
static void get(int data) {
System.out.println(calc(data));
}
static int calc(int data) {
if (data == 0)
return 1;
return calc(data - 1) * data;
}
}
全排列。
public class Main {
public static void main(String[] args) {
Permutation.generate(new int[] { 1, 2, 3 });
}
}
class Permutation {
static void generate(int[] nums) {
var gens = new java.util.ArrayList<int[]>();
dfs(nums, new boolean[nums.length], 0, new int[nums.length], gens);
gens.stream().forEach(e -> System.out.println(java.util.Arrays.toString(e)));
}
static void dfs(int[] nums, boolean[] table, int idx, int[] gen, java.util.List<int[]> gens) {
if (idx == nums.length) {
gens.add(java.util.Arrays.copyOf(gen, nums.length));
return;
}
for (var i = 0; i < nums.length; i++) {
if (table[i])
continue;
table[i] = true;
gen[idx] = nums[i];
dfs(nums, table, idx + 1, gen, gens);
table[i] = false;
}
}
}
选择排序,堆排为选择的优化也会在这里介绍。
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
Select.sort(new int[] { 5, 1, 3, 4, 2, 6 });
Select.heap(new int[] { 5, 1, 3, 4, 2, 6 });
}
}
class Select {
public static void sort(int[] nums) {
for (var i = 0; i < nums.length; i++) {
var min = i;
for (var j = i; j < nums.length; j++) {
if (nums[min] > nums[j]) {
min = j;
}
}
nums[i] = nums[min] ^ nums[i] ^ (nums[min] = nums[i]);
}
System.out.println(Arrays.toString(nums));
}
public static void heap(int[] nums) {
for (var root = nums.length / 2 - 1; root >= 0; root--)
heapfy(nums, root, nums.length);
for (var root = nums.length - 1; root >= 0; root--) {
nums[0] = nums[root] ^ nums[0] ^ (nums[root] = nums[0]);
heapfy(nums, 0, root);
}
System.out.println(Arrays.toString(nums));
}
public static void heapfy(int[] nums, int root, int len) {
var child = 2 * root + 1;
if (child < len) {
if (child + 1 < len && nums[child + 1] > nums[child])
child += 1;
if (nums[child] > nums[root]) {
nums[child] = nums[root] ^ nums[child] ^ (nums[root] = nums[child]);
heapfy(nums, child, len);
}
}
}
}
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
DateDiff.format(new int[] { 20230504, 20000101 });
}
}
class DateDiff {
static int[][] m = new int[2][];
static {
m[0] = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
m[1] = Arrays.copyOf(m[0], m[0].length);
m[1][1] += 1;
}
static int[] y = new int[2];
static {
y[0] = 365;
y[1] = 366;
}
public static void format(int[] date) {
assert date.length == 2;
// static export
int d0 = date[0] % 100, m0 = date[0] / 100 % 100, y0 = date[0] / 10000 % 10000;
int d1 = date[1] % 100, m1 = date[1] / 100 % 100, y1 = date[1] / 10000 % 10000;
int sum0 = d0;
for (var yy = 1; yy < y0; yy += 1)
sum0 += y[isleap(yy)];
for (var mm = 1; mm < m0; mm += 1)
sum0 += m[isleap(y0)][mm - 1];
int sum1 = d1;
for (var yy = 1; yy < y1; yy += 1)
sum1 += y[isleap(yy)];
for (var mm = 1; mm < m1; mm += 1)
sum1 += m[isleap(y1)][mm - 1];
System.out.println(Math.abs(sum0 - sum1));
}
public static int isleap(int yy) {
return ((yy % 4 == 0) && (yy % 100 != 0)) || yy % 400 == 0 ? 1 : 0;
}
}
public class Main {
public static void main(String[] args) {
HexConvert.fromN(123, 8);
HexConvert.toN(83, 8);
}
}
class HexConvert {
public static void fromN(int data, int nn) { // nn转10
assert 0 < nn && nn <= 10;
int ndata = 0;
for (var i = 0; data != 0; data /= 10, i++) {
ndata += (data % 10) * (int) Math.pow(nn, i);
}
System.out.println(ndata);
}
public static void toN(int data, int nn) { // 10转nn
assert 0 < nn && nn <= 10;
int ndata = 0;
for (var i = 0; data != 0; data /= nn, i++) {
ndata += (data % nn) * (int) Math.pow(10, i);
}
System.out.println(ndata);
}
}
用到pow还是不够优雅,优化为这个版本,使用了java中的lambda。
不知道大家注意到没有lambda实际上不能写递归的,算是一个缺点吧。
public class Main {
public static void main(String[] args) {
Radix.to10("12a3", 16); // 4771
Radix.toN("4771", 16); // 12a3
}
}
class Radix {
static void to10(String data, int nn) {
java.util.function.Function<Character, Integer> atoi = (c) -> {
if ((int) (c - 'a') >= 0) {
return (int) (c - 'a') + 10;
}
if ((int) (c - 'A') >= 0) {
return (int) (c - 'A') + 10;
}
if ((int) (c - '0') >= 0) {
return (int) (c - '0');
}
return 0;
};
char[] datachars = data.toCharArray();
var ndata = 0;
for (var i = 0; i < datachars.length; i++)
ndata = ndata * nn + atoi.apply(datachars[i]);
System.out.println(String.format("%s", ndata));
}
static void toN(String data, int nn) {
java.util.function.Function<Integer, Character> itoa = (i) -> {
if (i >= 10) {
return (char) ('a' + i - 10);
}
return (char) ('0' + i);
};
var sb = new StringBuilder();
for (int idata = Integer.valueOf(data); idata != 0; idata /= nn)
sb.insert(0, itoa.apply(idata % nn));
System.out.println(sb.toString());
}
}