05 算法
概念:数组是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
错误描述改正:数组是适合查找操作,但是查找的时间复杂度并不为O(1)。即便是排好序的数组,你用二分查找,时间复杂度也是O(logn)。所以正确的表述应该是,数组支持随机访问,根据下标随机访问的时间复杂度为O(1)。
06 链表
缓存的大小有限,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该保留?这就需要缓存淘汰策略来决定。常见的三种策略:先进先出策略(FIFO),最少使用策略(LFU),最近最少使用策略(LRU)。
数组与链表的区别:数组需要一块连续的内存空间来存储,链表通过指针将一组零碎的内存块串联起来使用。
写链表的代码技巧:
-
理解指针或引用的含义
将某个变量赋值给引用,实际上就是将这个变量的地址赋值给引用,或者反过来说,引用中存储了这个变量的内存地址,指向了这个变量,通过引用就能找到这个变量。
-
警惕指针丢失和内存泄漏
-
利用哨兵简化实现难度
-
重点留意边界条件处理
如果链表为空时,代码能否正常工作?
如果链表只包含一个结点时,代码能否正常工作?
如果链表只包含两个结点时,代码能否正常工作?
代码逻辑在处理头结点和未结点的时候,能否正常工作?
单链表反转
package linkedlist;
/**
* Description:单链表反转
*
* @author hanshuangwen
* @create 2021-05-23 22:24
*/
public class SingleInversion {
static class Node{
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
public static void main(String[] args) {
Node sentinel=new Node(-1);
sentinel.next=null;
Node head=sentinel;
insertHead(head,1);
insertHead(head,2);
insertHead(head,3);
insertHead(head,4);
inverion(head);
print(head);
}
private static void print(Node head) {
Node p=head.next;
while (p!=null){
System.out.println(p.data);
p=p.next;
}
}
private static void inverion(Node head) {
Node after=null;
Node cur=head.next;
Node before=null;
while (cur!=null){
before=cur.next;
cur.next=after;
after=cur;
cur=before;
}
head.next=after;
}
private static void insertHead(Node head, int i) {
Node newNode=new Node(i);
newNode.next=head.next;
head.next=newNode;
}
}
删除链表倒数第n个节点
两个有序的链表合并
package linkedlist;
/**
* Description:两个有序链表的合并
*
* @author hanshuangwen
* @create 2021-05-23 22:57
*/
public class MergeLinked {
static class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
public static void main(String[] args) {
Node sentinel = new Node(-1);
sentinel.next = null;
Node head1 = sentinel;
insertHead(head1, 9);
insertHead(head1, 7);
insertHead(head1, 5);
insertHead(head1, 3);
insertHead(head1, 1);
Node head2 = sentinel;
insertHead(head2, 8);
insertHead(head2, 6);
insertHead(head2, 4);
insertHead(head2, 2);
merge(head1, head2);
print(head1);
}
private static Node merge(Node head1, Node head2) {
if (head1.next == null || head2.next == null) {
return head1.next == null ? head1 : head2;
}
Node head = head1.next.data < head2.next.data ? head1 : head2;
Node cur1 = head == head1 ? head1.next : head2.next;
Node cur2 = head == head1 ? head2.next : head1.next;
Node pre = null;
Node next = null;
while (cur1 != null && cur2 != null) {
if (cur1.data <= cur2.data) {
pre = cur1;
cur1 = cur1.next;
} else {
pre.next = cur2;
next = cur2.next;
cur2.next = cur1;
//注意要走pre
pre = cur2;
cur2 = next;
}
}
pre.next = cur1 == null ? cur2 : cur1;
return head;
}
private static void insertHead(Node head, int i) {
Node newNode = new Node(i);
newNode.next = head.next;
head.next = newNode;
}
private static void print(Node head) {
Node p = head.next;
while (p != null) {
System.out.println(p.data);
p = p.next;
}
}
}
如何基于链表实现LRU缓存淘汰算法?
思路:维护一个有序单链表,越靠近链表尾部的节点是越早之前访问的。当有一个新数据被访问时,我们从头开始顺序遍历链表。
1、如果此数据之前被缓存在链表中了,我们遍历得到这个数据对应的节点,并将其从原来的位置删除,然后再插入到链表的头部。
2、如果数据没有在缓存中,又可以分为两种情况
- 如果此时缓存未满,则直接插入到链表的头部
- 如果此时缓存已满,则链表尾节点删除,将新的数据节点插入链表的头部
代码:
package array;
/**
* Description:
*
* @author hanshuangwen
* @create 2021-05-21 22:53
*/
public class LruDemo {
public static final int size=4;
public static int count=0;
static class Node{
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
public static void main(String[] args) {
Node sentinel=new Node(-1);
Node head=sentinel;
sentinel.next=null;
insertNode(head,1);
insertNode(head,2);
insertNode(head,3);
insertNode(head,4);
// use(head,4);
use(head,5);
// use(head,4);
traverse(head);
}
private static void traverse(Node head){
Node p=head;
while (p!=null){
System.out.println(p.data);
p=p.next;
}
}
private static boolean delete(Node head,int data){
Node p=head;
while (p.next!=null){
if (p.next.data==data){
p.next=p.next.next;
count--;
return true;
}
p=p.next;
}
return false;
}
private static void deleteLast(Node head){
Node p=head;
Node n=head.next;
while (p!=null&&n!=null){
if (n.next==null){
p.next=n.next;
count--;
return;
}
p=p.next;
n=n.next;
}
}
private static void insertNode(Node head,int num){
if (count==size){
throw new RuntimeException("链表已满");
}
count++;
Node newNode=new Node(num);
newNode.next=head.next;
head.next=newNode;
}
private static void use(Node head, int num) {
boolean isHave = delete(head, num);
if (isHave){
insertNode(head,num);
return;
}
if (count<size){
insertNode(head,num);
return;
}
deleteLast(head);
insertNode(head,num);
}
}
用单链表判断一个字符串是否时回文字符串?
package array;
import java.util.Stack;
/**
* Description:
*
* @author hanshuangwen
* @create 2021-05-22 0:14
*/
public class ReString {
static class Node {
char data;
Node next;
public Node(char data) {
this.data = data;
}
}
public static void main(String[] args) {
String a = "abcdba";
judgeRe(a);
}
private static void judgeRe(String a) {
StringToNode(a);
}
private static void insertHead(Node head, char data) {
Node newNode = new Node(data);
newNode.next = head.next;
head.next = newNode;
}
private static void print(Node head) {
Node p = head;
while (p != null) {
System.out.println(p.data);
p = p.next;
}
}
private static void StringToNode(String a) {
Node sentinel = new Node('/');
sentinel.next = null;
Node head = sentinel;
for (int i = 0; i < a.length(); i++) {
insertHead(head, a.charAt(i));
}
//方法一,通过一个栈实现
// System.out.println(isPalindrome1(head));
//方法二:也是通过栈实现,但是只压前半部分数据进栈
// System.out.println(isPalindrome2(head));
System.out.println(isPalindrome3(head));
print(head);
}
private static boolean isPalindrome3(Node head) {
Node n1 = head.next;
Node n2 = head.next;
while (n2.next != null && n2.next.next != null) {
n1 = n1.next;
n2 = n2.next.next;
}
n2 = n1.next;
n1.next = null;
Node n3 = null;
while (n2 != null) {
n3 = n2.next;
n2.next = n1;
n1 = n2;
n2 = n3;
}
n3 = n1;
n2 = head.next;
boolean res = true;
while (n1 != null && n2 != null) {
if (n1.data != n2.data) {
res = false;
break;
}
n1 = n1.next;
n2 = n2.next;
}
n1 = n3.next;
n3.next = null;
while (n1 != null) {
n2 = n1.next;
n1.next = n3;
n3 = n1;
n1 = n2;
}
return res;
}
private static boolean isPalindrome2(Node head) {
if (head.next == null || head.next.next == null) {
return true;
}
//找偶数链表的右中节点,所以慢节点会从第二节点开始
//如果要找链表的左中节点,那慢节点要从第一个节点开始
Node fast = head.next;
Node right = fast.next;
while (fast.next != null && fast.next.next != null) {
fast = fast.next.next;
right = right.next;
}
Stack<Character> stack = new Stack<>();
while (right != null) {
stack.push(right.data);
right = right.next;
}
fast = head.next;
while (!stack.isEmpty()) {
if (stack.pop() != fast.data) {
return false;
}
fast = fast.next;
}
return true;
}
private static boolean isPalindrome1(Node head) {
Stack<Character> stack = new Stack<>();
Node p = head.next;
while (p != null) {
stack.push(p.data);
p = p.next;
}
p = head.next;
while (p != null) {
if (p.data != stack.pop()) {
return false;
}
p = p.next;
}
return true;
}
}
08 栈
概念:当某个数据集合只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性,我们就应该首选“栈”这种数据结构。
- 用数组实现的栈,叫做顺序栈
- 用链表实现的栈,叫做链式栈
package stack;
/**
* Description:顺序栈
*
* @author hanshuangwen
* @create 2021-05-24 22:42
*/
public class ArrayStack {
private String[] items;
private int count;
private int n;
public ArrayStack(int n) {
this.items = new String[n];
this.count = 0;
this.n = n;
}
public boolean push(String item) {
if (count >= n) {
resize();
}
items[count] = item;
++count;
return true;
}
private void resize() {
String[] newItems = new String[n * 2];
System.arraycopy(items, 0, newItems, 0, n);
items = newItems;
n = 2 * n;
}
public String pop() {
if (count == 0) {
return null;
}
return items[--count];
}
public static void main(String[] args) {
ArrayStack stack = new ArrayStack(3);
stack.push("a");
stack.push("b");
stack.push("c");
stack.push("d");
stack.push("e");
stack.push("f");
stack.push("g");
System.out.println(stack.pop());
}
}
package stack;
/**
* Description:链式栈
*
* @author hanshuangwen
* @create 2021-05-25 23:42
*/
public class LinkedStack {
Node head = null;
int count;
int n;
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
public LinkedStack(int size) {
Node sentinel = new Node(-1);
sentinel.next = null;
head = sentinel;
n = size;
count = 0;
}
public int pop() {
if (count == 0) {
return -1;
}
--count;
return deleteHead(head);
}
protected int deleteHead(Node head) {
Node del = head.next;
head.next = head.next.next;
return del.data;
}
public boolean push(int data) {
if (count >= n) {
return false;
}
++count;
insertHead(head, data);
return true;
}
protected void insertHead(Node head, int data) {
Node newNode = new Node(data);
newNode.next = head.next;
head.next = newNode;
}
public static void main(String[] args) {
LinkedStack linkedStack = new LinkedStack(3);
linkedStack.push(1);
linkedStack.push(2);
linkedStack.push(3);
System.out.println(linkedStack.pop());
}
}
09 队列
顺序队列:用数组实现的队列
链式队列:用链表实现的队列
package queue;
/**
* Description:顺序队列
*
* @author hanshuangwen
* @create 2021-05-26 22:43
*/
public class ArrayQueue {
private int[] items;
private int n;
private int head = 0;
private int tail = 0;
public ArrayQueue(int capacity) {
this.n = capacity;
items = new int[capacity];
}
public boolean enQueue(int data) {
if (tail >= n) {
if (head == 0) {
return false;
}
for (int i = head; i < tail; i++) {
items[i - head] = items[i];
}
head = 0;
tail -= head;
}
items[tail++] = data;
return true;
}
public int deQueue() {
if (head == tail) {
throw new RuntimeException();
}
return items[head++];
}
public static void main(String[] args) {
ArrayQueue arrayQueue = new ArrayQueue(3);
arrayQueue.enQueue(1);
arrayQueue.enQueue(2);
arrayQueue.enQueue(3);
System.out.println(arrayQueue.deQueue());
}
}
package queue;
/**
* Description:链式队列
*
* @author hanshuangwen
* @create 2021-05-26 22:54
*/
public class LinkedQueue {
static class Node {
private int data;
private Node next;
public Node(int data) {
this.data = data;
}
}
private int n;
private int count = 0;
private Node head;
private Node tail;
private Node sentinel;
public LinkedQueue(int n) {
this.n = n;
sentinel = new Node(-1);
sentinel.next = null;
tail = sentinel;
}
public boolean enQueue(int data) {
if (count >= n) {
return false;
}
tail.next = new Node(data);
tail = tail.next;
head = sentinel.next;
++count;
return true;
}
public int deQueue() {
if (head == tail) {
throw new RuntimeException();
}
Node del = head;
sentinel.next = sentinel.next.next;
head = sentinel.next;
--count;
return del.data;
}
public static void main(String[] args) {
LinkedQueue linkedQueue = new LinkedQueue(3);
linkedQueue.enQueue(1);
linkedQueue.enQueue(2);
linkedQueue.enQueue(3);
System.out.println(linkedQueue.deQueue());
}
}
package queue;
/**
* Description:循环队列
*
* @author hanshuangwen
* @create 2021-05-26 22:43
*/
public class CycleQueue {
private int[] items;
private int n;
private int head = 0;
private int tail = 0;
public CycleQueue(int capacity) {
this.n = capacity;
items = new int[capacity];
}
public boolean enQueue(int data) {
if ((tail + 1) % n == head) {
return true;
}
items[tail] = data;
tail = (tail + 1) % n;
return true;
}
public int deQueue() {
if (head == tail) {
throw new RuntimeException();
}
int del = items[head];
head = (head + 1) % n;
return del;
}
public static void main(String[] args) {
CycleQueue arrayQueue = new CycleQueue(3);
arrayQueue.enQueue(1);
arrayQueue.enQueue(2);
arrayQueue.enQueue(3);
System.out.println(arrayQueue.deQueue());
}
}
10 递归
概念:去的过程叫“递“,回来的过程叫”归“。
递归需要满足的三个条件:
- 一个问题的解可以分为几个子问题的解
- 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
- 存在递归终止条件
写递归代码的关键:写出递推公式,找到终止条件
总结:写递归代码的关键就是找打如何将大问题分解成小问题的规律,并且基于此写出递推公式,然后推敲终止条件,最后将递推公式和终止条件翻译成代码。
递归代码两个常见问题:
- 递归代码要警惕堆栈溢出
- 递归代码要警惕重复计算(将已经计算过的值,放到hash表存起来)
package recursion;
/**
* Description:累加递归
*
* @author hanshuangwen
* @create 2021-05-27 23:39
*/
public class LeijiaRec {
public static void main(String[] args) {
//递推公式:f(n)=f(n-1)+1
//终止条件:f(1)=1;
int leijia = leijia(5);
System.out.println(leijia);
//非递归
int i = leijiaNon(5);
System.out.println(i);
}
private static int leijiaNon(int num) {
int ret = 1;
for (int i = 2; i <= num; i++) {
ret += 1;
}
return ret;
}
private static int leijia(int i) {
if (i == 1) {
return 1;
}
return leijia(i - 1) + 1;
}
}
package recursion;
import java.util.HashMap;
/**
* Description:走台阶
*
* @author hanshuangwen
* @create 2021-05-27 23:49
*/
public class Steps {
public static void main(String[] args) {
//递推公式:f(n)=f(n-1)+f(n-2)
//终止条件:f(1)=1 f(2)=2
int num = goStep(5);
System.out.println(num);
//非递归实现
int num2 = goStepNon(5);
System.out.println(num2);
}
private static int goStepNon(int data) {
if (data == 1) {
return 1;
}
if (data == 2) {
return 2;
}
int pp = 1;
int p = 2;
int res = 0;
for (int i = 3; i <= data; i++) {
res = pp + p;
pp = p;
p = res;
}
return res;
}
private static int goStep(int i) {
HashMap<Integer, Integer> existing = new HashMap<>();
if (existing.containsKey(i)) {
return existing.get(i);
}
if (i == 1) {
return 1;
}
if (i == 2) {
return 2;
}
int i1 = goStep(i - 1) + goStep(i - 2);
existing.put(i, i1);
return i1;
}
}
11 排序
排序算法的执行效率:
对于排序算法的执行效率的分析,我们一般会从这几个方面来衡量:
- 最好情况、最坏情况、平均情况时间复杂度
- 时间复杂度的系数、常数、低阶
- 比较次数和交换(移动次数)
排序算法的内存消耗:
原地排序:特指空间复杂度是O(1)的算法。
排序算法的稳定性:
稳定性:如果待排序的序列中存在值相等的元素,经过排序后,相等元素之间原有的先后顺序不变。
为什么要稳定:稳定排序算法可以保持第一个排序值相同的两个对象,在经过其他值第二次排序后,前后顺序不变。
冒泡排序
package sort;
/**
* Description:冒泡排序
*
* @author hanshuangwen
* @create 2021-05-29 21:16
*/
public class BubbleSort {
public static void bubbleSort(int[] a) {
if (a.length < 1) {
return;
}
for (int i = 1; i < a.length; i++) {
//提前退出冒泡排序的标志位
boolean isSort = true;
for (int j = 0; j < a.length - i; j++) {
if (a[j + 1] < a[j]) {
int temp = a[j + 1];
a[j + 1] = a[j];
a[j] = temp;
isSort = false;
}
}
if (isSort) {
break;
}
}
}
public static void main(String[] args) {
int[] a = {4, 5, 6, 3, 2, 1};
bubbleSort(a);
for (int i : a) {
System.out.println(i);
}
}
}
最好情况:123456,会提前退出循环,所以是O(n)
最坏情况:654321,所以是O(n2);
是原地排序。
是稳定排序算法。
插入排序
package sort;
/**
* Description:
*
* @author hanshuangwen
* @create 2021-05-29 22:09
*/
public class InsertSort {
public static void insertSort(int[] a, int n) {
if (n < 1) {
return;
}
for (int i = 1; i < n; i++) {
int value = a[i];
int j = i - 1;
while (j >= 0) {
if (a[j] > value) {
a[j + 1] = a[j];
} else {
break;
}
j--;
}
a[j + 1] = value;
}
}
}
最好情况:123456,O(n)
最坏情况:654321,O(n2)
是原地排序
是稳定排序算法
选择排序
package sort;
/**
* Description:
*
* @author hanshuangwen
* @create 2021-05-29 22:38
*/
public class SelectSort {
public static void selectSort(int[] a,int n){
if (n<1){
return;
}
for (int i = 0; i < n; i++) {
int min=i;
for (int j=i+1;j<n;j++){
if (a[j]<a[min]){
min=j;
}
}
int temp=a[i];
a[i]=a[min];
a[min]=temp;
}
}
public static void main(String[] args) {
int[] a={4,5,6,3,2,1};
selectSort(a,a.length);
for (int i : a) {
System.out.println(i);
}
}
}
最好情况最坏情况:O(n2)
是原地排序算法
不是稳定的排序算法
归并排序
package sort;
/**
* Description:归并排序
*
* @author hanshuangwen
* @create 2021-05-30 21:19
*/
public class MergeSort {
public static void mergeSort(int[] a,int p,int r,int[] temp){
if (p>=r){
return;
}
int q=p+(r-p)/2;
mergeSort(a,p,q,temp);
mergeSort(a,q+1,r,temp);
merge(a,p,q,r,temp);
}
private static void merge(int[] a, int p, int q, int r,int[] temp) {
int i=p;
int j=q+1;
int k=0;
while (i<=q&&j<=r){
if (a[i]<=a[j]){
temp[k++]=a[i++];
}else {
temp[k++]=a[j++];
}
}
while (i<=q){
temp[k++]=a[i++];
}
while (j<=r){
temp[k++]=a[j++];
}
for (int l = 0; l < r-p+1; l++) {
a[p+l]=temp[l];
}
}
public static void main(String[] args) {
int[] a={11,8,3,9,7,1,2,5};
int[] temp=new int[a.length];
mergeSort(a,0,a.length-1,temp);
for (int i : a) {
System.out.println(i);
}
}
}
递归的时间复杂度判断:一个问题a可以分解成多个子问题b、c,那求解问题a就可以分解为求解问题b,c。T(a)=T(b)+T©+K。
其中K等于将两个子问题b,c的结果合并成问题a的结果所消耗的时间。
假设对n个元素进行归并排序需要的时间是T(n),那分解成两个子数组排序的时间就是T(n/2)。merge()函数合并两个有序子数组的时间复杂度是O(n)。所以归并排序的时间复杂度的计算公式就是:
T(1)=C n=1时,只需要常量级的执行时间,所以表示成C
T(n)=2*T(n/2)+n n>1
最好最坏平均都是O(nlogn),是稳定排序算法。
快速排序
package sort;
/**
* Description:
*
* @author hanshuangwen
* @create 2021-05-31 22:19
*/
public class QuickSort {
public static void quickSort(int[] a, int p, int r) {
if (p >= r) {
return;
}
//找中间比较数
int q = findMid(a, p, r);
quickSort(a, p, q - 1);
quickSort(a, q + 1, r);
}
private static int findMid(int[] a, int p, int r) {
int pivot = a[r];
int i = p;
int j = p;
while (i <= r - 1 && j <= r - 1) {
if (a[j] >= pivot) {
j++;
} else {
int temp = a[j];
a[j] = a[i];
a[i] = temp;
i++;
j++;
}
}
int temp = a[j];
a[j] = a[i];
a[i] = temp;
return i;
}
public static void main(String[] args) {
int[] a = {8, 10, 2, 3, 6, 1, 5};
quickSort(a, 0, a.length - 1);
for (int i : a) {
System.out.println(i);
}
}
}
假设每次分区,都能正好把数组分成两个相同大小的小区间,并且分区的时间复杂度是O(n)那快排的时间复杂度公式如下:
T(1)=C n=1
T(n)=n+T(n/2) n>1
但是当分区不均等时,并且是递增有序时,那分区就要进行n次,所以最终快排的时间复杂度会退化成O(n2)
总结:
- 归并排序的处理过程时由下到上的,是稳定的O(nlogn),但不是原地排序算法。
- 快排的处理过程是由上到下的,不是稳定的,且可能退化成O(n2)的,但可以是原地排序算法。
如何用快排思想在O(n)内找打无序数组中第k大元素?
package sort;
/**
* Description:在O(n)内找无序数组的第k大
*
* @author hanshuangwen
* @create 2021-06-01 22:04
*/
public class FindTopK {
public static int findTopK(int[] a, int p, int r, int k) {
int q = findPivot(a, p, r);
if (q + 1 == k) {
return q;
} else if (k > q + 1) {
return findTopK(a, q + 1, r, k);
} else {
return findTopK(a, p, q - 1, k);
}
}
private static int findPivot(int[] a, int p, int r) {
int pivot = a[r];
int i = p;
int j = p;
while (i <= r - 1 && j <= r - 1) {
if (a[j] < pivot) {//注意左分区是大于中间节点的,如果是找第K小,那么右分区应该是大于中间节点的
j++;
} else {
int temp = a[j];
a[j] = a[i];
a[i] = temp;
i++;
j++;
}
}
int temp = a[j];
a[j] = a[i];
a[i] = temp;
return i;
}
public static void main(String[] args) {
int[] a = {6, 1, 3, 5, 7, 2, 4, 9, 11, 8};
int topK = findTopK(a, 0, a.length - 1, 1);
System.out.println(a[topK]);
}
}
以最好的情况来看,当前每次分区都是划分成相同大小的两个区间的时候,则复杂度公式如下:
T(1)=1
T(2)=2
T(3)=4
...
T(n)=2的n-1次方
复杂度为O(n)