左神数据结构与算法 笔记
文章目录
一、第一节课
时间复杂度:
意义:衡量算法流程的复杂度指标
评估算法的核心指标
时间复杂度 额外空间复杂度 常数项时间
常见的常数时间操作
- 常见的算数用算(+ - * % /)
- 常见的位运算(>>, >>>, <<, |, &, ^ )
- 赋值,比较自增,自减操作 以及数组的寻址操作
: 前者右移:带动符号位右移并且最左侧拿符号位补
确定时间复杂度:
拆分代码流程,直至每一个基本动作,保证每个动作是是常数时间操作列出表达式,留下最高阶的项。
三个排序
选择排序:
public static void selectSort(int[] arr)
{
if(arr == null || arr.length<2)
{
return;
}
for(int i = 0;i < arr.length - 1;i++)
{
minIndex = i;
for(int j = i + 1;j < arr.length;j++)
{
minvalue = arr[minIndex] > arr[j] ? j :minIndex;
}
swap(arr,i,minIndex)
}
}
冒泡排序:
public static void BubbleSort(int[] arr)
{
if(arr == null || arr.length < 2)
{
return ;
}
for(int i = arr.length-1;i > 0;i--)
{
for(int j = 0;j < i;j++ )
{
if(arr[j] > arr[j +1])
{
swap(arr,j,j+1);
}
}
}
}
插入排序:
public static void InsertSort(int[] arr)
{
if(arr == null || arr.length < 2)
{
return ;
}
for(int i = 1;i < arr.length;i++)
{
if(int j = i -1;j >= 0 && arr[j] < arr[j + 1]; j--)
{
swap(arr,j,j+1);
}
}
}
交换骚操作
public 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];
}
二分法
有序数组找某个数字是否存在
public static boolean Two01Exist(int[] arr,int num)
{
if(arr.length == null || arr.length == 0)
{
return false;
}
int right = arr.length -1; //arr.length 数组长度 长度-1: 数组最后一个数字的下标
int left = 0; //数组 开始下标
while(left < right)
{
int mid = left + ((right - left) >> 1); //数组中间数字 下标
if(num == arr[mid])
{
return true;
}else if(arr[mid] < num)
{
left = mid +1;
}else{
right = mid -1;
}
}
return num == arr[left];
}
在一个有序数组中,找>=某个数最左侧的位置
public static int leftExit(int[] arr,int value)
{
int Index = -1;
int left = 0;
int right = arr.length -1;
while(left <= right)
{
mid = left + ((right - left) >> 1);
if(arr[mid] >= value)
{
Index = mid;
right = mid -1;
}else{
left = mid +1;
}
}
return Index;
}
在一个无序数组中,找<=某个数最右侧的位置
局部最小值问题
public static int getLessIndex(int[] arr)/*数组无序*/
{
if(arr == null || arr.length == 0)
{
return ;
}
if(arr.length == 1 || arr[1] >arr[o])
{
return 0;
}
if(arr[arr.length-2]>arr[arr.length-1])
{
return arr.length-1;
}
int left = 1;
int right = arr.length - 2;
while(left < right)
{
mid = left - ((right - left) >> 1);
if(arr[mid] > arr[mid+1])
{
left = mid +1;
}else if(arr[mid] > arr[mid -1]){
right = mid -1
}
else{
return mid;
}
return left;
}
}
认识异或运算
异或运算记成(相同为0,不同为1):无进位相加 满足交换律与结合律
同或运算:相同为1,不同为0
数组中,其他数都是偶数,找到出现奇数次的数:用一个x= 0 去异或所有数, 得到的x
public static void printTimes(int[] arr)
{
int eor = 0;
for(int i = 0; i < arr.length;i++)
{
eor ^= arr[i];
}
return eor;
}
-
怎么把一个int 的数字,提取出最右侧的1来
eor & (~eor + 1)
-
一个数组中,有两种数出现了奇数次,其他数字都出现了偶数次,找到这两个数字
public static void printOddTimesNum1(int[] arr) {
int eO = 0;
for (int cur : arr) {
eO ^= cur;
}
System.out.println(eO);
}
public static void printOddTimesNum2(int[] arr) {
int eO = 0, eOhasOne = 0;
for (int curNum : arr) {
eO ^= curNum;
}
int rightOne = eO & (~eO + 1);
for (int cur : arr) {
if ((cur & rightOne) != 0) {
eOhasOne ^= cur;
}
}
System.out.println(eOhasOne + " " + (eO ^ eOhasOne));
}
二、第二节课
一 链表
链表的反转
public static class Node {
public int value;
public Node next;
public Node(int data) {
value = data;
}
}
public static class DoubleNode {
public int value;
public DoubleNode last;
public DoubleNode next;
public DoubleNode(int data) {
value = data;
}
}
// head
// a -> b -> c -> null
// c -> b -> a -> null
public static Node reverseLinkedList(Node head) {
Node pre = null;
Node next = null;
while (head != null) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
public static DoubleNode reverseDoubleList(DoubleNode head) {
DoubleNode pre = null;
DoubleNode next = null;
while (head != null) {
next = head.next;
head.next = pre;
head.last = next;
pre = head;
head = next;
}
return pre;
}
给定值删除
public static Node removeValue(Node head,int data)
{
while(head != null){
if(head.value != data)
{
break;
}
head = head.next;
}
Node pre = head;
Node cur = head;
while(cur != null)
{
if(data == cur.value)
{
pre.next = cur.next;
}else{
pre = cur;
}
cur = cur.next;
}
return head;
}
二、栈和队列
双向链表实现其结构
public static class Node<T> {
public T value;
public Node<T> last;
public Node<T> next;
public Node(T data) {
value = data;
}
}
public static class DoubleEndsQueue<T> {
public Node<T> head;
public Node<T> tail;
public void addFromHead(T value) {
Node<T> cur = new Node<T>(value);
if (head == null) {
head = cur;
tail = cur;
} else {
cur.next = head;
head.last = cur;
head = cur;
}
}
public void addFromBottom(T value) {
Node<T> cur = new Node<T>(value);
if (head == null) {
head = cur;
tail = cur;
} else {
cur.last = tail;
tail.next = cur;
tail = cur;
}
}
public T popFromHead() {
if (head == null) {
return null;
}
Node<T> cur = head;
if (head == tail) {
head = null;
tail = null;
} else {
head = head.next;
cur.next = null;
head.last = null;
}
return cur.value;
}
public T popFromBottom() {
if (head == null) {
return null;
}
Node<T> cur = tail;
if (head == tail) {
head = null;
tail = null;
} else {
tail = tail.last;
tail.next = null;
cur.last = null;
}
return cur.value;
}
public boolean isEmpty() {
return head == null;
}
}
public static class MyStack<T> {
private DoubleEndsQueue<T> queue;
public MyStack() {
queue = new DoubleEndsQueue<T>();
}
public void push(T value) {
queue.addFromHead(value);
}
public T pop() {
return queue.popFromHead();
}
public boolean isEmpty() {
return queue.isEmpty();
}
}
public static class MyQueue<T> {
private DoubleEndsQueue<T> queue;
public MyQueue() {
queue = new DoubleEndsQueue<T>();
}
public void push(T value) {
queue.addFromHead(value);
}
public T poll() {
return queue.popFromBottom();
}
public boolean isEmpty() {
return queue.isEmpty();
}
}
public static boolean isEqual(Integer o1, Integer o2) {
if (o1 == null && o2 != null) {
return false;
}
if (o1 != null && o2 == null) {
return false;
}
if (o1 == null && o2 == null) {
return true;
}
return o1.equals(o2);
}
数组实现其结构
public static class MyQueue {
private int[] arr;
private int pushi;// end
private int polli;// begin
private int size;
private final int limit;
public MyQueue(int limit) {
arr = new int[limit];
pushi = 0;
polli = 0;
size = 0;
this.limit = limit;
}
public void push(int value) {
if (size == limit) {
throw new RuntimeException("队列满了,不能再加了");
}
size++;
arr[pushi] = value;
pushi = nextIndex(pushi);
}
public int pop() {
if (size == 0) {
throw new RuntimeException("队列空了,不能再拿了");
}
size--;
int ans = arr[polli];
polli = nextIndex(polli);
return ans;
}
public boolean isEmpty() {
return size == 0;
}
// 如果现在的下标是i,返回下一个位置
private int nextIndex(int i) {
return i < limit - 1 ? i + 1 : 0;
}
}
四、递归
求数组最大值
/*
复杂度 O(N)
*/
public static int get Max(int[] arr)
{
return process(arr,0,arr.length-1);
}
public static int process(int[] arr,int left,int right)
{
if(left == right)
{
return arr[left];
}
int mid = left + ((rigrt - left)>>1);
int leftMax = process(arr,left,mid);
int rightMax = process(arr,mid + 1,right);
return Math.max(rightMax,leftMax);
}
一类递归的时间复杂度公式
T(N) = aT(N/b) + O(N^d) 子问题规模:b 调用的次数:a 其余时间复杂度:O(N^d)
形如
T ( N )= a * T ( N / b )+ O ( NAd )(其中的 a 、 b 、 d 都是常数)的递归函数,可以直接通过 Master 公式来确定时间复杂度
如果 log ( b , a )< d ,复杂度为 O ( N^d )
如果 log ( b , a ) > d ,复杂度为 O ( N^log ( b , a ))
如果 log ( b , a )== d ,复杂度为 O ( N^d * logN )
哈希表
1)哈希表在使用层面上可以理解为一种集合结构
2)如果只有 key ,没有伴随数据 value ,可以使用 HashSet 结构
3)如果既有 key ,又有伴随数据 value ,可以使用 HashMap 结构
4)有无伴随数据,是 HashMap 和 HashSet 唯一的区别,实际结构是一回事
5)使用哈希表增( put )、删( remove )、改( put )和查( get )的操作,可以认为时间复杂度为 O (1),但是常数时间比较大
6)放入哈希表的东西,如果是基础类型,内部按值传递,内存占用是这个东西的大小
7)放入哈希表的东西,如果不是基础类型,内部按引用传递,内存占用是8字节
第三节课
归并排序
用处: 纠结每个数字的左边或者右边的数字比自己大或者小 例如 求数组小和
整体是个递归:左右分别排好序 然后merge让整体有序
// 递归方法实现
public static void mergeSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
// 请把arr[L..R]排有序
// l...r N
// T(N) = 2 * T(N / 2) + O(N)
// O(N * logN)
public static void process(int[] arr, int L, int R) {
if (L == R) { // base case
return;
}
int mid = L + ((R - L) >> 1);
process(arr, L, mid);
process(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 i = 0;
int p1 = L;
int p2 = M + 1;
while (p1 <= M && p2 <= R) {
help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
}
// 要么p1越界了,要么p2越界了
while (p1 <= M) {
help[i++] = arr[p1++];
}
while (p2 <= R) {
help[i++] = arr[p2++];
}
for (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
}
非递归方法
public static void mergeSort(int[] arr)
{
if(arr == null || arr.length < 2)
{
return ;
}
int N = arr.length;
int size = 1;
while( size < N)
{
int L = 0;
while(L < N)
{
int mid = L +size -1;
if(mid > N)
{
break;
}
int R = Math.min(mid +size,N-1)
mergr(arr,L,mid,R);
L = R +1;
}
if(size > N/ 2)
{
brea
}
size >> 1;
}
}
求数组小和
在一个数组中,一个数左边比她小的数的总和,叫做数的小和所有数字的小和累加起来叫做数组小和
public static int smallSum(int[] arr) {
if (arr == null || arr.length < 2) {
return 0;
}
return process(arr, 0, arr.length - 1);
}
// arr[L..R]既要排好序,也要求小和返回
// 所有merge时,产生的小和,累加
// 左 排序 merge
// 右 排序 merge
// merge
public static int process(int[] arr, int l, int r) {
if (l == r) {
return 0;
}
// l < r
int mid = l + ((r - l) >> 1);
return
process(arr, l, mid)
+
process(arr, mid + 1, r)
+
merge(arr, l, mid, r);
}
public static int merge(int[] arr, int L, int m, int r) {
int[] help = new int[r - L + 1];
int i = 0;
int p1 = L;
int p2 = m + 1;
int res = 0;
while (p1 <= m && p2 <= r) {
res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 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 (i = 0; i < help.length; i++) {
arr[L + i] = help[i];
}
return res;
}
快速排序
Partition过程
给定一个数组 和整数,把小于整数的放在左边 大于的放在右边
要求: 时间复杂度O(N) 额外空间复杂度O(1)
public static int partition(int[] arr, int L, int R) {
if (L > R) {
return -1;
}
if (L == R) {
return L;
}
int lessEqual = L - 1;
int index = L;
while (index < R) {
if (arr[index] <= arr[R]) {
swap(arr, index, ++lessEqual);
}
index++;
}
swap(arr, ++lessEqual, R);
return lessEqual;
}
// arr[L...R] 玩荷兰国旗问题的划分,以arr[R]做划分值
// <arr[R] ==arr[R] > arr[R]
public static int[] netherlandsFlag(int[] arr, int L, int R) {
if (L > R) { // L...R L>R
return new int[] { -1, -1 };
}
if (L == R) {
return new int[] { L, R };
}
int less = L - 1;
int more = R;
int index = L;
while (index < more) {
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
swap(arr, index++, ++less);
} else {
swap(arr, index, --more);
}
}
swap(arr, more, R); // <[R] =[R] >[R]
return new int[] { less + 1, more };
}
public static void quickSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process1(arr, 0, arr.length - 1);
}
public static void process1(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// L..R partition arr[R] [ <=arr[R] arr[R] >arr[R] ]
int M = partition(arr, L, R);
process1(arr, L, M - 1);
process1(arr, M + 1, R);
}
public static void quickSort2(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process2(arr, 0, arr.length - 1);
}
// arr[L...R] 排有序,快排2.0方式
public static void process2(int[] arr, int L, int R) {
if (L >= R) {
return;
}
// [ equalArea[0] , equalArea[0]]
int[] equalArea = netherlandsFlag(arr, L, R);
process2(arr, L, equalArea[0] - 1);
process2(arr, equalArea[1] + 1, R);
}
public static void quickSort3(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process3(arr, 0, arr.length - 1);
}
public static void process3(int[] arr, int L, int R) {
if (L >= R) {
return;
}
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
int[] equalArea = netherlandsFlag(arr, L, R);
process3(arr, L, equalArea[0] - 1);
process3(arr, equalArea[1] + 1, R);
}
荷兰国旗问题
// 荷兰国旗问题
public static int[] netherlandsFlag(int[] arr, int L, int R) {
if (L > R) {
return new int[] { -1, -1 };
}
if (L == R) {
return new int[] { L, R };
}
int less = L - 1;
int more = R;
int index = L;
while (index < more) {
if (arr[index] == arr[R]) {
index++;
} else if (arr[index] < arr[R]) {
swap(arr, index++, ++less);
} else {
swap(arr, index, --more);
}
}
swap(arr, more, R);
return new int[] { less + 1, more };
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
快速排序1.0
在 arr [ L . R ]范围上,进行快速排序的过程:
1)用 arr [ R ]对该范围做 partition ,<= arr [ R ]的数在左部分并且保证 arr [ R ]最后来到左部分的最后一个位置,记为 M ;<= arr [ R ]的数在右部分( arr [ M +1… R ])
2)对 arr [ L … M -1]进行快速排序(递归)
3)对 arr [ M +1.R]进行快速排序(递归)因为每一次 partition 都会搞定一个数的位置且不会再变动,所以排序能完成
// 快排递归版本
public static void quickSort1(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
process(arr, 0, arr.length - 1);
}
public static void process(int[] arr, int L, int R) {
if (L >= R) {
return;
}
swap(arr, L + (int) (Math.random() * (R - L + 1)), R);
int[] equalArea = netherlandsFlag(arr, L, R);
process(arr, L, equalArea[0] - 1);
process(arr, equalArea[1] + 1, R);
}
第四节课
堆结构
1)堆结构就是用数组实现的完全二叉树结构
2)完全二叉树中如果每棵子树的最大值都在顶部就是大根堆
3)完全二叉树中如果每棵子树的最小值都在顶部就是小根堆
4)堆结构的 heaplnsert 与 heapify 操作
5)堆结构的增大和减少
6)优先级队列结构,就是堆结构
public static class MyMaxHeap {
private int[] heap;
private final int limit;
private int heapSize;
public MyMaxHeap(int limit) {
heap = new int[limit];
this.limit = limit;
heapSize = 0;
}
public boolean isEmpty() {
return heapSize == 0;
}
public boolean isFull() {
return heapSize == limit;
}
public void push(int value) {
if (heapSize == limit) {
throw new RuntimeException("heap is full");
}
heap[heapSize] = value;
// value heapSize
heapInsert(heap, heapSize++);
}
// 用户此时,让你返回最大值,并且在大根堆中,把最大值删掉
// 剩下的数,依然保持大根堆组织
public int pop() {
int ans = heap[0];
swap(heap, 0, --heapSize);
heapify(heap, 0, heapSize);
return ans;
}
// 新加进来的数,现在停在了index位置,请依次往上移动,
// 移动到0位置,或者干不掉自己的父亲了,停!
private void heapInsert(int[] arr, int index) {
// [index] [index-1]/2
// index == 0
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
// 从index位置,往下看,不断的下沉
// 停:较大的孩子都不再比index位置的数大;已经没孩子了
private void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left < heapSize) { // 如果有左孩子,有没有右孩子,可能有可能没有!
// 把较大孩子的下标,给largest
int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
// index和较大孩子,要互换
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
private void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
}
堆排序
1,先让整个数组都变成大根堆结构,建立堆的过程:
1)从上到下的方法,时间复杂度为 O ( N * logN )
2)从下到上的方法,时间复杂度为 O ( N )
2,把堆的最大值和堆末尾的值交换,然后减少堆的大小之后,再去调整堆,一直周而复始,时间复杂度为 O ( N * logN )
3,堆的大小减小成0之后,排序完成
// 堆排序额外空间复杂度O(1)
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// O(N*logN)
// for (int i = 0; i < arr.length; i++) { // O(N)
// heapInsert(arr, i); // O(logN)
// }
// O(N)
for (int i = arr.length - 1; i >= 0; i--) {
heapify(arr, i, arr.length);
}
int heapSize = arr.length;
swap(arr, 0, --heapSize);
// O(N*logN)
while (heapSize > 0) { // O(N)
heapify(arr, 0, heapSize); // O(logN)
swap(arr, 0, --heapSize); // O(1)
}
}
// arr[index]刚来的数,往上
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;
}
}
// arr[index]位置的数,能否往下移动
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1; // 左孩子的下标
while (left < heapSize) { // 下方还有孩子的时候
// 两个孩子中,谁的值大,把下标给largest
// 1)只有左孩子,left -> largest
// 2) 同时有左孩子和右孩子,右孩子的值<= 左孩子的值,left -> largest
// 3) 同时有左孩子和右孩子并且右孩子的值> 左孩子的值, right -> 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;
}
}
相关题目:
已知一个几乎游戏的数组。几乎:若数组排好序的话每个元素移动的距离一定不超过k,并且k相当于对数租的长度来说是比较小的
public static void sortedArrDistanceLessK(int[] arr, int k) {
if (k == 0) {
return;
}
// 默认小根堆
PriorityQueue<Integer> heap = new PriorityQueue<>();
int index = 0;
// 0...K-1
for (; index <= Math.min(arr.length - 1, k - 1); index++) {
heap.add(arr[index]);
}
int i = 0;
for (; index < arr.length; i++, index++) {
heap.add(arr[index]);
arr[i] = heap.poll();
}
while (!heap.isEmpty()) {
arr[i++] = heap.poll();
}
}
比较器
1)比较器的实质就是重载比较运算符
2)比较器可以很好的应用在特殊标准的排序上
3)比较器可以很好的应用在根据特殊标准排序的结构上
4)写代码变得异常容易,还用于范型编程
/*
* T一定要是非基础类型,有基础类型需求包一层
*/
public class HeapGreater<T> {
private ArrayList<T> heap;
private HashMap<T, Integer> indexMap;
private int heapSize;
private Comparator<? super T> comp;
public HeapGreater(Comparator<? super T> c) {
heap = new ArrayList<>();
indexMap = new HashMap<>();
heapSize = 0;
comp = c;
}
public boolean isEmpty() {
return heapSize == 0;
}
public int size() {
return heapSize;
}
public boolean contains(T obj) {
return indexMap.containsKey(obj);
}
public T peek() {
return heap.get(0);
}
public void push(T obj) {
heap.add(obj);
indexMap.put(obj, heapSize);
heapInsert(heapSize++);
}
public T pop() {
T ans = heap.get(0);
swap(0, heapSize - 1);
indexMap.remove(ans);
heap.remove(--heapSize);
heapify(0);
return ans;
}
public void remove(T obj) {
T replace = heap.get(heapSize - 1);
int index = indexMap.get(obj);
indexMap.remove(obj);
heap.remove(--heapSize);
if (obj != replace) {
heap.set(index, replace);
indexMap.put(replace, index);
resign(replace);
}
}
public void resign(T obj) {
heapInsert(indexMap.get(obj));
heapify(indexMap.get(obj));
}
// 请返回堆上的所有元素
public List<T> getAllElements() {
List<T> ans = new ArrayList<>();
for (T c : heap) {
ans.add(c);
}
return ans;
}
private void heapInsert(int index) {
while (comp.compare(heap.get(index), heap.get((index - 1) / 2)) < 0) {
swap(index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
private void heapify(int index) {
int left = index * 2 + 1;
while (left < heapSize) {
int best = left + 1 < heapSize && comp.compare(heap.get(left + 1), heap.get(left)) < 0 ? (left + 1) : left;
best = comp.compare(heap.get(best), heap.get(index)) < 0 ? best : index;
if (best == index) {
break;
}
swap(best, index);
index = best;
left = index * 2 + 1;
}
}
private void swap(int i, int j) {
T o1 = heap.get(i);
T o2 = heap.get(j);
heap.set(i, o2);
heap.set(j, o1);
indexMap.put(o2, i);
indexMap.put(o1, j);
}
}
第五节课
前缀树
1)单个字符串中,字符从前到后的加到一棵多叉树上
2)字符放在路上,节点上有专属的数据项(常见的是 pass 和 end 值)
3)所有样本都这样填加,如果没有路就新建,如有路就复用
4)沿途节点的 pass 值增加1,每个字符串结束时来到的节点 end 值增加1可以完成前缀相关的查询
例子
设计一种结构。用户可以:
1) voidinsert ( Stringstr )頃加某个字符串,可以重复頃加,每次算1个
2) intsearch ( Stringstr )查询某个字符串在结构中还有几个
3) voiddelete ( Stringstr )删掉某个字符串,可以重复删除,每次算1个
4) intprefixNumber ( Stringstr )查询有多少个字符串,是以 str 做前缀的
固定数组实现:
class Trie {
class Node {
public int pass;
public int end;
public Node[] nexts;
public Node() {
pass = 0;
end = 0;
nexts = new Node[26];
}
}
private Node root;
public Trie() {
root = new Node();
}
//加入word
public void insert(String word) {
if (word == null) {
return;
}
char[] str = word.toCharArray();
Node node = root;
node.pass++;
int path = 0;
for (int i = 0; i < str.length; i++) { // 从左往右遍历字符
path = str[i] - 'a'; // 由字符,对应成走向哪条路
if (node.nexts[path] == null) {
node.nexts[path] = new Node();
}
node = node.nexts[path];
node.pass++;
}
node.end++;
}
//删除word
public void erase(String word) {
if (countWordsEqualTo(word) != 0) {
char[] chs = word.toCharArray();
Node node = root;
node.pass--;
int path = 0;
for (int i = 0; i < chs.length; i++) {
path = chs[i] - 'a';
if (--node.nexts[path].pass == 0) {
node.nexts[path] = null;
return;
}
node = node.nexts[path];
}
node.end--;
}
}
//word单词加入过几次
public int countWordsEqualTo(String word) {
if (word == null) {
return 0;
}
char[] chs = word.toCharArray();
Node node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {
return 0;
}
node = node.nexts[index];
}
return node.end;
}
public int countWordsStartingWith(String pre) {
if (pre == null) {
return 0;
}
char[] chs = pre.toCharArray();
Node node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {
return 0;
}
node = node.nexts[index];
}
return node.pass;
}
}
哈希表实现:
class Trie {
class Node {
public int pass;
public int end;
public HashMap<Integer, Node> nexts;
public Node() {
pass = 0;
end = 0;
nexts = new HashMap<>();
}
}
private Node root;
public Trie() {
root = new Node();
}
public void insert(String word) {
if (word == null) {
return;
}
char[] chs = word.toCharArray();
Node node = root;
node.pass++;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = (int) chs[i];
if (!node.nexts.containsKey(index)) {
node.nexts.put(index, new Node());
}
node = node.nexts.get(index);
node.pass++;
}
node.end++;
}
public void erase(String word) {
if (countWordsEqualTo(word) != 0) {
char[] chs = word.toCharArray();
Node node = root;
node.pass--;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = (int) chs[i];
if (--node.nexts.get(index).pass == 0) {
node.nexts.remove(index);
return;
}
node = node.nexts.get(index);
}
node.end--;
}
}
public int countWordsEqualTo(String word) {
if (word == null) {
return 0;
}
char[] chs = word.toCharArray();
Node node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = (int) chs[i];
if (!node.nexts.containsKey(index)) {
return 0;
}
node = node.nexts.get(index);
}
return node.end;
}
public int countWordsStartingWith(String pre) {
if (pre == null) {
return 0;
}
char[] chs = pre.toCharArray();
Node node = root;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = (int) chs[i];
if (!node.nexts.containsKey(index)) {
return 0;
}
node = node.nexts.get(index);
}
return node.pass;
}
}
不基于比较的排序(桶排序)
桶排序思想下的排序:计数排序&基数排序
1)桶排序思想下的排序都是不基于比较的排序
2)时间复杂度为 O ( N ),额外空间负载度 O ( M )
3)应用范围有限,需要样本的数据状况满足桶的划分
计数排序—一般要求:样本整数而且范围较窄:
public static void countSort(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 i = 0;
for (int j = 0; j < bucket.length; j++) {
while (bucket[j]-- > 0) {
arr[i++] = j;
}
}
}
基数排序—一般是样本十进制的正整数
// only for no-negative value
public static void radixSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
radixSort(arr, 0, arr.length - 1, maxbits(arr));
}
public static int maxbits(int[] arr) {
int max = Integer.MIN_VALUE;
for (int i = 0; i < arr.length; i++) {
max = Math.max(max, arr[i]);
}
int res = 0;
while (max != 0) {
res++;
max /= 10;
}
return res;
}
// arr[L..R]排序 , 最大值的十进制位数digit
public static void radixSort(int[] arr, int L, int R, int digit) {
final int radix = 10;
int i = 0, j = 0;
// 有多少个数准备多少个辅助空间
//不再用桶来逐个放入, 而是直接算出桶里面应该有多少个元素, 这样就直接知道排序后下标的位置了.也就不需要桶了.
int[] help = new int[R - L + 1];
for (int d = 1; d <= digit; d++) { // 有多少位就进出几次
// 10个空间
// count[0] 当前位(d位)是0的数字有多少个
// count[1] 当前位(d位)是(0和1)的数字有多少个
// count[2] 当前位(d位)是(0、1和2)的数字有多少个
// count[i] 当前位(d位)是(0~i)的数字有多少个
int[] count = new int[radix]; // count[0..9]
for (i = L; i <= R; i++) {
// 103 1 3
// 209 1 9
j = getDigit(arr[i], d);
count[j]++;
}
for (i = 1; i < radix; i++) {
count[i] = count[i] + count[i - 1];
}
for (i = R; i >= L; i--) {
j = getDigit(arr[i], d);
help[count[j] - 1] = arr[i];
count[j]--;
}
for (i = L, j = 0; i <= R; i++, j++) {
arr[i] = help[j];
}
}
}
public static int getDigit(int x, int d) {
return ((x / ((int) Math.pow(10, d - 1))) % 10);
}
排序算法的稳定性
稳定性是指同样大小的样本再排序之后不会改变相对次序
对基础类型来说,稳定性毫无意义
对非基础类型来说,稳定性有重要意义
有些排序算法可以实现成稳定的,而有些排序算法无论如何都实现不成稳定的
排序算法总结时间复杂度
后两者是不基于比较 | 时间复杂度 | 额外空间复杂度 | 稳定性 |
---|---|---|---|
选择排序 | O (NA2) | O (1) | 无 |
冒泡排序 | O (NA2) | O (1) | 有 |
插入排序 | O (NA2) | O (1) | 有 |
归并排序 | O ( N * logN ) | O ( N ) | 有 |
随机快排 | O ( N * logN ) | O ( logN ) | 无 |
堆排序 | O ( N * ogN ) | O ( 1 ) | 无 |
计数排序 | O ( N ) | O ( M ) | 有 |
基数排序 | O ( N ) | O ( N ) | 有 |
排序算法总结
1)不基于比较的排序,对样本数据有严格要求,不易改写
2)基于比较的排序,只要规定好两个样本怎么比大小就可以直接复用
3)基于比较的排序,时间复杂度的极限是 O ( N * logN )°4)时间复杂度 O ( N * logN )、额外空间复杂度低于 O ( N )、且稳定的基于比较的排序是不存在的。
5)为了绝对的速度选快排、为了省空间选堆排、为了稳定性选归并
按需使用:稳定性 :选择归并 额外空间:堆排序 速度 :快排
第六节课
1.链表
使用用容器
1.1快慢指针
1)输入链表头节点,奇数长度返回中点,偶数长度返回上中点
2)输入链表头节点,奇数长度返回中点,偶数长度返回下中点
3)输入链表头节点,奇数长度返回中点前一个,偶数长度返回上中点前一个
4)输入链表头节点,奇数长度返回中点前一个,偶数长度返回下中点前一个
public static class Node {
public int value;
public Node next;
public Node(int v) {
value = v;
}
}
// head 头
public static Node midOrUpMidNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return head;
}
// 链表有3个点或以上
Node slow = head.next;
Node fast = head.next.next;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
public static Node midOrDownMidNode(Node head) {
if (head == null || head.next == null) {
return head;
}
Node slow = head.next;
Node fast = head.next;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
public static Node midOrUpMidPreNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
Node slow = head;
Node fast = head.next.next;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
public static Node midOrDownMidPreNode(Node head) {
if (head == null || head.next == null) {
return null;
}
if (head.next.next == null) {
return head;
}
Node slow = head;
Node fast = head.next;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
//使用用容器
public static Node right1(Node head) {
if (head == null) {
return null;
}
Node cur = head;
ArrayList<Node> arr = new ArrayList<>();
while (cur != null) {
arr.add(cur);
cur = cur.next;
}
return arr.get((arr.size() - 1) / 2);
}
1.2给定一个单链表head,判断单链表是否为回文结构
public static boolean isPalindrome1(Node head) {
Stack<Node> stack = new Stack<Node>();
Node cur = head;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
while (head != null) {
if (head.value != stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
public static boolean isPalindrome2(Node head) {
if (head == null || head.next == null) {
return true;
}
Node right = head.next;
Node cur = head;
while (cur.next != null && cur.next.next != null) {
right = right.next;
cur = cur.next.next;
}
Stack<Node> stack = new Stack<Node>();
while (right != null) {
stack.push(right);
right = right.next;
}
while (!stack.isEmpty()) {
if (head.value != stack.pop().value) {
return false;
}
head = head.next;
}
return true;
}
修改原链表
// need O(1) extra space
public static boolean isPalindrome3(Node head) {
if (head == null || head.next == null) {
return true;
}
Node n1 = head;
Node n2 = head;
while (n2.next != null && n2.next.next != null) { // find mid node
n1 = n1.next; // n1 -> mid
n2 = n2.next.next; // n2 -> end
}
// n1 中点
n2 = n1.next; // n2 -> right part first node
n1.next = null; // mid.next -> null
Node n3 = null;
while (n2 != null) { // right part convert
n3 = n2.next; // n3 -> save next node
n2.next = n1; // next of right node convert
n1 = n2; // n1 move
n2 = n3; // n2 move
}
n3 = n1; // n3 -> save last node
n2 = head;// n2 -> left first node
boolean res = true;
while (n1 != null && n2 != null) { // check palindrome
if (n1.value != n2.value) {
res = false;
break;
}
n1 = n1.next; // left to mid
n2 = n2.next; // right to mid
}
n1 = n3.next;
n3.next = null;
while (n1 != null) { // recover list
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
1.3将单链表遵照某值左边小,中间等于,右边大
将单链表放于数组里,在数组里面做partition
public static Node listPartition1(Node head, int pivot) {
if (head == null) {
return head;
}
Node cur = head;
int i = 0;
while (cur != null) {
i++;
cur = cur.next;
}
Node[] nodeArr = new Node[i];
i = 0;
cur = head;
for (i = 0; i != nodeArr.length; i++) {
nodeArr[i] = cur;
cur = cur.next;
}
arrPartition(nodeArr, pivot);
for (i = 1; i != nodeArr.length; i++) {
nodeArr[i - 1].next = nodeArr[i];
}
nodeArr[i - 1].next = null;
return nodeArr[0];
}
public static void arrPartition(Node[] nodeArr, int pivot) {
int small = -1;
int big = nodeArr.length;
int index = 0;
while (index != big) {
if (nodeArr[index].value < pivot) {
swap(nodeArr, ++small, index++);
} else if (nodeArr[index].value == pivot) {
index++;
} else {
swap(nodeArr, --big, index);
}
}
}
public static void swap(Node[] nodeArr, int a, int b) {
Node tmp = nodeArr[a];
nodeArr[a] = nodeArr[b];
nodeArr[b] = tmp;
}
分成小中大三部分,各部分串起来
public static Node listPartition2(Node head, int pivot) {
Node sH = null; // small head
Node sT = null; // small tail
Node eH = null; // equal head
Node eT = null; // equal tail
Node mH = null; // big head
Node mT = null; // big tail
Node next = null; // save next node
// every node distributed to three lists
while (head != null) {
next = head.next;
head.next = null;
if (head.value < pivot) {
if (sH == null) {
sH = head;
sT = head;
} else {
sT.next = head;
sT = head;
}
} else if (head.value == pivot) {
if (eH == null) {
eH = head;
eT = head;
} else {
eT.next = head;
eT = head;
}
} else {
if (mH == null) {
mH = head;
mT = head;
} else {
mT.next = head;
mT = head;
}
}
head = next;
}
// 小于区域的尾巴,连等于区域的头,等于区域的尾巴连大于区域的头
if (sT != null) { // 如果有小于区域
sT.next = eH;
eT = eT == null ? sT : eT; // 下一步,谁去连大于区域的头,谁就变成eT
}
// 下一步,一定是需要用eT 去接 大于区域的头
// 有等于区域,eT -> 等于区域的尾结点
// 无等于区域,eT -> 小于区域的尾结点
// eT 尽量不为空的尾巴节点
if (eT != null) { // 如果小于区域和等于区域,不是都没有
eT.next = mH;
}
return sH != null ? sH : (eH != null ? eH : mH);
}
1.4一种特殊的单链表节点类
描述如下
public static class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
rand 指针是单链表节点结构中新增的指针, rand 可能指向链表中的任意一个节点,也可能指向 null 。
给定一个由 Node 节点类型组成的无环单链表的头节点 head ,请实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。
【要求】
时间复杂度 O ( N ),额外空间复杂度 O (1)
哈希表
public static Node copyRandomList1(Node head) {
// key 老节点
// value 新节点
HashMap<Node, Node> map = new HashMap<Node, Node>();
Node cur = head;
while (cur != null) {
map.put(cur, new Node(cur.val));
cur = cur.next;
}
cur = head;
while (cur != null) {
// cur 老
// map.get(cur) 新
// 新.next -> cur.next克隆节点找到
map.get(cur).next = map.get(cur.next);
map.get(cur).random = map.get(cur.random);
cur = cur.next;
}
return map.get(head);
}
public static Node copyRandomList2(Node head) {
if (head == null) {
return null;
}
Node cur = head;
Node next = null;
// 1 -> 2 -> 3 -> null
// 1 -> 1' -> 2 -> 2' -> 3 -> 3'
while (cur != null) {
next = cur.next;
cur.next = new Node(cur.val);
cur.next.next = next;
cur = next;
}
cur = head;
Node copy = null;
// 1 1' 2 2' 3 3'
// 依次设置 1' 2' 3' random指针
while (cur != null) {
next = cur.next.next;
copy = cur.next;
copy.random = cur.random != null ? cur.random.next : null;
cur = next;
}
Node res = head.next;
cur = head;
// 老 新 混在一起,next方向上,random正确
// next方向上,把新老链表分离
while (cur != null) {
next = cur.next.next;
copy = cur.next;
cur.next = next;
copy.next = next != null ? next.next : null;
cur = next;
}
return res;
}
1.5链表
给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回 nul
【要求】
如果两个链表长度之和为 N ,时间复杂度请达到 O ( N ),额外空间复杂度请达到 O (1)。’
哈希set方法::一小时半
public static Node getIntersectNode(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
if (loop1 == null && loop2 == null) {
return noLoop(head1, head2);
}
if (loop1 != null && loop2 != null) {
return bothLoop(head1, loop1, head2, loop2);
}
return null;
}
// 找到链表第一个入环节点,如果无环,返回null
public static Node getLoopNode(Node head) {
if (head == null || head.next == null || head.next.next == null) {
return null;
}
// n1 慢 n2 快
Node slow = head.next; // n1 -> slow
Node fast = head.next.next; // n2 -> fast
while (slow != fast) {
if (fast.next == null || fast.next.next == null) {
return null;
}
fast = fast.next.next;
slow = slow.next;
}
// slow fast 相遇
fast = head; // n2 -> walk again from head
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
// 如果两个链表都无环,返回第一个相交节点,如果不想交,返回null
public static Node noLoop(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node cur1 = head1;
Node cur2 = head2;
int n = 0;
while (cur1.next != null) {
n++;
cur1 = cur1.next;
}
while (cur2.next != null) {
n--;
cur2 = cur2.next;
}
if (cur1 != cur2) {
return null;
}
// n : 链表1长度减去链表2长度的值
cur1 = n > 0 ? head1 : head2; // 谁长,谁的头变成cur1
cur2 = cur1 == head1 ? head2 : head1; // 谁短,谁的头变成cur2
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
// 两个有环链表,返回第一个相交节点,如果不想交返回null
public static Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
Node cur1 = null;
Node cur2 = null;
if (loop1 == loop2) {
cur1 = head1;
cur2 = head2;
int n = 0;
while (cur1 != loop1) {
n++;
cur1 = cur1.next;
}
while (cur2 != loop2) {
n--;
cur2 = cur2.next;
}
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 == head1 ? head2 : head1;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
} else {
cur1 = loop1.next;
while (cur1 != loop1) {
if (cur1 == loop2) {
return loop1;
}
cur1 = cur1.next;
}
return null;
}
}
1.6链表(一)
能不能不给单链表头结点,只给想要删除的节点就能做到在链表上把这个点删掉