欠缺

数组:广义表

树:层次遍历算法,线索二叉树,二叉搜索(排序)树BST,自平衡二叉树AVL(左旋右旋),B树(多路平衡查找树),B+树,——红黑树

并查集:

图:

数组 Arrarry

C

  • 使用数组中的第 i + 1 个元素:1.数组名[i] ; 或者使用指针*p = &数组: *(p +i)

C 语言不允许返回一个完整的数组作为函数的参数。

想要从函数返回一个数组,需先声明一个返回指针的函数:

1
2
3
int * myFunction(){

}

C 不支持在函数外返回局部变量的地址,除非定义局部变量为 static 变量。

示例(生成随机数,并用数组返回):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int * getRandom( )
{
static int r[10];
int i;

/* 设置种子 */
srand( (unsigned)time( NULL ) );
for ( i = 0; i < 10; ++i){
r[i] = rand();
printf( "r[%d] = %d\n", i, r[i]);
}
return r;
}

/* 要调用上面定义函数的主函数 */
int main (){
/* 一个指向整数的指针 */
int *p;
int i;

p = getRandom();
for ( i = 0; i < 10; i++ ) {
printf( "*(p + %d) : %d\n", i, *(p + i));
}

return 0;
}

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.util.ArrayList;
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
//1.数组创建
//solution 1
int[] a = {1, 2, 3};
System.out.println("a: " + Arrays.toString(a));

//solution 2
int[] b = new int[]{1, 2, 3};
System.out.println("b: " + Arrays.toString(b));

//solution 3
int[] c = new int[3];
for (int i = 0; i < a.length; i++) {
c[i] = i + 1;
}
System.out.println("c: " + Arrays.toString(c));

//solution 4
ArrayList<Integer> arr = new ArrayList<>();
for (int i = 0; i < 3; i++) {
arr.add(i + 1);//add方法:括号内为(数组索引值,元素),索引值可以不加,则将元素插入到末尾
}
System.out.println("arr: " + arr.toString());

//2.添加元素
arr.add(99); //O(1)
arr.add(3, 88); //O(N)

//3.访问元素 : O(1)
int c1 = c[1];
int arr1 = arr.get(1); //()里为索引值

//4.更新元素 :O(1)
c[1] = 11;
arr.set(1, 11);

//5.删除元素 : O(N)
arr.remove(1); //删除下标为1处的元素
arr.remove(Integer.valueOf(4)); //删除第一个值为4的元素
for (int i = 0; i < arr.size(); i++) { //删除所有值为3的元素
if (arr.get(i) == 3) {
arr.remove(i--); //每删除一个元素后当前下标应-1,否则会漏掉下一个元素
}
}

//6.数组长度 :O(1)
int cSize = c.length;
int arrSize = arr.size();

//7.遍历数组
for (int i = 0; i < c.length; i++) {
int current = c[i];
System.out.println("c at index " + i + ": " + current);
}
for (int i = 0; i < arr.size(); i++) {
int current = arr.get(i);
System.out.println("c at index " + i + ": " + current);
}

//8.查找元素
for (int i = 0; i < c.length; i++) {
if (c[i] == 99) {
System.out.println("we found 99 at int c!");
}
}
boolean is99 = arr.contains(99); //找到目标值则返回True

//9. 数组排序 : O(N logN)
Arrays.sort(c);
Collections.sort(arr); //升序排列
Collections.sort(arr, Collections.reverseOrder()); //降序排列
}

数组的时间复杂度

访问(Access):O(1)

搜索(Search):O(N)

插入(Insert):O(N)

删除(Delete):O(N)

链表 Linked List

C

创建链表:

创建结构变量指针,使用前要先分配内存空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <stdlib.h>

typedef struct listNode{
int data;
struct listNode *next;
}LinkList;

//创建链表
LinkList *CreatList(int n){
LinkList *head = (LinkList*)malloc(sizeof(LinkList));
LinkList *temp = head;
for(int i = 1; i < n; ++i){
LinkList *a = (LinkList*)malloc(sizeof(LinkList));
scanf("%d", &a->data);
a->next = NULL;
temp->next = a;
temp = a;
}
return head;
}

//修改链表结点的值
LinkList changeList(LinkList *list, int n){
LinkList *temp = list;
int i = 0;
while(i < n && temp != NULL){
temp = temp->next;
i++;
}
if(temp != NULL){
printf("请输入要修改的值:");
scanf("%d", &temp->data);
}
else printf("结点不存在");
}

//向链表的第n个位置插入数据
LinkList *insert(LinkList *list, int n){
LinkList *temp = list; //temp用于表示插入位置的上一个结点
for(int i = 1; i < n; ++i){
if(temp ==NULL){
printf("插入位置无效\n");
return list;
}
temp = temp->next;
}
LinkList *in = (LinkList*)malloc(sizeof(LinkList));
scanf("%d",&in->data);
in->next = temp->next;
temp->next = in;
return list;
}

//删除链表中第n个节点
LinkList *delete(LinkList *list, int n){
LinkList *temp = list; //temp用于表示删除位置的上一个结点
for(int i = 0; i < n; ++i) {
temp = temp->next;
}
LinkList *del = temp->next;
temp->next = temp->next->next;
free(del);
return list;
}

//打印链表
void display(LinkList *list){
LinkList *temp = list;
while(temp->next){
temp = temp->next;
printf("%d",temp->data);
}
}

头插法和尾插法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include<stdio.h>
#include<stdlib.h>

struct list{
int data;
struct list *next;
};

int i;
void output(struct list *head);

//尾插法
void creat(struct list *head, int n){
struct list *p, *q;
head->next=NULL;
p=head;

for(i=0; i<n; i++){
q=(struct list*)malloc( sizeof(struct list) );
scanf("%d", &q->data);
q->next = p->next;
p->next=q;
p=q;
}
}
//头插法
void creat_1(struct list *head, int n){
struct list *p;
head->next=NULL;

for(i=0; i<n; i++){
p=(struct list*)malloc( sizeof(struct list) );
scanf("%d", &p->data);
p->next=head->next;
head->next=p;
}
}
void output(struct list *head){
struct list *p=head->next;
while(p){
printf("%d ", p->data);
p=p->next;
}
puts("\n\n");
}

void main(){
struct list *head;
head=(struct list*)malloc( sizeof(struct list) );
creat(head, 5);
output(head);

creat_1(head, 5);
output(head);
}

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.LinkedList;
public class Test {
public static void main(String[] args) {
//1.创建链表
LinkedList<Integer> list = new LinkedList<>();

//2.添加元素
list.add(1); //O(1)
list.add(2, 99); //O(N)

//3.访问元素
int element = list.get(2);

//4.搜索元素
int index = list.indexOf(99); //搜索元素值,然后返回索引值(下标)

//5.更新元素 : O(N)
list.set(2,88);

//6.删除元素 : O(N)
list.remove(2);

//7.长度
int length = list.size();
}
}

203.移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。

1
2
输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Java
//设置哨兵节点,便于删除头节点
public ListNode removeElements(ListNode head, int val) {
ListNode pre = new ListNode(0);
pre.next = head;
ListNode temp = pre;
while (temp.next != null) {
if (temp.next.val == val){
temp.next = temp.next.next;
}
else temp = temp.next;
}
return pre.next;
}
}
//递归
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
head.next = removeElements(head.next, val);
return head.val == val ? head.next : head;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//C
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
//设置哨兵节点,便于删除链表头部节点
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode sentinel = new ListNode(0); //sentinel:哨兵
sentinel.next = head;

ListNode prev = ans, curr = head;
while(curr != null){
if(curr.val == val){
prev.next = curr.next;
}else{
prev = curr;
}
curr = curr.next;
}
return sentinel.next;
}
}

206.反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

1
2
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Java
//迭代
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
//递归
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null)
return head;

ListNode temp = reverseList(head.next);
head.next.next = head;
head.next = null;
return head;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//C
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode prev = null, curr = head;
while(curr != null){
ListNode temp = curr.next;
curr.next = prev;
prev = curr;
curr = temp;
}
return prev;
}
}

链表的时间复杂度

访问(Access):O(N)

搜索(Search):O(N)

插入(Insert):O(1)

删除(Delete):O(1)

队列 Queue

C

判断队列是否为空:

  1. 设置一个标志变量;
  2. 当队列满时,保留一个元素空间;设队列最大尺寸为QueueSize,则队列满的条件是:(rear+1)%QueueSize == front。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <stdio.h>
#include <stdlib.h>

typedef int QuDataType;

typedef struct QListNode{
struct QListNode *_next;
QuDataType _data;
}QueueNode;

//队列的结构
typedef struct Queue{
QueueNode *_front;
QueueNode *_rear;
}Queue;

void QueueInit(Queue *q);//初始化队列

void QueuePush(Queue *q,QuDataType data);//队尾入队列

void QueuePop(Queue *q);//队头出队列

QuDataType QueueFront(Queue* q);// 获取队列头部元素

QuDataType QueueBack(Queue* q);// 获取队列队尾元素

int QueueSize(Queue* q);// 获取队列中有效元素个数

int QueueEmpty(Queue* q);// 检测队列是否为空,如果为空返回非零结果,如果非空返回0

void QueueDestroy(Queue* q);// 销毁队列

//一个数据入队列必须要先创建节点
QueueNode * BuyQueueNode(QuDataType x){ //创建节点并初始化此节点
QueueNode * cur = (QueueNode *)malloc(sizeof(QueueNode));
cur->_data = x;
cur->_next = NULL;
return cur;
}

void QueueInit(Queue* q){ //初始化队列结构
q->_front = NULL;
q->_rear = NULL;
}

void QueuePush(Queue* q, QuDataType x) { //队列尾部入数据
QueueNode * cur = BuyQueueNode(x); //先把创建好的节点传过来
if (q->_front == NULL){ //若是队列本身为空,队列里就只有这一个节点,又为队列头又为队列尾
q->_front = q->_rear = cur;
}
else{
q->_rear->_next = cur; //否则,链表尾插操作
q->_rear = cur;
}
}

void QueuePop(Queue* q) { //队列头部出数据
if (q->_front == NULL){ //本身队列为空,不做操作
return;
}
QueueNode* tmp = q->_front->_next; //先保留下一个节点,防止断链
free(q->_front);
q->_front = tmp; //更新对列头部
}

QuDataType QueueFront(Queue* q){ //获取队列首部元素
return q->_front->_data;
}

QuDataType QueueBack(Queue* q){//获取队列尾部元素
return q->_rear->_data;
}

int QueueEmpty(Queue* q){ //判断队列是否为空
return q->_front == NULL; //为空,返回1
}

int QueueSize(Queue* q){ //获取队列中的元素个数
QueueNode * cur;
int count = 0;
for (cur = q->_front; cur; cur = cur->_next)//循环遍历,计数即可
{
count++;
}
return count;
}

void QueueDestory(Queue* q) //销毁队列
{
if (q->_front == NULL){
return;
}
while (q->_front){
QueuePop(q);//对每一个元素迭代出队即可
}
}

int main(){
Queue q;
QueueInit(&q);
QueuePush(&q, 1);
QueuePush(&q, 2);
QueuePush(&q, 3);
QueuePush(&q, 4);

printf("%d\n", QueueFront(&q)); //1
QueuePop(&q);//出队列
printf("%d\n", QueueFront(&q));//2 此时队首为2

printf("%d\n", QueueBack(&q));//4 队尾为4

return 0;
}

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.queue;
import java.util.LinkedList;
public class Test {
public static void main(String[] args) {
//1.创建队列
//用LinkedList(链表)作为对象,插入和删除操作时间复杂度只有O(1)
Queue<Integer> queue = new LinkedList<>();

//2.添加元素 :O(1)
queue.add(1);
queue.add(2);

//3.获取即将出队的元素 :O(1)
int temp1 = queue.peek();
System.out.println(temp1);

//4.删除即将出队的元素 :O(1)
int temp2 = queue.poll(); //返回值,并将其删除
System.out.println(temp2);
System.out.println(queue.toString());

//5.判断队列是否为空 :O(1)
System.out.println(queue.isEmpty()); //队列空则返回True


//6.队列长度 :O(1)
//创建链表时会有一个变量,当加入一个元素时变量加1,获取长度时会直接返回这个变量值
System.out.println(queue.size());

//7.遍历队列 :O(N)
while(!queue.isEmpty()){
int temp = queue.poll();
System.out.print(temp);
}
}
}

933.最近的请求次数

写一个 RecentCounter 类来计算特定时间范围内最近的请求。

请你实现 RecentCounter 类:

RecentCounter() 初始化计数器,请求数为 0 。
int ping(int t) 在时间 t 添加一个新请求,其中 t 表示以毫秒为单位的某个时间,并返回过去 3000 毫秒内发生的所有请求数(包括新请求)。确切地说,返回在 [t-3000, t] 内发生的请求数。
保证 每次对 ping 的调用都使用比之前更大的 t 值。

1
2
3
4
5
输入:
["RecentCounter", "ping", "ping", "ping", "ping"]
[[], [1], [100], [3001], [3002]]
输出:
[null, 1, 2, 3, 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class RecentCounter {
Queue<Integer> q;
public void RecentCounter() {
q = new LinkedList();
}

public int ping(int t) {
q.add(t);
while(q.peek() < t - 3000){
q.poll();
}
return q.size();
}
}

225.用队列实现栈

利用两个队列 来实现栈的基本操作:进栈,显示栈顶元素,删除栈顶元素,判空f

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class MyStack{
Queue<Integer> queue1;
Queue<Integer> queue2;
/** Initialize your data structure here. */
public MyStack() {
queue1 = new LinkedList<Integer>();
queue2 = new LinkedList<Integer>();
}

/** Push element x onto stack. */
public void push(int x) {
queue2.add(x);
while (!queue1.isEmpty()){
queue2.add(queue1.poll());
}
Queue<Integer> temp = queue1;
queue1 = queue2;
queue2 = temp;
}

/** Removes the element on top of the stack and returns that element. */
public int pop() {
return queue1.poll();
}

/** Get the top element. */
public int top() {
return queue1.peek();
}

/** Returns whether the stack is empty. */
public boolean empty() {
return queue1.isEmpty();
}
}

622.设计循环队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

class MyCircularQueue {
int[] queue;
int index;
int rear;
int count;
int capacity;

public MyCircularQueue(int k) {
capacity = k;
queue = new int[k + 1];
index = 0;
rear = 0;
count = 0;
}

public boolean enQueue(int value) {
if (count == capacity) return false;

queue[rear] = value;
rear = (rear + 1) % capacity;
count++;
return true;
}

public boolean deQueue() {
if (count == 0) return false;
else {
index = (index + 1) % capacity;
count--;
return true;
}
}

public int Front() {
if (count == 0) return -1;
else {
return queue[index];
}
}

public int Rear() {
if (count == 0) return -1;
else {
return queue[(rear + capacity - 1) % capacity];
}
}

public boolean isEmpty() {
return count == 0;
}

public boolean isFull() {
return count == capacity;
}
}

Deque(双端队列)

Deque(java.util.Deque)接口代表着双向队列,意思就是可以从队列的两端增加或者删除元素,

  • deque 是“double ended queue(双端队列)”的缩写,通常读为“deck”。

Deque的实现:
既然Deque是个接口所以初始化时就要用到其具体的实现,在 Collections API中有下面两种实现:

1
2
java.util.LinkedList
java.util.ArrayDeque

LinkedList类是非常标准的Deque和Queue的实现,它在内部使用链接列表来建模queue或deque。

ArrayDeque类内部存储元素是数组,如果元素数超过数组中的空间,则分配一个新的数组,并移动所有元素,换句话说,ArrayDeque根据需要增长,即使它将元素存储在数组中。

创建Deque
在使用Deque之前首先要创建 Deque接口实现的实例,下面是创建 LinkedList实例:

1
Deque deque = new LinkedList();

创建 ArrayDeque实例:

1
Deque deque = new ArrayDeque();

Deque泛型
默认 Deque放入的Object对象,但是也可以用泛型:

1
Deque<MyObject> deque = new LinkedList<MyObject>();

这个Deque中只能添加 MyObject的实例对象,并且访问时不需要强制类型转换:

1
2
3
4
5
MyObject myObject = deque.remove();

for(MyObject anObject : deque){
//do someting to anObject...
}

Deque中添加元素
前面讲到可以在Deque 的两端增加元素,Deque 中有下面几种添加元素的方法:

add()
addLast()
addFirst()
offer()
offerFirst()
offerLast()

方法具体说明:

add()
可以使用add()方法在Deque 的尾部添加元素:

1
2
3
Deque<String> deque = new ArrayDeque<>();

deque.add("element 1");

如果元素不能插入到Deque,那么add(),方法将抛异常,而 offer()方法不一样,如果不能添加元素offer()方法将返回false。add()方法实际是继承Queue接口。

offer()
offer()方法可以在Deque的尾部添加元素,如果元素没满则添加成功返回true,否则返回false。这是和 add()抛异常方法不同的地方,下面是使用offer()方法:

1
2
3
Deque<String> deque = new ArrayDeque<>();

deque.offer("element 1");

push()
push()方法是在Deque的头部添加元素,如果Deque中的元素满了,则会抛异常,这和addFirst()方法比较相似:

1
2
3
Deque<String> deque = new LinkedList<>();

deque.push("element 0");

获取元素:

peek()
peekFirst()
peekLast()
getFirst()
getLast()
peek()
peek()返回Deque中的第一个元素并且不删除,如果Deque是空则返回null:

1
2
3
4
5
6
Deque<String> deque = new ArrayDeque<>();

deque.add("first element");
deque.add("last element");

String firstElement = deque.peek();

执行完代码后firstElement将指向Deque的第一个元素: “first element”。

getFirst()
getFirst()方法获取Deque的第一个元素并且不删除,如果Deque是空则抛异常:

1
2
3
4
5
6
7
Deque<String> deque = new ArrayDeque<>();

deque.add("first element");
deque.add("last element");


String firstElement = deque.getFirst();

执行完代码后firstElement的值是: “first element”。

移除Deque中的元素
以下几种方法可以移除Deque 中的元素:

remove()
removeFirst()
removeLast()
poll()
pollFirst()
pollLast()
remove()
remove()方法移除Deque中的第一个元素并返回:

1
2
3
4
5
Deque<String> deque = new LinkedList<>();

deque.add("element 0");

String removedElement = deque.remove();

如果Deque 是空则抛异常,这一点和poll()返回null不一样。

poll()
poll()方法移除Deque中的第一个元素,如果Deque为空则poll()返回null,这和remove()方法抛异常不一样:

1
2
3
4
5
6
7
Deque<String> deque = new LinkedList<>();

deque.add("element 0");
deque.add("element 1");
deque.add("element 2");

String removedElement = deque.poll();

pop()
pop()方法移除Deque的第一个元素,如果Deque是空则抛异常:

1
2
3
4
5
Deque<String> deque = new LinkedList<>();

deque.push("element 0");

String removedElement = deque.pop();

检查Deque是否包含某个元素
可以用contains()方法检查Deque中是否包含某个元素,如果包含返回true否则返回false:

1
2
3
4
5
6
Deque<String> deque = new ArrayDeque<>();

deque.add("first element");

boolean containsElement1 = deque.contains("first element");
boolean containsElement2 = deque.contains("second element");

执行完代码后containsElement1的值是true ,containsElement2值是false。

Deque的大小
Deque的size()方法可以返回Deque中存储的元素个数:

1
2
3
4
5
6
Deque<String> deque = new ArrayDeque<>();

deque.add("first element");
deque.add("second element");

int size = deque.size();

执行完代码后size大小是2,因为Deque中包含两个元素。

迭代Deque中的元素
可以通过两种方法迭代Deque中的元素:

使用Iterator.
使用for-each循环.
具体使用哪一种迭代取决Deque的实现。

通过迭代器迭代Deque
第一种方法是获取Deque的Iterator,下面是代码:

1
2
3
4
5
6
7
8
9
10
Deque<String> deque = new LinkedList<>();

deque.add("element 0");
deque.add("element 1");
deque.add("element 2");

Iterator<String> iterator = deque.iterator();
while(iterator.hasNext(){
String element = iterator.next();
}

通过For-Each循环迭代Deque
第二种方法是通过for-each循环迭代Deque:

1
2
3
4
5
6
7
8
9
Deque<String> deque = new LinkedList<>();

deque.add("element 0");
deque.add("element 1");
deque.add("element 2");

for(String element : deque) {
System.out.println(element);
}

队列的时间复杂度

访问(Access):O(N)

搜索(Search):O(N)

插入(Insert):O(1)

删除(Delete):O(1)

栈 Stack

java:

(Java堆栈Stack已经过时,推荐使用Deque替代Stack。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.Deque;
import java.util.LinkedList;
public class Test {
public static void main(String[] args) {
//创建栈
Deque<Integer> stack = new LinkedList<>();

//添加元素 : O(1)
stack.push(1);
stack.push(3);

//获取栈顶元素 : O(1)
stack.peek();

//删除栈顶元素 : O(1)
int temp = stack.pop(); //pop方法会删除元素并返回被删除的值

//栈的大小 : O(1)
stack.size();

//栈是否为空 : O(1)
stack.isEmpty();

//栈的遍历 : O(N)
while(!stack.isEmpty()){
int num = stack.pop();
System.out.println(num);
}
}
}

单调栈

单调栈中存放的数据应该是有序的,所以单调栈也分为单调递增栈和单调递减栈

  • 单调递增栈:单调递增栈就是从栈底到栈顶数据是从大到小
  • 单调递减栈:单调递减栈就是从栈底到栈顶数据是从小到大

假设:

现在有一组数10,3,7,4,12。从左到右依次入栈,则如果栈为空或入栈元素值小于栈顶元素值,则入栈;否则,如果入栈则会破坏栈的单调性,则需要把比入栈元素小的元素全部出栈。单调递减的栈反之。

10入栈时,栈为空,直接入栈,栈内元素为10。

3入栈时,栈顶元素10比3大,则入栈,栈内元素为10,3。

7入栈时,栈顶元素3比7小,则栈顶元素出栈,此时栈顶元素为10,比7大,则7入栈,栈内元素为10,7。

4入栈时,栈顶元素7比4大,则入栈,栈内元素为10,7,4。

12入栈时,栈顶元素4比12小,4出栈,此时栈顶元素为7,仍比12小,栈顶元素7继续出栈,此时栈顶元素为10,仍比12小,10出栈,此时栈为空,12入栈,栈内元素为12。

伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int stack()

for (遍历这个数组)
{
if (栈空 || 栈顶元素大于等于当前比较元素)
{
入栈;
}
else
{
while (栈不为空 && 栈顶元素小于当前元素)
{
栈顶元素出栈;
更新结果;
}
当前数据入栈;
}
}

20.有效的括号

给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。

示例 2:

1
2
输入:s = "()[]{}"
输出:true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;

class Solution {
public boolean isValid(String s) {
int n = s.length();
if (n % 2 == 1) {
return false;
}

HashMap<Character, Character> hash = new HashMap<Character, Character>();
hash.put(')', '(');
hash.put(']', '[');
hash.put('}', '{');

Deque<Character> stack = new LinkedList<Character>();
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
if (hash.containsKey(c)) {
if (stack.isEmpty() || stack.peek() != hash.get(c)) {
return false;
}
stack.pop();
} else
stack.push(c);
}
return stack.isEmpty();
}
}

496.下一个更大元素1

给定两个没有重复元素的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。

请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。

nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。

示例 :

1
2
3
4
5
6
输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
int n = nums1.length, m = nums2.length;
HashMap<Integer, Integer> map = new HashMap<>();
Deque<Integer> stack = new LinkedList<>();
for(int i = 0; i < m; i++){
while (!stack.isEmpty() && nums2[i] > stack.peek()){
map.put(stack.pop(),nums2[i]);
}
map.put(nums2[i],-1);
stack.push(nums2[i]);
}
for (int i = 0; i < n; i++){
nums1[i] = map.get(nums1[i]);
}
return nums1;
}
}

232.用栈实现队列

只使用两个栈,实现一个先入先出的队列(push,pop,peek,empty)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.Deque;
import java.util.LinkedList;

class MyQueue {
Deque<Integer> stack1;
Deque<Integer> stack2;

public MyQueue() {
stack1 = new LinkedList<>();
stack2 = new LinkedList<>();
}

//stack1中的数据依次出栈并入栈stack2
//数据进栈stack2
//stack2中数据依次出栈并入栈stack1
public void push(int x) {
while (!stack1.isEmpty()){
stack2.push(stack1.pop());
}
stack2.push(x);
while (!stack2.isEmpty()){
stack1.push(stack2.pop());
}
}

public int pop() {
return stack1.pop();
}

public int peek() {
return stack1.peek();
}

public boolean empty() {
return stack1.isEmpty();
}
}

栈的时间复杂度

访问(Access):O(1)

搜索(Search):O(N)

插入(Insert):O(1)

删除(Delete):O(1)

字符串 String

字符串输入:

  1. 数组

    1
    2
    char arr[6] = "array";
    char arr[] = "array";
  2. 指针

    1
    2
    char *p = "pointer";
    printf("%s %c %c",p,*p,*(p + 2)); //第一个输出字符串;第二个输出指针指向,即字符串的第一个字符;第三个输出字符串中 第三个字符
  3. 数组/指针

    1
    2
    3
    4
    5
    6
    7
    char arr1[6];
    scang("%s\n",arr1);
    gets(arr1);

    char *arr2 = (char*)malloc( 100*sizeof(char) ); //记得为指针数组分配内存,否则会报错之类的
    scanf("%s\n",arr2);
    gets(arr2);

    (scanf只能输入一个单词,当遇到空格时会认为字符串结束;gets函数只知道数组的开始,并不知道数组的大小,如果输入过长容易溢出,并可能擦掉程序中的其他数据)

  4. 使用while完成多组测试,而不是在输入完一组数据后退出控制台

    1
    2
    3
    4
    5
    char arr1[100],arr2[2];
    int a;
    while((a = scanf("%s%s",arr1,arr2)) != EOF){ //也可用while((a = getchar(arr1)) != '\n')
    pritnf("scanf返回l %d,s1是%s,s2是%s",a,arr1,arr2); //这行输出后可以继续进行输入
    }

KMP模式匹配

KMP算法实现过程接近人为模式匹配

  • 匹配失败后,模式串移动的距离和主串无关,只与模式串本身有关,模式串后移等价于指针j前移。

模式串后移等价于 j 前移

因此,我们可以给每个模式串配备一个数组(例如 next[]),用于存储模式串中每个字符对应指针 j 重定向的位置(也就是存储模式串的数组下标),比如 j=3,则该字符匹配失败后指针 j 指向模式串中第 3 个字符。

模式串中各字符对应 next 值的计算方式是,取该字符前面的字符串(不包含自己),其前缀字符串和后缀字符串相同字符的最大个数再 +1 就是该字符对应的 next 值。

(模式串中第一个字符对应的值为 0,第二个字符对应 1 ,这是固定不变的。)

Next函数

一般的Next函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void Next(char*T,int *next){
next[1]=0;
next[2]=1;
int i=2;
int j=1;
while (i<strlen(T)) {
if (j==0||T[i-1]==T[j-1]) {
i++;
j++;
next[i]=j;
}else{
j=next[j];
}
}
}

但这样可能会有多余的操作,如:

Next函数的缺陷

当匹配失败时,Next 函数会由图 10b) 开始继续进行模式匹配,但是从图中可以看到,这样做是没有必要的。

如此,问题在当 T[i-1]==T[j-1] 成立时,没有继续对 i++ 和 j++ 后的 T[i-1] 和 T[j-1] 的值做判断。

因此改进后的Next函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void Next(char*T,int *next){ 
next[1]=0;
next[2]=1;
int i=2;
int j=1;
while (i<strlen(T)) {
if (j==0||T[i-1]==T[j-1]) {
i++;
j++;
if (T[i-1]!=T[j-1]) {
next[i]=j;
}else{
next[i]=next[j];
}
}else{
j=next[j];
}
}
}

KMP实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 此处next数组下标0chu
void Next(char[] T, int[] next) {
next[1] = 0;
int i = 1;
int j = 0;
while (i < T.length) {
if (j == 0 || T[i - 1] == T[j - 1]) {
i++;
j++;
if (T[i - 1] != T[j - 1]) {
next[i] = j;
} else {
next[i] = next[j];
}
} else {
j = next[j];
}
}
}

int KMP(char[] S, char[] T) {
int[] next = new int[T.length + 1];
Next(T, next); //根据模式串T,初始化next数组
int i = 1;
int j = 1;
while (i <= S.length && j <= T.length) {
//j==0:代表模式串的第一个字符就和当前测试的字符不相等;S[i-1]==T[j-1],如果对应位置字符相等,两种情况下,指向当前测试的两个指针下标i和j都向后移
if (j == 0 || S[i - 1] == T[j - 1]) {
i++;
j++;
} else {
j = next[j];//如果测试的两个字符不相等,i不动,j变为当前测试字符串的next值
}
}
if (j > T.length) {//如果条件为真,说明匹配成功
return i - T.length - 1; // 返回下标
}
return -1;
}

散列表(Hash)

  • 哈希表(hashtable),又叫散列表,是根据关键码(key)来进行访问的数据结构,通过哈希函数把key转成哈希值来定位数据存储的位置。通常是键-值对(即key-value)的结构,即用key算出的哈希值来定位存储位置,在该位置上存储内容为value。

  • 哈希集合(hashset),也是一种通过哈希值来定位存储位置的数据结构,只是它不是键-值对结构,而是储存key本身,相当于只有哈希表(hashtable)的key部分,即用key算出的哈希值来定位存储位置,在该位置上存储内容为key本身。
    简单来说就是,哈希集合是不能存储重复元素的数据结构(集合),而哈希表是存储键-值对(key-value),其中键key不能重复。

  • 哈希映射(hashmap)是和哈希表(hashtable)相似的数据结构,也是键-值对存储,只是哈希映射是线程安全的,而哈希表是非线程安全的。所谓线程安全,就是多线程同时操作数据的时候,能确保在同一时刻只能有一个线程能访问同一个数据(也就是会给数据操作加锁);如果不能确保这个,就是非线程安全。

哈希表 可能存在特殊情况,就是通过不同的 Key,可能访问到同一个地址,这种现象叫作碰撞(Collision)。而通过某个 Key 一定会得到唯一的 Value 地址。

目前,这个哈希函数比较常用的实现方法比较多,通常需要考虑几个因素:关键字的长度、哈希表的大小、关键字的分布情况、记录的查找频率,等等。

下面简单介绍几种哈希函数。

  • 直接寻址法:取关键字或关键字的某个线性函数值为散列地址。
  • 数字分析法:通过对数据的分析,发现数据中冲突较少的部分,并构造散列地址。例如同学们的学号,通常同一届学生的学号,其中前面的部分差别不太大,所以用后面的部分来构造散列地址。
  • 平方取中法:当无法确定关键字里哪几位的分布相对比较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为散列地址。这是因为:计算平方之后的中间几位和关键字中的每一位都相关,所以不同的关键字会以较高的概率产生不同的散列地址。
  • 取随机数法:使用一个随机函数,取关键字的随机值作为散列地址,这种方式通常用于关键字长度不同的场合。
  • 除留取余法:取关键字被某个不大于散列表的表长 n 的数 m 除后所得的余数 p 为散列地址。这种方式也可以在用过其他方法后再使用。该函数对 m 的选择很重要,一般取素数或者直接用 n。

对散列表函数产生冲突的解决办法:

​ 有时不同的 Key 通过哈希函数可能会得到相同的地址,这在我们操作时可能会对数据造成覆盖、丢失。之所以产生冲突是由于哈希函数有时对不同的 Key 计算之后获得了相同的地址。

冲突的处理方式也有很多,下面介绍几种。

  • 开放地址法(也叫线性探测):实际上就是当需要存储值时,对Key哈希之后,发现这个地址已经有值了,这时该怎么办?不能放在这个地址,不然之前的映射会被覆盖。这时对计算出来的地址进行一个探测再哈希,比如往后移动一个地址,如果没人占用,就用这个地址。如果超过最大长度,则可以对总长度取余。这里移动的地址是产生冲突时的增列序量。(如 f(key) = { f(key + d) } % m)
  • 再哈希法:在产生冲突之后,使用关键字的其他部分继续计算地址,如果还是有冲突,则继续使用其他部分再计算地址。这种方式的缺点是时间增加了。
  • 链地址法:链地址法其实就是对Key通过哈希之后落在同一个地址上的值,做一个链表。其实在很多高级语言的实现当中,也是使用这种方式处理冲突的,我们会在后面着重学习这种方式。
  • 建立一个公共溢出区:这种方式是建立一个公共溢出区,当地址存在冲突时,把新的地址放在公共溢出区里。

散列表实现

java

C:

实现简单的哈希表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <stdio.h>
#include <stdlib.h>
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 //定义散列表长为数组的长度
#define NULLKEY -32768
typedef int Status;

typedef struct {
int *elem; // 数据元素存储基址,动态分配数组
int count; // 当前数据元素个数
} HashTable;

int m = 0; //散列表表长,全局变量

//初始化散列表
Status InterHashTable(HashTable *H) {
int i;
m = HASHSIZE;
H->count = m;
H->elem = (int *) malloc(m * sizeof(int));
for (i = 0; i < m; i++) {
H->elem[i] = NULLKEY;
}
return 1;
}

//散列函数
int Hash(int key) {
return key % m; //除留余数法
}

//插入关键字进散列表
void IntertHash(HashTable *H, int key) {
int addr = Hash(key); //求散列地址
while (H->elem[addr] != NULLKEY) //如果不为空,则冲突
addr = (addr + 1) % m; //开放地址法的线性探测
H->elem[addr] = key; //直到有空位后插入关键字
}

//散列表查找关键字
Status SearchHash(HashTable H, int key, int *addr) {
*addr = Hash(key); //求散列地址
while (H.elem[*addr] != key) { //如果不为空,则冲突
*addr = (*addr + 1) % m; //开放地址法的线性探测
if (H.elem[*addr] == NULLKEY || *addr == Hash(key)) { //如果循环回到原点
return UNSUCCESS; //则说明关键字不存在
}
}
return SUCCESS;
}

int main() {
int a[12] = {12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34};
HashTable H;
int i;
InterHashTable(&H);
for (i = 0; i < m; i++)
IntertHash(&H, a[i]);
printf("插入之后的哈希表为:");
for (i = 0; i < m; i++)
printf("%d,", H.elem[i]);
int addr, j;
j = SearchHash(H, a[5], &addr);
printf("搜索到a[5]的地址是:%d", j);
}

实现哈希集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

struct List {
int val;
struct List *next;
};
void listPush(struct List *head, int x) {
struct List *tmp = malloc(sizeof(struct List));
tmp->val = x;
tmp->next = head->next;
head->next = tmp;
}
void listDelete(struct List *head, int x) {
for (struct List *it = head; it->next; it = it->next) {
if (it->next->val == x) {
struct List *tmp = it->next;
it->next = tmp->next;
free(tmp);
break;
}}}
bool listContains(struct List *head, int x) {
for (struct List *it = head; it->next; it = it->next) {
if (it->next->val == x) return true;
}
return false;
}
void listFree(struct List *head) {
while (head->next) {
struct List *tmp = head->next;
head->next = tmp->next;
free(tmp);
}
}
const int base = 769;

int hash(int key) {
return key % base;
}

typedef struct {
struct List *data;
} MyHashSet;

MyHashSet *myHashSetCreate() {
MyHashSet *ret = malloc(sizeof(MyHashSet));
ret->data = malloc(sizeof(struct List) * base);
for (int i = 0; i < base; i++) {
ret->data[i].val = 0;
ret->data[i].next = NULL;
}
return ret;
}
void myHashSetAdd(MyHashSet *obj, int key) {
int h = hash(key);
if (!listContains(&(obj->data[h]), key)) {
listPush(&(obj->data[h]), key);
}
}
void myHashSetRemove(MyHashSet *obj, int key) {
int h = hash(key);
listDelete(&(obj->data[h]), key);
}
bool myHashSetContains(MyHashSet *obj, int key) {
int h = hash(key);
return listContains(&(obj->data[h]), key);
}
void myHashSetFree(MyHashSet *obj) {
for (int i = 0; i < base; i++) {
listFree(&(obj->data[i]));
}
free(obj->data);
}

实现哈希映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
struct List {
int key;
int val;
struct List* next;
};

void listPush(struct List* head, int key, int val) {
struct List* tmp = malloc(sizeof(struct List));
tmp->key = key;
tmp->val = val;
tmp->next = head->next;
head->next = tmp;
}

void listDelete(struct List* head, int key) {
for (struct List* it = head; it->next; it = it->next) {
if (it->next->key == key) {
struct List* tmp = it->next;
it->next = tmp->next;
free(tmp);
break;
}
}
}

struct List* listFind(struct List* head, int key) {
for (struct List* it = head; it->next; it = it->next) {
if (it->next->key == key) {
return it->next;
}
}
return NULL;
}

void listFree(struct List* head) {
while (head->next) {
struct List* tmp = head->next;
head->next = tmp->next;
free(tmp);
}
}

const int base = 769;

int hash(int key) {
return key % base;
}

typedef struct {
struct List* data;
} MyHashMap;

MyHashMap* myHashMapCreate() {
MyHashMap* ret = malloc(sizeof(MyHashMap));
ret->data = malloc(sizeof(struct List) * base);
for (int i = 0; i < base; i++) {
ret->data[i].key = 0;
ret->data[i].val = 0;
ret->data[i].next = NULL;
}
return ret;
}

void myHashMapPut(MyHashMap* obj, int key, int value) {
int h = hash(key);
struct List* rec = listFind(&(obj->data[h]), key);
if (rec == NULL) {
listPush(&(obj->data[h]), key, value);
} else {
rec->val = value;
}
}

int myHashMapGet(MyHashMap* obj, int key) {
int h = hash(key);
struct List* rec = listFind(&(obj->data[h]), key);
if (rec == NULL) {
return -1;
} else {
return rec->val;
}
}

void myHashMapRemove(MyHashMap* obj, int key) {
int h = hash(key);
listDelete(&(obj->data[h]), key);
}

void myHashMapFree(MyHashMap* obj) {
for (int i = 0; i < base; i++) {
listFree(&(obj->data[i]));
}
free(obj->data);
}

java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.HashMap;
class Test {
public static void main(String[] args) {
//创建哈希表
String[] hashTable = new String[4]; //用数组创建
HashMap<Integer, String> map = new HashMap<>(); //用HashMap方法

//添加元素 : O(1)
hashTable[1] = "hello";
map.put(1, "hello");

//更新元素 : O(1)
hashTable[1] = "world";
map.put(1, "world");

//删除元素 : O(1)
hashTable[1] = ""; //将其改为不会用到的值
map.remove(1);

//获取元素 : O(1)
String temp = hashTable[1];
map.get(3);

//检查key是否存在 : O(1)
map.containsKey(3);

//长度,是否为空 : O(1)
map.size();

map.isEmpty();

//获取指定key对应的value,如果找不到key,则返回设置的默认值。

}
}

散列表的特点

散列表有两种用法:一种是 Key 的值与 Value 的值一样,一般我们称这种情况的结构为 Set(集合);而如果 Key 和 Value 所对应的内容不一样时,那么我们称这种情况为 Map,也就是人们俗称的键值对集合。

根据散列表的存储结构,我们可以得出散列表的以下特点。

  1. 访问速度很快

由于散列表有散列函数,可以将指定的 Key 都映射到一个地址上,所以在访问一个 Key(键)对应的 Value(值)时,根本不需要一个一个地进行查找,可以直接跳到那个地址。所以我们在对散列表进行添加、删除、修改、查找等任何操作时,速度都很快。

  1. 需要额外的空间

首先,散列表实际上是存不满的,如果一个散列表刚好能够存满,那么肯定是个巧合。而且当散列表中元素的使用率越来越高时,性能会下降,所以一般会选择扩容来解决这个问题。另外,如果有冲突的话,则也是需要额外的空间去存储的,比如链地址法,不但需要额外的空间,甚至需要使用其他数据结构。

这个特点有个很常用的词可以表达,叫作“空间换时间”,在大多数时候,对于算法的实现,为了能够有更好的性能,往往会考虑牺牲些空间,让算法能够更快些。

3) 无序

散列表还有一个非常明显的特点,那就是无序。为了能够更快地访问元素,散列表是根据散列函数直接找到存储地址的,这样我们的访问速度就能够更快,但是对于有序访问却没有办法应对。

4) 可能会产生碰撞

没有完美的散列函数,无论如何总会产生冲突,这时就需要采用冲突解决方案,这也使散列表更加复杂。通常在不同的高级语言的实现中,对于冲突的解决方案不一定一样。

散列表的适用场景

根据散列表的特点可以想到,散列表比较适合无序、需要快速访问的情况。

缓存

通常我们开发程序时,对一些常用的信息会做缓存,用的就是散列表,比如我们要缓存用户的信息,一般用户的信息都会有唯一标识的字段,比如 ID。这时做缓存,可以把 ID 作为 Key,而 Value 用来存储用户的详细信息,这里的 Value 通常是一个对象(高级语言中的术语,前面提到过),包含用户的一些关键字段,比如名字、年龄等。

在我们每次需要获取一个用户的信息时,就不用与数据库这类的本地磁盘存储交互了(其实在大多数时候,数据库可能与我们的服务不在一台机器上,还会有相应的网络性能损耗),可以直接从内存中得到结果。这样不仅能够快速获取数据,也能够减轻数据库的压力。

有时我们要查询一些数据,这些数据与其他数据是有关联的,如果我们进行数据库的关联查询,那么效率会非常低,这时可以分为两部分进行查询:将被关联的部分放入散列表中,只需要遍历一遍;对于另一部分数据,则通过程序手动关联,速度会很快,并且由于我们是通过散列表的 Key、Value 的对应关系对应数据的,所以性能也会比较好。

快速查找

这里说的查找,不是排序,而是在集合中找出是否存在指定的元素。

这样的场景很多,比如我们要在指定的用户列表中查找是否存在指定的用户,这时就可以使用散列表了。在这个场景下使用的散列表其实是在上面提到的 Set 类型,实际上不需要 Value 这个值。

还有一个场景,我们一般对网站的操作会有个IP地址黑名单,我们认为某些 IP 有大量的非法操作,于是封锁了这些 IP 对我们网站的访问。这个 IP 是如何存储的呢?就是用的散列表。当一个访问行为发送过来时,我们会获取其 IP,判断其是否存在于黑名单中,如果存在,则禁止其访问。这种情况也是使用的 Set。

当然,对于上面说的两个例子,用列表也是可以实现的,但是访问速度会受到很大的影响,尤其是列表越来越长时,查找速度会很慢,而散列表则不会。

217.存在重复元素

给定一个整数数组,判断是否存在重复元素。

如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。

1
2
3
4
5
6
7
8
9
10
11
public boolean containsDuplicate(int[] nums) {
HashMap<Integer, Integer> hash = new HashMap<>();
int n = nums.length;
for (int i : nums) {
if (hash.containsKey(i)) {
return true;
} else
hash.put(i, 1);
}
return false;
}

389.找不同

给定两个字符串 s 和 t,它们只包含小写字母。

字符串 t 由字符串 s 随机重排,然后在随机位置添加一个字母。

请找出在 t 中被添加的字母。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public char findTheDifference(String s, String t) {
int[] cnt = new int[26];
int n = s.length(), m = t.length();
for(int i = 0; i < n; i++){
cnt[s.charAt(i) - 'a']++;
}
for(int i = 0; i < m; i++){
char c = t.charAt(i);
cnt[c - 'a']--;
if (cnt[c - 'a'] < 0)
return c;
}
return ' ';
}
}

哈希表的时间复杂度

访问(Access):

搜索(Search):O(1)

插入(Insert):O(1)

删除(Delete):O(1)

集合 Set

  • 无序,不重复
  1. 检查元素是否存在
  2. 检查元素是否重复(原数据长度与集合长度是否相同)

java(哈希集合HashSet)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.HashSet;

public class Test {
public static void main(String[] args) {
//创建
HashSet<Integer> set = new HashSet<>();

//添加元素 : O(1)
set.add(10);
set.add(2);
set.add(2);
//[10,2]
System.out.println(set.add(2)); //添加的元素已存在,会返回false,不存在则返回true

//搜索元素 : O(1)0
set.contains(2); //存在则返回true
System.out.println(set.contains(2));
//删除元素 : O(1)
set.remove(2);
System.out.println(set.toString());

//长度
set.size();
}
}

217.存在重复元素

给定一个整数数组,判断是否存在重复元素。

如果存在一值在数组中出现至少两次,函数返回 true 。如果数组中每个元素都不相同,则返回 false 。

1
2
输入: [1,2,3,1]
输出: true
1
2
3
4
5
6
7
8
9
class Solution {  
public boolean containsDuplicate(int[] nums){
Set<Integer> set = new HashSet<>();
for(int x : nums){
if(!set.add(x)) return true;
}
return false;
}

树 Tree

  • 树中有一个根结点,其余节点可以分成若干个互不相交的“子树”。

  • 除了根节点,每个结点有且只有一个父节点;

  • 一棵N个结点的树有N - 1条边;

结点的度:结点的子树个数;

树的度:树中所有结点中最大的度;

叶结点:度为0的结点;

兄弟结点:具有同一父结点的各个结点;

路径和路径结点:从结点N到结点Nk的路径,其路径结点为路径中结点个数 - 1,即路径中所包含的边的个数;

祖先结点:从某一结点,到根结点的所有节点,都是这个结点的祖先节点;

子孙结点:某一结点的子树中的所有节点,都是这个结点的子孙;

层次:规定根结点 在0/1层,其他结点的层数是其父结点的层数+1;

深度:从上往下数,等于层;高度:从下往上数

二叉树:

普通二叉树:每个结点最多两个子结点

满二叉树:除了叶结点,每个结点都有两个子结点,并且所有叶结点都在同一层上;

满二叉树的深度为k,则该树一共有 2^k^ - 1 个节点。

完全二叉树:从树的根结点出发,从上到下,从左到右,依次填满结点形成的二叉树

(满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树)

二叉树的遍历:

前序遍历 根节点->左子树->右子树
中序遍历 左子树->根节点->右子树
后序遍历 左子树->右子树->根子树
层次遍历 从上到下,从左到右

(层次遍历需要用到 队列 )

二叉树实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#include <stdio.h>
#include <stdlib.h>

typedef struct node { //创建树的结点
int data;
struct node *left;
struct node *right;
} Node;

typedef struct { //创建树根
Node *root;
} Tree;

void insert(Tree *tree, int value) { //创建树
Node *node = (Node *) malloc(sizeof(Node));//创建一个结点
node->data = value;
node->left = NULL;
node->right = NULL;
if (tree->root == NULL) tree->root = node; //判断树是不是空树
else { //不是空树
Node *temp = tree->root; //从树根开始
while (temp != NULL) {
if (value < temp->data){ //小于就进左儿子
if (temp->left == NULL) {
temp->left = node;
return;
}
else { //继续判断
temp = temp->left;
}
}
else { //否则进右儿子
if (temp->right == NULL) {
temp->right = node;
return;
}
else { //继续判断
temp = temp->right;
}
}
}
}
return;
}
//树的中序遍历
void inorder(Node *node){
if(node != NULL){
inorder(node->left);
printf("%d",node->data);
inorder(node->right);
}
}
//树的前序遍历
void inorder(Node *node){
if(node == NULL){
return;
}
printf("%d",node->data);
inorder(node->left);
inorder(node->right);
}
//树的后序遍历
void inorder(Node *node){
if(node == NULL){
return;
}
inorder(node->left);
inorder(node->right);
printf("%d",node->data);
}

//删除结点;
void Deleteer(Node *node, int value)
{
Node *L,*LL; //在删除左右子树都有的结点时使用;
Node *p=node;
Node *parent=node;
int child=0; //0表示左子树,1表示右子树;
if(!node) //如果排序树为空,则退出;
return ;
while(p) //二叉排序树有效;
{
if(p->data == value)
{
if(!p->left && !p->right) //叶结点(左右子树都为空);
{
if(p == node) //被删除的结点只有根结点;
free(p);
else if(child==0)
{
parent->left=NULL; //设置父结点左子树为空;
free(p); //释放结点空间;
}
else //父结点为右子树;
{
parent->right=NULL; //设置父结点右子树为空;
free(p); //释放结点空间;
}
}

else if(!p->left) //左子树为空,右子树不为空;
{
if(child==0) //是父结点的左子树;
parent->left=p->right;
else //是父结点的右子树;
parent->right=p->right;
free(p); //释放被删除的结点;
}

else if(!p->right) //右子树为空,左子树不为空;
{
if(child==0) //是父结点的左子树;
parent->left=p->left;
else //是父结点的右子树;
parent->right=p->left;
free(p); //释放被删除的结点;
}

else
{
LL=p; //保存左子树的结点;
L=p->right; //从当前结点的右子树进行查找;
if(L->left) //左子树不为空;
{
LL=L;
L=L->left; //查找左子树;
p->data=L->data; //将左子树的数据保存到被删除结点;
LL->left=L->left; //设置父结点的左子树指针为空;
for(; L->left; L=L->left);
L->left=p->left;
p->left=NULL;
}
else
{
p->data=L->data;
LL->right=L->right;
}
}
p=NULL;
}

else if(value < p->data) //需删除记录的关键字小于结点的数据;
{
//要删除的结点p是parent的左子树;
child=0; //标记在当前结点左子树;
parent=p;//保存当前结点作为父结点;
p=p->left; //查找左子树;
}

else //需删除记录的关键字大于结点的数据;
{
//要删除的结点p是parent的右子树;
child=1; //标记在当前结点右子树查找;
parent=p; //保存当前结点作为父结点;
p=p->right; //查找右子树;
}
}
}

int main() {
Tree tree;
tree.root = NULL; //创建一个空树
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++) { //输入n个数并创建这个树
int temp;
scanf("%d", &temp);
insert(&tree, temp);
}
inorder(tree.root);
Deleteer(tree.root,5);
return 0;
}

线索二叉树

二叉搜索树BST

二又排序树、二叉查找树(BST, Binary Search Tree)
是一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:

  • 左子树上所有结点的关键字均小于根结点的关键字
  • 右子树上所有结点的关键字均大于根结点的关键字
  • 左子树和右子树又各是一棵二叉排序树。

左子树结点值 < 根结点值 < 右子树结点值;

进行中序遍历,可以得到一个递增的有序序列。

查找:时间复杂度 O(h),若排序树很平衡,则h接近log2n,若非常不平衡,则h会接近n。

插入:时间复杂度 O(h)

若原二叉排序树为空,则直接插入结点;否则,若关键字k小于 根结点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树。

删除:

先搜索找到目标结点:

  • ①若被删除结点z是叶结点,则直接删除,不会破坏二又排序树的性质。
  • ②若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。
  • ③若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。

自平衡二叉树AVL

平衡二叉树( Balanced Binary Tree),简称平衡树(AVL树)ー一树上任一结点的左子树和右子树的高度之差不超过1。

结点的平衡因子 = 左子树高 - 右子树高。

  • 平衡二叉树结点的平衡因子的值只可能是-1、0或1。否则就不是平衡二叉树了。

查找:时间复杂度不超过O(h) 、O(log2n)

插入:

image-20211228111813606

调整最小不平衡子树A(从插入点找到的第一个不平衡点为根的子树):

  • LL:在A的左孩子的左子树中插入导致不平衡;
    调整:A的左孩子结点右上旋;

  • RR :在A的右孩子的右子树中插入导致不平衡;

    调整:A的右孩子结点左上旋;

  • LR :在A的左孩子的右子树中插入导致不平衡;
    调整:A的左孩子的右孩子先左上旋再右上旋;

  • RL :在A的右孩子的左子树中插入导致不平衡;
    调整:A的右孩子的左孩子先右上旋后左上旋;

哈夫曼树

结点的带权路径长度:

树的每一个结点,都可以拥有自己的“权重”(Weight),权重在不同的算法当中可以起到不同的作用。结点的带权路径长度,是指树的根结点到该结点的路径长度,和该结点权重的乘积。

树的带权路径长度

在一棵树中,所有叶子结点的带权路径长度之和,被称为树的带权路径长度,也被简称为WPL。

而哈夫曼树(Huffman Tree)是在叶子结点和权重确定的情况下,带权路径长度最小的二叉树,也被称为最优二叉树。

一颗有n个叶子结点的的哈夫曼树共有2n-1个结点

原则上,我们应该让权重小的叶子结点远离树根,权重大的叶子结点靠近树根。

思路:

借助辅助队列,将权值最小的两个叶子节点挑出来,并根据这两个结点生成一个新的父结点,父节点的权值是这两个结点权值之和;从队列中移除已选择的节点,把新的父结点加入队列;

重复这一步骤,直到所有结点组成一颗完整二叉树

(如果是构造k叉哈夫曼树,且节点数无法严格构成k叉树,则需要补充几个权值为0的 “假节点”)

202204201806626 202204201806756 202204201807079
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#include <cstdio>
#include <cstring>

using namespace std;

typedef struct {
int weight; // 结点权值?
int parent, lc, rc; // 双亲结点和左 右子节点
} HTNode, *HuffmanTree;

void Select(HuffmanTree &HT, int n, int &s1, int &s2) {
int minum; // 定义一个临时变量保存最小值?
for (int i = 1; i <= n; i++){ // 以下是找到第一个最小值
if (HT[i].parent == 0) {
minum = i;
break;
}
}
for (int i = 1; i <= n; i++) {
if (HT[i].parent == 0)
if (HT[i].weight < HT[minum].weight)
minum = i;
}
s1 = minum;
// 以下是找到第二个最小值,且与第一个不同
for (int i = 1; i <= n; i++) {
if (HT[i].parent == 0 && i != s1) {
minum = i;
break;
}
}
for (int i = 1; i <= n; i++) {
if (HT[i].parent == 0 && i != s1)
if (HT[i].weight < HT[minum].weight)
minum = i;
}
s2 = minum;
}

void CreatHuff(HuffmanTree &HT, int *w, int n) {
int m, s1, s2;
m = n * 2 - 1; // 总结点的个数
HT = new HTNode[m + 1]; // 分配空间
for (int i = 1; i <= n; i++){ // 1 - n 存放叶子结点,初始化
HT[i].weight = w[i];
HT[i].parent = 0;
HT[i].lc = 0;
HT[i].rc = 0;
}
for (int i = n + 1; i <= m; i++) { // 非叶子结点的初始化
HT[i].weight = 0;
HT[i].parent = 0;
HT[i].lc = 0;
HT[i].rc = 0;
}
printf("\nthe HuffmanTree is: \n");
for (int i = n + 1; i <= m; i++) // 创建非叶子节点,建哈夫曼树
{ // 在HT[1]~HT[i-1]的范围内选择两个parent为0且weight最小的两个结点,其序号分别赋值给 s1 s2
Select(HT, i - 1, s1, s2);
HT[s1].parent = i; // 删除这两个结点
HT[s2].parent = i;
HT[i].lc = s1; // 生成新的树,左右子节点是 s1和s2
HT[i].rc = s2;
HT[i].weight = HT[s1].weight + HT[s2].weight; // 新树的权�?
printf("%d (%d, %d)\n", HT[i].weight, HT[s1].weight, HT[s2].weight);
}
printf("\n");
}

int main() {
HuffmanTree HT;

int *w, n, wei;
printf("input the number of node\n");
scanf("%d", &n);
w = new int[n + 1];
printf("\ninput the %dth node of value\n", n);

for (int i = 1; i <= n; i++) {
scanf("%d", &wei);
w[i] = wei;
}
CreatHuff(HT, w, n);
return 0;
}

哈夫曼编码

哈夫曼编码:对一棵具有n个叶子的哈夫曼树,若对树中的每个左分支赋0,右分支赋1,则从根到每个叶子的路径上,各分支的赋值分别构成一个二进制串,该二进制串就称为哈夫曼编码。

哈夫曼编码的基本思想是:为出现次数较多的字符编以较短的编码,在压缩原理中有重要作用。

B树

B树,又称多路平衡查找树。

B树中所有结点的孩子个数的最大值称为B树的阶,通常用m表示。一棵m阶B树或为空树,或为满足如下特性的m叉树:

  1. 树中每个结点至多有m棵子树,即至多含有m-1个关键字
  2. 若根结点不是终端结点,则至少有两棵子树。
  3. 除根结点外的所有非叶结点至少有 m/2 棵子树,即至少含有 (m/2) -1 个关键字。
  4. 所有的叶结点都出现在同一层次上,并且不帯信息(可以视为外部结点或类似于折半查找判定树的查找失败结点,实际上这些结点不存在,指向这些结点的指针为空)。

image-20211227205412747

m阶B树的核心特性

  1. 根节点的子树数∈[2,m], 关键字数∈[1, m-1]
    其他结点的子树数∈ [(m/2), m] ;关键字数∈[(m/2)-1, m-1]
  2. 对任一结点,其所有子树高度都相同
  3. 关键字的值:子树0 < 关键字1 < 子树1 <关键字2 < 子树2 <. (类比二叉査找树左<中<右)

注:算B树高度时,不用包括叶子节点/失败节点;

n个关键字的B树必有n+1个叶子节点。(相当于在n个关键字中插入木板,两边也插,则插了n+1个木板)

B树的插入

新元素一定是插入到最底层“终端节点”,用“查找”来确定插入位置。

在插入key后,若导致原结点关键字数超过上限,则从中间位置( [m/2] )将其中的关键字分为两部分,左部分包含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置( [m/2] )的结点插入原结点的父结点。

若此时导致其父结点的关键字个数也超过了上限,则继续进行这种分裂操作,直至这个过程传到根结点为止,进而导致B树高度增。

image-20211227210741287


B树的删除

  • 若被删除关键字在终端节点,则直接删除该关键字(要注意节点关键字个数是否低于下限 [m/2] - 1 )

  • 若被删除关键字在非终端节点,则用直接前驱或直接后继来替代被删除的关键字(直接前驱:当前关键字左侧指针所指子树中“最右下”的元素)——对非终端结点关键字的删除,必然可以转化为对终端结点的删除操作

image-20211227211302810

  • 若删除关键字所在节点删除后,关键字低于下限:

    • 如果兄弟节点关键字充足,可以调整该节点、兄弟节点、父节点来达到”借关键字“的目的。

      image-20211227211924677

    • 如果兄弟节点关键字不足,可以调整该节点、兄弟节点、父节点来达到”合并“的目的。

      image-20211227212543467

B+树

B+树 可以类比分块查找。

分块查找:

image-20211227212836480

B+树:

image-20211227212949012

一棵m阶的B+树需满足下列条件:

  1. 每个分支结点最多有m棵子树(孩子结点)。
  2. 非叶根结点至少有两棵子树,其他每个分支结点至少有 m/2 棵子树。
  3. 结点的子树个数与关键字个数相等。(区别于B树,因为B+树的关键字和子树是一一对应的)
  4. 所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按大小顺序排列,并且相邻叶结点按大小顺序相互链接起来。(支持顺序查找)
  5. 所有分支结点中仅包含它的各个子结点中关键字的最大值及指向其子结点的指针。

B+的查找:

无论查找成功与否,最终一定要走到最下面一层节点。因为只有叶子节点存放了所需记录。


B树和B+树的对比:

image-20211227213649466

红黑树

概念性质

红黑树的概念模型是2-3-4树,以二叉树为基础,在二叉树属性中加入颜色属性来表示2-3-4树中不同的节点。

红黑树的五大定义:

  1. 节点颜色只能为红或黑;
  2. 根节点是黑色;
  3. 每个叶子节点是黑色的(这里的叶子节点其实是空节点)
  4. 不能出现连续的红色节点;
  5. 从任意节点到它的每个子节点的所有路径都包含相同的黑色节点;

这5条定义确保了红黑树的关键特性:

  • 从根到叶子节点的最长路径,不会超过最短路径的两倍;
  • 这棵树基本是平衡的;
  • 虽然不是绝对平衡,但可以保证最坏情况下,红黑树依然是高效的;

为什么能做到最长路径不超过最短路径的两倍?

性质4规定了路径上不能有两个相连的红节点,因此,最长路径一定是红色节点和黑节点交替而成;

性质2和3规定了根节点和叶子节点都是黑色,所以最短路径的黑节点一定多于红节点(可能全部为黑);

性质5规定每条路径黑节点的数目相同;

因此,没有路径的长能多于其他任何路径两倍。

红黑树的三种变换:变色,左旋转,右旋转

红黑树插入的新节点为红色的:

因为插入红节点,不会违反红黑树的规则,虽然可能会红红相连,不过可以通过变色和旋转来调整;而插入黑色节点,必然会导致一条路径上多一个黑节点(难以调整)。


左旋转:以当前节点X为根,逆时针旋转二叉树,使得父节点原来的位置被右子节点替代,左子节点的位置被父节点替代。同时,节点Y的左子树 b 向左平移成为了节点X的右子树

image-20200303132706061

旋转之后仍然是二叉搜索树:

image-20200303132617108

右旋转:以节点X为根,顺时针旋转二叉搜索树,使得父节点原来的位置被自己的左子节点替代,右子节点的位置被父节点替代;

image-20200303132529476

2-3树 到 红黑树

2-3-4树中的2节点对应红黑树中的黑色节点,而非2节点以红节点+黑节点的方式表示,黑色父节点+一个红色子节点表示3节点,黑色父节点+左右两个红色子节点表示4节点;

2-3-4树 :是一颗阶数为4的B树,它可能会存在2节点,3节点,4节点(2节点就是普通的二叉树节点;3节点:由2个属性X和Y,3个子节点,分别是小于X的,大于X小于Y的,大于Y的;4节点跟3节点类似)

B树到红黑树的转化

下面是以较为简单的 2-3树和左倾红黑树 为例,理解两者的转化关系

2-3树不用考虑2-3-4树中复杂的4节点分裂;

左倾红黑树规定了红色节点只能在左子树,这个限制减少了原本红黑树的调平难度,更容易理解;


2-3树 和 左倾红黑树 :把左倾红黑树中的红色节点顺时针方向旋转45°使其与黑父平行,然后再将它们看作一个整体,可以近似得到一颗2-3树。

B树到红黑树的转化

从2-3树来看,红黑树的红节点和黑节点是绑定的,因为在2-3树中它们本来就在同一层,所有只有黑节点在2-3树中体现树的高度


对于2-3树的插入:

我们的插入操作需要遵循一个原则:先将这个元素尝试性地放在已经存在的节点中(即插入一个红色节点),如果要存放的节点是2节点,那么插入后会变成3节点,如果要存放的节点是3节点,那么插入后会变成4节点(临时)。然后,我们对可能生成的临时4节点进行分裂处理,使得临时4节点消失。

如图:

2-3树的插入

2-3-4树的插入

在2-3树中如果待插入节点是个2节点,而2节点对应着红黑树中的黑色父节点;所以向2节点插入元素,即在黑色父节点下面增加一个红色儿子,

3节点插入元素,即已经有一个黑色父节点和一个红色子节点,则再给黑色父节点一个红色子节点


对于2-3树的删除:

2-3树的删除我们主要要考虑待删除元素在2节点这种情况,因为如果待删除元素在3节点,那么可以直接将这个元素删除,而不会破坏2-3树的任何性质(删除这个元素不会引起高度的变化)。

当待删除元素在2节点的时候,由于删除这个元素会导致2节点失去自己唯一的元素,引发2节点自身的删除,会使得树中某条路径的高度发生变化,树变得不平衡。

对此有两种方案:

  1. 先删除这个2节点,再对树进行平衡调整;
  2. 想办法让被删除元素不可能出现在2节点中;

这里看第二种方案,我们在搜索到这个节点的路径中,不断地判断当前节点是否为2节点,如果是,就从它的兄弟节点或者它的父节点借一个元素,使得当前节点由2节点成为一个3节点或者一个临时4节点。

这种操作会产生一种结果:除非当前节点是根节点,否则当前节点的父节点一定是一个非2节点(因为搜索的路径是自上而下,父节点已经进行过了这种操作,所以不可能是2节点),那么我们可以保证到达叶子节点的时候,也能顺利的从父节点或者兄弟节点处借到元素,使得自己成为非2节点。从而能够直接删除某个元素
2-3树的删除

红黑树的插入和删除

插入和删除操作的父子节点上下浮动和删除里借元素的做法,就是红黑树的调整(变色,旋转);

看左倾红黑树的 插入和删除(图示):

左倾红黑树的插入

左倾红黑树的插入

左倾红黑树的插入

左倾红黑树的插入

左倾红黑树的删除

删除之后需要一些调整(左倾红黑树不允许出现右倾)

左倾红黑树的删除

堆 Heap

性质:

  1. 结构性:用数组表示的完全二叉树。
  2. 有序性:任意结点的关键字(权值)是其子树所有结点的最小值/最大值
    1. 父结点大于子结点:最大堆/大根堆(MaxHeap)
    2. 父结点小于子结点:最小堆/小根堆(MinHeap)

应用:

  1. 优先队列:是一种特殊的“队列”,从队列中取出元素的顺序是依照元素的关键字(权值/数值)大小,而不是元素进入队列的先后顺序。
  2. 堆排序

最小堆操作与实现:

对于任意最多有MaxSize个元素的最小堆H ∈ MinHeap 元素,item ∈

ElementType主要操作有:

  • MinHeap Create(int Maxsize): 创建一个空的最小堆。
  • void Destroy(MinHeap):释放堆的空间。
  • Boolean IsFull(MinHeap H): 判断最小堆是否已满。
  • Boolean IsEmpty(MinHeap H): 判断最小堆是否为空。
  • void Insert(MinHeap H,ElementType item): 将元素item插入最小堆H。
  • ElementType DeleteMin(MinHeap H): 返回最小堆H中最小元素(高优先级)。
  • MinHeap BuildMinHeap(ElementType *Element,int Size,int MaxSize):创建一个非空的最小堆

数组下标为0的位置放一个比所有堆中元素都要小的元素(可以是ElementType的最小值),称为“哨兵”。从下标为1的位置开始存放堆中元素。因为是完全二叉树,所以父亲节点与其左右子节点下标满足一些关系。

由子节点找父节点:父节点下标=子节点下标 / 2
由父节点找左子节点:左子节点下标=父节点下标 * 2
由父节点找右子节点:右子节点下标=父节点下标 * 2 + 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define ElementType int

typedef struct HeapStruct {
ElementType *Elenment; //存储堆元素的数组
int Size; //堆的当前元素个数(最后一个元素的下标)
int MaxSize; //堆存储空间的大小
} HeapStruct, *MinHeap;

MinHeap Create(int MaxSize) {
MinHeap H = (MinHeap) malloc(sizeof(struct HeapStruct)); //分配堆结构空间
H->Elenment = (ElementType *) malloc(sizeof(ElementType) * (MaxSize + 1)); //分配储存堆元素的数组的空间
H->Size = 0;
H->MaxSize = MaxSize;
H->Elenment[0] = -2020;
return H;
}

//释放堆申请的空间
void Destroy(MinHeap H) {
free(H->Elenment); //先释放堆节点的数组空间
free(H); //再释放堆节点的空间
}
//判断最小堆是否已满
bool IsFull(MinHeap H) {
return (H->Size == H->MaxSize); //判断最小堆中元素个数size是否等于最大容量MaxSize
}
//判断最小堆是否为空
bool IsEmpty(MinHeap H) {
return (H->Size == 0); //判断堆中元素个数是否等于0
}

//将元素item插入最小堆H
void Inset(MinHeap H, ElementType item) {
int i = 0;
//判断堆H是否已满
if (IsFull(H)) {
printf("Heap is full\n");
return;
}
// 如果H未满,将item放入堆最后一个元素,查看它的父节点,如果它的父节点比它大,将它和它的父节点互换位置循环此过程,直至它的父节点小于它。可能它比所有它的父节点都要小,但是一定会比哨兵大(数组中下标为0的位置),所以一定最后它的下标一定大于哨兵的下标0。这就是哨兵的意义。
H->Size++;
for (i = H->Size; H->Elenment[i / 2] > item; i = i / 2) {
H->Elenment[i] = H->Elenment[i / 2];
}
H->Elenment[i] = item;
}
//返回最小堆H中最小元素(高优先级)
//将堆根结点元素取出,并将堆元素重新排序
ElementType Delete(MinHeap H){
int parent = 0,child = 0;
ElementType item, temp;

//判断堆是否已经空了
if (IsEmpty(H)){
printf("Heap is Empty\n");
return H->Elenment[0];
}
//堆没有空,将根节点返回,最后一个叶子节点放到根节点位置,然后比较它与它的左右子节点中最小节点的大小,如果它比较大,则将它和它的较小的子节点互换位置,重复此过程,直至他比两个子节点都小或者它不在有子节点
item = H->Elenment[1];
temp = H->Elenment[H->Size];
H->Size--;
for(parent = 1; parent * 2 <= H->Size; parent = child){
child = parent*2;
//找出左右子结点最小的那个
if(child != H->Size && (H->Elenment[child] > H->Elenment[child +1])){
child ++;
}
if (temp > H->Elenment[child]){
H->Elenment[parent] = H->Elenment[child];
}
else{
break;
}
}
H->Elenment[parent] = temp;
return item;
}

//创建一个非空的堆
//可以先建一个空堆,再用Insert函数将元素一个一个插入;另一种方法是:将数组复制到堆结点的Element,再进行排序第二种方法时间复杂度更低
//已知一个数组,创建一个由数组元素组成的最小堆
MinHeap BuildMinHeap(ElementType *Element,int Size,int MaxSize){
MinHeap H = NULL;
int i = 0, parent = 0, child = 0;
ElementType Temp;

H = Create(MaxSize); //创建一个空最小堆
if (Size > H->MaxSize){
printf("堆储存空间不足\n");
return NULL;
}
//复制数组元素
for(i = 0; i <Size; i++){
H->Elenment[i + 1] = Element[i];
}
H->Size = Size;
//给最小堆排序
//最后一个节点的父节点的左右指针都指向一个堆,将最后一个节点的父节点和它的两个子节点排序(方法类似与删除节点的操作),使得最后一个节点、其父节点和其兄弟节点形成一个堆。循环操作,从最后一个节点的父节点往上依次执行这个操作,最后使得整个树都是一个堆。
for (parent = H->Size/2; parent >= 1; parent--){
Temp = H->Elenment[parent];
for(; parent * 2 <= H->Size; parent = child){
child = parent * 2;

//如果左右子树都存在,将child作为两者较小者的下标
if(child != H->Size && (H->Elenment[child] > H->Elenment[child + 1])){
child ++;
}
//比较parent和child分别指向的结点的大小,parent较大则互换位置
if(Temp > H->Elenment[child]){
H->Elenment[parent] = H->Elenment[child];
}
else break;
}
H->Elenment[parent] = Temp;
}
return H;
}

堆排序

Heapsort是利用堆所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质,即子结点的键值总小于(或大于)它的父结点。它有两种方法来排序:

  1. 大顶堆:每个结点的值都大于或等于其他子结点的值,在堆排序中可用于升序排序。(因为其中每一次排序都会先将堆顶的元素交换到后面)
  2. 小顶堆:每个结点的值都小于或等于其他子结点的值,在堆排序中可用于降序排序。

堆排序的平均时间复杂度是O(nlogn)

堆排序操作与实现

  1. 创建一个堆;
  2. 把堆首与堆尾互换;
  3. 把堆的尺寸缩小1,并调用heapify(堆化)将堆重新调整好;
  4. 重复步骤2、3,直到堆的尺寸为1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
#include <stdlib.h>

void swap(int *a,int *b){
int temp = *b;
*b = *a;
*a = temp;
}
void max_heapify(int arr[],int start, int end){
//建立父结点指标和子结点指标
int parent = start;
int child = parent * 2 + 1;
while(child <= end){ //子结点指标在范围内才做比较
if(child + 1 <= end && arr[child] < arr[child + 1]) //选择两个子结点中较大的
child ++;
if(arr[parent] > arr[child]) //如果父结点较大,则表示调整完毕,直接跳出
return;
else{ //否则交换父子结点内容,再继续子结点和孙结点比较
swap(&arr[parent],&arr[child]);
parent = child;
child = parent *2 + 1;
}
}
}
void heap_dort(int arr[],int len){
int i;
//初始化,i从最后一个父结点开始调整
for(i = len / 2 -1; i >= 0; --i)
max_heapify(arr, i ,len - 1);
//先将第一个元素和已经排好的元素前一位交换,再重新调整
for(i = len - 1; i > 0; --i){
swap(&arr[0],&arr[i]);
max_heapify(arr, 0, i - 1);
//因为除了交换的两个元素,其他已经堆化好了,所以从上往下只经过一条路径即可
}
}
int main() {
int arr[] = { 3, 5, 3, 0, 8, 6, 1, 5, 8, 6, 2, 4, 9, 4};
int len = (int) sizeof(arr) / sizeof(*arr);
heap_sort(arr, len);
int i;
for (i = 0; i < len; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}

215.数组中的第k个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

示例 :

1
2
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5

(可以自行构造最大堆或最小堆,加深对堆的认识)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> maxheap = new PriorityQueue<>(Collections.reverseOrder());
int n = nums.length;
for (int num : nums) {
maxheap.add(num);
}

for(int i = 0; i < k - 1; i++){
maxheap.poll();
}
return maxheap.peek();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//最小堆实现(只需k个元素在堆里)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
// 对前k个元素建成小根堆
for (int i = 0; i < k; i++) {
swim(nums, i);
}
// 剩下的元素与堆顶比较,若大于堆顶则去掉堆顶,再将其插入
for (int i = k; i < nums.size(); i++) {
if (nums[i] > nums[0]) {
swap(nums[0], nums[i]);
sink(nums, 0, k - 1);
}
}
// 结束后第k个大的数就是小根堆的堆顶
return nums[0];
}

private:
// 若v1比v2优先度高,返回true
bool priorityThan(int v1, int v2) { return v1 < v2; }

// 上浮 从下到上调整堆
void swim(vector<int>& heap, int i) {
while (i > 0 && priorityThan(heap[i], heap[(i - 1) / 2])) {
swap(heap[i], heap[(i - 1) / 2]);
i = (i - 1) / 2;
}
}

// 下沉 从上到下调整堆
void sink(vector<int>& heap, int i, int N) {
while (2 * i + 1 <= N) {
int j = 2 * i + 1;
if (j + 1 <= N && priorityThan(heap[j + 1], heap[j])) {
j++;
}
if (priorityThan(heap[i], heap[j])) {
break;
}
swap(heap[i], heap[j]);
i = j;
}
}
}

692.前k个高频单词

给一非空的单词列表,返回前 k 个出现次数最多的单词。

返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字母顺序排序。

示例 :

1
2
3
4
输入: ["the", "day", "is", "sunny", "the", "the", "the", "sunny", "is", "is"], k = 4
输出: ["the", "is", "sunny", "day"]
解析: "the", "is", "sunny" 和 "day" 是出现次数最多的四个单词,
出现次数依次为 4, 3, 2 和 1 次。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//创建一个小根优先队列。
//将每一个字符串插入到优先队列中,如果优先队列的大小超过了 k,那么我们就将优先队列顶端元素弹出。这样最终优先队列中剩下的 kk 个元素就是前 kk 个出现次数最多的单词。
import java.util.*;

class Solution {
public List<String> topKFrequent(String[] words, int k) {
//用哈希表存储字符串和出现次数
Map<String, Integer> cnt = new HashMap<String, Integer>();
for (String word : words) {
cnt.put(word, cnt.getOrDefault(word, 0) + 1);
}
//一个PriorityQueue对象:将key-value转为entry对象,放入优先队列(堆)中,(自定义)排序(构造最小堆)
PriorityQueue<Map.Entry<String, Integer>> pq = new PriorityQueue<Map.Entry<String, Integer>>(new Comparator<Map.Entry<String, Integer>>() {
public int compare(Map.Entry<String, Integer> entry1, Map.Entry<String, Integer> entry2) {
return entry1.getValue() == entry2.getValue() ? entry2.getKey().compareTo(entry1.getKey()) : entry1.getValue() - entry2.getValue(); //注意这里 entry2.compareTo(entry1) 中entry1与entry2在数组中的位置,entry2表示后加进来的元素,entry1表示原来的元素.
}
});
//将哈希表中的key-value依次放入最小堆中
for (Map.Entry<String, Integer> entry : cnt.entrySet()) {
pq.offer(entry);
if (pq.size() > k) { //大小超过k后,去掉堆顶元素(去掉最小值,因为题目要求最大的k个)
pq.poll();
}
}

List<String> ret = new ArrayList<String>();
while (!pq.isEmpty()) {
ret.add(pq.poll().getKey());
}
Collections.reverse(ret);
return ret;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//哈希表 + 排序
import java.util.*;

class Solution {
public List<String> topKFrequent(String[] words, int k) {
HashMap<String, Integer> hash = new HashMap<>();

for (String s : words) {
hash.put(s, hash.getOrDefault(s, 0) + 1);
}

//将每一组value变为一个entry对象存入list数组
List<String> rec = new ArrayList<String>();
for (Map.Entry<String, Integer> entry : hash.entrySet()) {
rec.add(entry.getKey());
}

Collections.sort(rec, new Comparator<String>() {
public int compare(String word1, String word2) {
return hash.get(word1).equals(hash.get(word2)) ? word1.compareTo(word2) : hash.get(word2) - hash.get(word1);
}
});
return rec.subList(0, k);
}
}

图

要素

G( V , E ):图由顶点集合V(G)和边集合E(G)构成;

端点:

无向图:一条边(i,j) ,它的顶点 i 和 j 叫端点,它们互为邻接点;

有向图:一条边< i , j > ,它的顶点 i 为 起始端点 (起点),j 为终止端点 (终点),它们互为邻接点;

度:

以某点为端点的 边的数量 叫该顶点的度

入度:进入该点的边数;

出度:从该点出发的边数

完全图:

完全无向图:n( n - 1 ) / 2 条边

完全有向图:n( n - 1) 条边

当一个图接近完全图时,称为稠密图,反之,则为稀疏图;

路径:

路径长度:一条路径上经过的边的数目;

简单路径:除开始点和结束点可以相同,其它顶点均不同;

环/回路:一条路径上开始点和结束点为同一顶点;

连通:指从 i 到 j 有路径,则 i 和 j 连通;

连通图:无向图中任意两点连通;

强连通图:有向图中任意两点连通;

连通分量:指无向图G中的极大连通子图;

强连通分量:指有向图G中的极大连通子图;

(强连通图的连通分量只有一个,非连通图有多个连通分量)(强连通图也一样)

权:边上附加的一个数值,可以表示从一个顶点到另一个顶点的距离或代价;

1
2
3
4
5
6
7
8
9
问:
设有一个图G=(V,E),取V的子集V',E的子集E'。那么,(V',E)一定是G的子图吗?

答:
如果对顶点的子集和边的子集不加限定的话,(V',E')不一定构成一个图。
假如边集对应的顶点包含在所取的顶点集中,则(V',E')构成G的子图
如果所取的顶点集不能包含s取的边集所对应的所有顶点,则(V',E')不构成G的子图。


邻接矩阵 存储类型:

image-20211227214212034

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define MAXV <最大顶点个数>

//声明 顶点 类型
typedef struct{
int no; //顶点编号
InfoType info; //顶点其他信息
}VertexType;

//声明 邻接矩阵 类型
typedef struct{
int edges[MAXV][MAXV];
int n, e;
VertexType vexs[MAXV];
}

邻接表 存储类型:

image-20211227214032490

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//声明 边节点 类型
typedef struct ANode{
int adjvex; //该边的终点编号
struct ANode *next; //指向下一条边
InfoType info; //该边的权值等信息
}ArcNode;

//声明 邻接表头节点 类型
typedef struct Vnode{
vertex data; //顶点信息
ArcNode *firstArc;//指向第一条边
}VNode;

//声明 邻接表 类型
typedef struct{
VNode adjList[MAXV];//邻接表
int n, e; //图中顶点数n和边数e
}AdjGraph;

邻接矩阵 和 邻接表 对比:

image-20211227214112779

十字链表(有向图)

image-20211227214400091

找顶点出边: 顺着绿色线找

找顶点入边:顺着橙色线找


邻接多重表(无向图)

image-20211227214717843


四种存储方式 对比

image-20211227214851706

一些基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//创建图的邻接表
void createAdj(AdjGraph *G, int a[MAXV][MAXV], int n, int e) {
int i, j;
ArcNode *p;
G = (AdjGraph *) malloc(sizeof(AdjGraph));
for (i = 0; i < n; ++i) {
G->adjList[i].firstArc = NULL; //所有头节点的指针域赋初值
}
for (i = 0; i < n; ++i) {
for (j = n - 1; j >= 0; --j) {
if (a[i][j] != 0 && a[i][j] != INF) { //这条边存在
p = (ArcNode *) malloc(sizeof(ArcNode));
p->adjvex = j; //存放邻接点
p->weight = a[i][j]; //存放 权
p->next = G->adjList[i].firstArc; //头插法插入节点
G->adjList[i].firstArc = p;
}
}
}
G->n = n;
G->e = n;
}
//输出邻接表G
void displayAdj(AdjGraph *G) {
int i;
ArcNode *p;
for (i = 0; i < G->n; ++i) {
p = G->adjList[i].firstArc;
printf("%d:", i);
while (p != NULL) {
printf("%d[%d]->", p->adjvex, p->weight);
p = p->next;
}
printf("^\n");
}
}

//销毁邻接表
void destory(AdjGraph *G) {
int i;
ArcNode *pre, *p;
for (i = 0; i < G->n; ++i) {
pre = G->adjList[i].firstArc;
if (pre != NULL) {
p = pre->next;
while (p != NULL) { //释放第i个单链表的所有节点
free(pre);
pre = p;
p = p->next;
}
free(pre);
}
}
free(G); //释放头节点数组
}

//邻接矩阵g 转化成 邻接表G (时间复杂度:O(n^2))
void MatToList(MatGraph g, AdjGraph *G) {
int i, j;
ArcNode *p;
G = (AdjGraph *) malloc(sizeof(AdjGraph));
for (i = 0; i < g.n; ++i) {
G->adjList[i].firstArc = NULL;
}
for (i = 0; i < g.n; i++) {
for (j = 0; j >= 0; ++j) {
if (g.edges[i][j] != 0 && g.edges[i][j] != INF) {
p = (ArcNode *) malloc(sizeof(ArcNode));
p->adjvex = j;
p->weight = g.edges[i][j];
p->next = G->adjList[i].firstArc;
G->adjList[i].firstArc = p;
}
}
}
G->n = g.n;
G->e = g.e;
}

//将邻接表G转换成邻接矩阵g
//时间复杂度:无向图为O(n+2e),有向图为O(n+e),e为边数
void ListToMat(AdjGraph *G, MatGraph g) {
int i;
ArcNode *p;
for (i = 0; i < G->n; i++)//扫描所有的单链表
{
p = G->adjList[i].firstArc;//p指向第i个单链表的首结点while(p!=NULL)//扫描第i个单链表
while (p != NULL) {
g.edges[i][p->adjvex] = p->weight;
p = p->next;
}
g.n = G->n;
g.e = G->e;
}
}

int visited[MAXV] = {0};

//深度优先搜索: O(n+e)
//用栈 或 递归
void DFS(AdjGraph *G, int v) {
ArcNode *p;
int w;
for (int i = 0; i < MAXV; i++) {
visited[i] = 0;
}
visited[v] = 1;
printf("%d ", v);
p = G->adjList[v].firstArc;
while (p != NULL) {
w = p->adjvex;
if (visited[w] == 0)
DFS(G, w);
p = p->next;
}
}

//广度优先搜索: O(n+e)
void BFS(AdjGraph *G, int v) {
int w, i;
ArcNode *p;
SqQueue *qu; //定义环形队列指针
IntiQueue(qu); //初始化队列
int visit[MAXV]; //顶点访问标记数组
for (i = 0; i < G->n; i++) {
visit[i] = 0;
}
printf("%2d", v); //输出被访问的顶点 编号
visit[v] = 1; //此点 已访问
EnQueue(qu, v);
while (!QueueEmpty(qu)){
DeQuenu(qu, w); //出队一个顶点w
p = G->adjList[w].firstArc; //指向w的第一个邻接点
while (p != NULL) { //查找w的所有邻接点
if(visit[p->adjvex] == 0){
printf("%2d", p->adjvex);
visit[p->adjvex] = 1;
EnQueue(qu, p->adjvex);//该点进队
}
p = p->next;//找下一个点
}
}
printf("\n");
}

生成树

一个连通图的生成树是一个极小连通子图,它含有图中全部n个顶点和构成一棵树的(n - 1 )条边。

遍历方法得到生成树:深度优先生成树,广度~~树

(所以一个连通图的生成树不一定唯一)

最小生成树

对 带权连通图G (权 均为大于0的实数),其权值之和最小的生成树 即 图的最小生成树。

普里姆(prim)算法

(从顶点 考虑)

  1. 选择初始点 把图中的顶点分成两个不同的顶点集S(生成树的顶点集),V-S
  2. 在横跨两个不同顶点集的边中选择一条权值最小的边加入到生成树中
  3. 将该边的另一个顶点加入到顶点集S中,并从V-S中删除
  4. 重复步骤2,3,知道V-S为空集
202204201810815 202204201810813
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void prim(MatGraph g, int v) {
int lowcost[MAXV]; //lowcost存储到未标顶点的最小权值
int min;
int closest[MAXV], i, j, k; //closest存储最小权值对应的顶点
for (i = 0; i < g.n; ++i) { //赋初值
lowcost[i] = g.edges[v][i];
closest[i] = v; //此时生成树只有一个顶点
}
for (i = 1; i < g.n; i++) { //输出(n - 1)条边
min = INF;
for (j = 0; j < g.n; j++) //在( V - U )中找出离U最近的顶点k
if (lowcost[j] != 0 && lowcost[j] < min) {
min = lowcost[j];
k = j; //k记录最近的顶点编号
}
printf("边(%d, %d)权为:%d\n", closest[k], k, min);
lowcost[k] = 0; //标记k已经加入U
for(j = 0; j < g.n; j++) //修改数组lowcost和cloest
if (lowcost[j] != 0 && g.edges[k][j] < lowcost[j]){
lowcost[j] = g.edges[k][j]; //更新S和V-S之间最小权值
closest[j] = k; //最小权值对应的 在Sz
}
}
}

克鲁斯卡尔(kruskal)算法

(从边 考虑)

  1. 把图中的顶点看成n个不同的顶点集,每个顶点集都只含1个顶点
  2. 每次都在横跨不同顶点集的边中选择一条权值最小的边加入到最小生成树中
  3. 直到图中所有的顶点都在同一个顶点集为止

202204201810581

202204201810936

思路:连通图的边按照权值从小到大进行排列;依次检查它的每条边对应的两个端点是否分别属于两个不同的顶点集,是,则加入生成树中。

1

最短路径

image-20211227204922551

狄克斯特拉(Dijkstra)算法

只适用 有向无环图,图中存在负权重、环 时,无法使用狄克斯特拉算法

202204201811792

过程如下:

从起点开始,起点到其所有邻居的距离2和6是初始值,其他不相邻的节点的距离为无穷大,

然后从{2,6,无穷大}中找到最小值2,2对应的节点是B,然后从B开始遍历B的所有邻居{A,终点},计算起点->B->A的距离为2+3=5,比之前的距离6要小,所以更新A的记录为5,继续计算起点->B->终点的距离为2+5=7,比之前的距离无穷大要小,所以更新终点的记录为7,至此B的所有相邻节点遍历完毕,故划去B,

之后继续从{2,5,7}中寻找最小值,由于2对应的B节点已经被划去,所以只要从{5,7}中找最小值,最小值为5,5对应于节点A,然后从A开始遍历A的所有邻居{终点},计算起点->A->终点的距离6,比之前的距离7要小,故更新终点的记录为6,至此A的所有邻居遍历完毕,故划去A,

最后只剩下终点这个节点,由于终点没有邻居,故图中的所有邻居遍历完毕。至此得出从起点到终点的最短距离是6。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//狄克斯特拉(Dijkstra)算法:O(n^2)
void Dijkstra(MatGraph g, int v) { //v为源点
int dist[MAXV], path[MAXV]; //dist[]存放到某点的最短路径长度,path[]存放最短路径
int s[MAXV]; //标记顶点是否访问过
int mindis, u; //mindis为最短距离,u为新考虑的中间点
for (int i = 0; i < g.n; i++) { //初始化dist和path
dist[i] = g.edges[v][i]; //距离初始化,即v点到其余点的距离
s[i] = 0;
if (g.edges[v][i] < INF)
path[i] = v;
else path[i] = -1;
}
s[v] = 1;
for (int i = 0; i < g.n; i++) {
mindis = INF;
for (int j = 0; j < g.n; j++) //找最小路径长度的顶点u
if (s[j] == 0 && dist[j] < mindis) {
u = j;
mindis = dist[j];
}
s[u] = 1;
for (int j = 0; j < g.n; j++) //修改未考虑的顶点的距离
if (s[j] == 0)
if (g.edges[u][j] < INF && dist[u] + g.edges[u][j] < dist[j]) {
dist[j] = dist[u] + g.edges[u][j];
path[j] = u;
}
}
Dispath(dist, path, s, g.n, v); //输出最短路径
}

弗洛伊德(Floyd)算法

基本思想:
弗洛伊德算法定义了两个二维矩阵:

矩阵D记录顶点间的最小路径
例如D[0q0] [3]= 10,说明顶点0 到 3 的最短路径为10;

矩阵P记录顶点间最小路径中的中转点
例如P[0] [3]= 1 说明,0 到 3 的最短路径轨迹为:0 -> 1 -> 3。

它通过3重循环,k为中转点,v为起点,w为终点,循环比较D[v] [w] 和 D[v] [k] + D[k] [w] 最小值,如果D[v] [k] + D[k] [w] 为更小值,则把D[v] [k] + D[k] [w] 覆盖保存在D[v] [w]中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
int pathmatirx[MAXV][MAXV];//记录对应点的最小路径的前驱点,例如p(1,3) = 2 说明顶点1到顶点3的最小路径要经过2
int shortPath[MAXV][MAXV];//记录顶点间的最小路径值

void short_path_floyd(MatGraph G, int P[MAXV][MAXV], int D[MAXV][MAXV]) {
int v, w, k;
//初始化floyd算法的两个矩阵
for (v = 0; v < G.n; v++) {
for (w = 0; w < G.n; w++) {
D[v][w] = G.edges[v][w];
P[v][w] = w;
}
}

//这里是弗洛伊德算法的核心部分
//k为中间点
for (k = 0; k < G.n; k++) {
//v为起点
for (v = 0; v < G.n; v++) {
//w为终点
for (w = 0; w < G.n; w++) {
if (D[v][w] > (D[v][k] + D[k][w])) {
D[v][w] = D[v][k] + D[k][w];//更新最小路径
P[v][w] = P[v][k];//更新最小路径中间顶点
}
}
}
}

printf("\n初始化的D矩阵\n");
for (v = 0; v < G.n; v++) {
for (w = 0; w < G.n; w++) {
printf("%d ", D[v][w]);
}
printf("\n");
}

printf("\n初始化的P矩阵\n");
for (v = 0; v < G.n; v++) {
for (w = 0; w < G.n; w++) {
printf("%d", P[v][w]);
}
printf("\n");
}

v = 0;
w = 3;
//求 0 到 3的最小路径
printf("\n%d -> %d 的最小路径为:%d\n", v, w, D[v][w]);
k = P[v][w];
printf("path: %d", v);//打印起点
while (k != w) {
printf("-> %d", k);//打印中间点
k = P[k][w];
}
printf("-> %d\n", w);
}

拓扑排序

AOV网(Activity On Vertex NetWork):用顶点表示活动的网。每个AOV网都有一个或多个拓扑排序序列。

AOV网有且仅有一个入度为0的开始点,出度为0的结束点。

拓扑排序:在有向无环图中,找出做事的先后顺序。

规则:

  • 图中每个顶点只出现一次。
  • A在B前面,则不存在B在A前面的路径。(不能成环!!!!)
  • 顶点的顺序是保证所有指向它的下个节点在被指节点前面!(例如A—>B—>C那么A一定在B前面,B一定在C前面)。所以,这个核心规则下只要满足即可,所以拓扑排序序列不一定唯一!

使用拓扑排序时,一般是稀疏图,所以用邻接表,

代码思想为:

新建node类,包含节点数值和它的指向;

一个数组包含node(这里默认编号较集中)。初始化,添加每个节点指向的时候同时被指的节点入度+1!(A—>C)那么C的入度+1;
扫描一遍所有node。将所有入度为0的点加入一个栈(队列)。

当栈(队列)不空的时候,抛出其中任意一个node(栈就是尾,队就是头,顺序无所谓,上面分析了只要同时入度为零可以随便选择顺序)。将node输出,并且node指向的所有元素入度减一。如果某个点的入度被减为0,那么就将它加入栈(队列)。

重复上述操作,直到栈为空。

210.课程表

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。

1
例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1]。

返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组。

题解:

本题是一道经典的「拓扑排序」问题。

  • 我们将每一门课看成一个节点;

  • 如果想要学习课程 A 之前必须完成课程 B,那么我们从 B 到 A 连接一条有向边。这样以来,在拓扑排序中,B一定出现在 A 的前面。

求出该图的拓扑排序,就可以得到一种符合要求的课程学习顺序。

方法一,深度优先搜索

对于一个节点 u,如果它的所有相邻节点都已经搜索完成,那么在搜索回溯到 u 的时候,u本身也会变成一个已经搜索完成的节点。这里的「相邻节点」指的是从 u 出发通过一条有向边可以到达的所有节点。

对于图中的任意一个节点,它在搜索的过程中有三种状态,即:

  • 「未搜索」:我们还没有搜索到这个节点;

  • 「搜索中」:我们搜索过这个节点,但还没有回溯到该节点,即该节点还没有入栈,还有相邻的节点没有搜索完成);

  • 「已完成」:我们搜索过并且回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置,满足拓扑排序的要求。

通过上述的三种状态,我们就可以给出使用深度优先搜索得到拓扑排序的算法流程,在每一轮的搜索搜索开始时,我们任取一个「未搜索」的节点开始进行深度优先搜索。

  • 我们将当前搜索的节点 u 标记为「搜索中」,遍历该节点的每一个相邻节点 v:

  • 如果 v 为「未搜索」,那么我们开始搜索 v,待搜索完成回溯到 u;

  • 如果 v 为「搜索中」,那么我们就找到了图中的一个环,因此是不存在拓扑排序的;

  • 如果 v 为「已完成」,那么说明 v 已经在栈中了,而 u 还不在栈中,因此 u 无论何时入栈都不会影响到 (u, v)(u,v) 之前的拓扑关系,所以不用进行任何操作。

  • 当 u 的所有相邻节点都为「已完成」时,我们将 u 放入栈中,并将其标记为「已完成」。

在整个深度优先搜索的过程结束后,如果我们没有找到图中的环,那么栈中存储这所有的 n 个节点,从栈顶到栈底的顺序即为一种拓扑排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Solution {
// 存储有向图
List<List<Integer>> edges;
// 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
int[] visited;
// 用数组来模拟栈,下标 n-1 为栈底,0 为栈顶
int[] result;
// 判断有向图中是否有环
boolean valid = true;
// 栈下标
int index;

public int[] findOrder(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
visited = new int[numCourses];
result = new int[numCourses];
index = numCourses - 1;
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {
dfs(i);
}
}
if (!valid) {
return new int[0];
}
// 如果没有环,那么就有拓扑排序
return result;
}

public void dfs(int u) {
// 将节点标记为「搜索中」
visited[u] = 1;
// 搜索其相邻节点
// 只要发现有环,立刻停止搜索
for (int v: edges.get(u)) {
// 如果「未搜索」那么搜索相邻节点
if (visited[v] == 0) {
dfs(v);
if (!valid) {
return;
}
}
// 如果「搜索中」说明找到了环
else if (visited[v] == 1) {
valid = false;
return;
}
}
// 将节点标记为「已完成」
visited[u] = 2;
// 将节点入栈
result[index--] = u;
}
}

复杂度分析

  • 时间复杂度: **O(n+m)**,其中 n 为课程数,m 为先修课程的要求数。这其实就是对图进行深度优先搜索的时间复杂度。

  • 空间复杂度: **O(n+m)**。题目中是以列表形式给出的先修课程关系,为了对图进行深度优先搜索,我们需要存储成邻接表的形式,空间复杂度为 O(n+m)。在深度优先搜索的过程中,我们需要最多 O(n) 的栈空间(递归)进行深度优先搜索,并且还需要若干个 O(n)的空间存储节点状态、最终答案等。

方法二:广度优先搜索

我们使用一个队列来进行广度优先搜索。开始时,所有入度为 0 的节点都被放入队列中,它们就是可以作为拓扑排序最前面的节点,并且它们之间的相对顺序是无关紧要的。

在广度优先搜索的每一步中,我们取出队首的节点 u:

我们将 u 放入答案中;

我们移除 u 的所有出边,也就是将 u 的所有相邻节点的入度减少 1。如果某个相邻节点 v 的入度变为 0,那么我们就将 v 放入队列中。

在广度优先搜索的过程结束后。如果答案中包含了这 n 个节点,那么我们就找到了一种拓扑排序,否则说明图中存在环,也就不存在拓扑排序了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class Solution {
// 存储有向图
List<List<Integer>> edges;
// 存储每个节点的入度
int[] indeg;
// 存储答案
int[] result;
// 答案下标
int index;

public int[] findOrder(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
indeg = new int[numCourses];
result = new int[numCourses];
index = 0;
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
++indeg[info[0]];
}

Queue<Integer> queue = new LinkedList<Integer>();
// 将所有入度为 0 的节点放入队列中
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
queue.offer(i);
}
}

while (!queue.isEmpty()) {
// 从队首取出一个节点
int u = queue.poll();
// 放入答案中
result[index++] = u;
for (int v: edges.get(u)) {
--indeg[v];
// 如果相邻节点 v 的入度为 0,就可以选 v 对应的课程了
if (indeg[v] == 0) {
queue.offer(v);
}
}
}

if (index != numCourses) {
return new int[0];
}
return result;
}
}

内排序

img

img

希尔排序

也称为缩小增量(递减增量)算法,是插入排序的改进。

希尔排序需要一个增量序列(增量会递减),一般用len/2,如:len为11,则增量分别为5,2,1,进行三次循环。

每次循环排序只排 间隔 为增量值的数,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ShellSort {
public void shellSort(int[] nums) {
int len = nums.length;
for (int i = len / 2; i > 0; i /= 2) { //i为增量,初始化为len/2
for (int j = i; j < len; j++) { //j用于每次更改增量后遍历一次数组
int k; //k用于对第j个元素进行插入排序,比较的是前面间隔为i的元素
int temp = nums[j];
for (k = j; k >= i && temp < nums[k - i]; k -= i) {
nums[k] = nums[k - i];
}
nums[k] = temp;
}
}
}

public static void main(String[] args) {
int[] nums = new int[]{7, 6, 9, 3, 1, 5, 2, 4};
new ShellSort().shellSort(nums);
System.out.println(Arrays.toString(nums));
}
}

归并排序

将数据不断分半,直到只剩下一个元素,然后从以分半的两段数据,逐个选出最小的元素移入临时数组的末尾。(归并排序需要用到临时数组)

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//合并操作
void merge(int arr[], int tempArr[],int left, int mid, int right){
//标记左半区第一个未排序的元素
int l_pos = left;
//标记右半曲第一个未排序的元素
int r_pos = mid + 1;
//临时变量元素的下标
int pos = left;

while (l_pos <= mid &&r_pos <= right){
if(arr[l_pos] < arr[r_pos]) //左半区剩余第一个元素更小
tempArr[pos++] = arr[l_pos++];
else //右半区剩余第一个元素更小
tempArr[pos++] = arr[r_pos++];
}
while(l_pos <= mid){ //合并左半区剩余元素(右半区合并完了)
tempArr[pos++] = arr[l_pos++];
}
while(r_pos <= right){ //合并右半区剩余元素(左半区合并完了)
tempArr[pos++] = arr[r_pos++];
}
//将临时数组中合并后的元素复制到原来的数组
while(left <= right){
arr[left] = tempArr[left];
left++;
}
}
//归并排序
void msort(int arr[], int tempArr, int left, int right){
//如果只有一个元素,那么不需要继续划分
if(left < right){
int mid = (left + right) / 2;
//递归划分左右半区
msort(arr,tempArr, left, mid);
msort(arr, tempArr, mid + 1, right);
merge(arr, tempArr, left, mid , right);
}
}
//归并排序入口
void merge_sort(int arr[], int n){
//分配一个辅助数组
int *tempArr = (int*)malloc(n * sizeof(int));
if(tempArr){ //辅助数组分配成功
msort(arr, tempArr, 0, n - 1);
free(tempArr);
}
else{
printf("error: failed to allocate memory");
}
}

快速排序

在区间中挑选一个元素作基准,将小于基准的元素放在基准之前,大于基准的元素放在基准之后,再分别对小数区与大数区进行同样操作。直到各个分区只有一个数时为止。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//这里以第一个元素作为基准
void quicksort(int *s, int start, int end) {
if (start < end) {
int i = start, j = end, x = s[start]; //以第一个元素作为基准,后面用了类似挖坑的写法,临时存储这个元素,遇到第一个需要交换的元素,直接赋值给基准处。
while (i < j) { //一边交换后,换另一边比较
// 每一次循环,都将第一个遇到的比基准小的数放在当前基准(开头)的位置;(没遇到就说明当前基准是最小的,放开头是对的)
// 第一个遇到的大于基准的数放在刚才换过去的较小数的位置。
// 然后基准放在这个较大数的位置。(一遍循环过后再放)
while (i < j && s[j] >= x) {
j--;
}
if (i < j) {
s[i++] = s[j];
}
while (i < j && s[i] < x) {
i++;
}
if (i < j) {
s[j--] = s[i];
}
}
s[i] = x;
quicksort(s, start, i - 1);
quicksort(s, i + 1, end);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void quick_sort_recursive(int arr[], int start, int end) {
if (start >= end)
return;
int mid = arr[end];
int i = start, j = end - 1;
while (i < j) {
while (arr[i] < mid && i < j)
i++;
while (arr[j] >= mid && i < j)
j--;
swap(&arr[i], &arr[j]);
}
if (arr[i] >= arr[end])
swap(&arr[i], &arr[end]);
else
i++;
if (i)
quick_sort_recursive(arr, start, i - 1);
quick_sort_recursive(arr, i + 1, end);
}
void quick_sort(int arr[], int len) {
quick_sort_recursive(arr, 0, len - 1);
}

堆排序

(堆积是一个近似完全二叉树的结构,并同时满足堆积的性质)

堆排序可以说是一种利用堆的概念来排序的选择排序。分为两种方法:

  • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;

  • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

算法步骤:

  1. 创建一个堆
  2. 把堆首(最大值)和堆尾互换;
  3. 把堆的尺寸减少1,并调用上浮/下沉操作,把新的最大值调整到堆首;
  4. 重复步骤2,3,直到堆的尺寸为1;

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class HeapSort {
public int[] sort(int[] arr){
int len = arr.length;
buildMaxHeap(arr, len);

for (int i = len - 1; i > 0; i--) {
swap(arr, 0, i);
len--;
heapify(arr, 0, len);
}
return arr;
}

private void buildMaxHeap(int[] arr, int len) {
for (int i = (int) Math.floor(len / 2); i >= 0; i--) {
heapify(arr, i, len);
}
}

private void heapify(int[] arr, int i, int len) {
int left = 2 * i + 1;
int right = 2 * i + 2;
int largest = i;

if (left < len && arr[left] > arr[largest]) {
largest = left;
}

if (right < len && arr[right] > arr[largest]) {
largest = right;
}

if (largest != i) {
swap(arr, i, largest);
heapify(arr, largest, len);
}
}

private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

计数排序

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。

作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

算法的步骤如下:

  1. 找出待排序的数组中最大和最小的元素

  2. 统计数组中每个值为i的元素出现的次数,存入数组C的第i项

  3. 反向填充目标数组:将每个元素 i 放在新数组的第C(i)项,每放一个元素就将C(i)减去1

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class CountingSort{
public int[] sort(int[] arr){
int maxValue = getMaxValue(arr);
return countingSort(arr, maxValue);
}

private int[] countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];

for (int value : arr) {
bucket[value]++;
}

int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}

private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
}

桶排序

排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:

  1. 在额外空间充足的情况下,尽量增大桶的数量
  2. 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中

同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。

最快的情况:当输入的数据可以均匀的分配到每一个桶中。

最慢的情况:当输入的数据被分配到了同一个桶中。

示例:

元素分布在桶中:

img

然后,元素在每个桶中排序:

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class BucketSort{
public int[] sort(int[] arr) {
return bucketSort(arr, 5); //桶大小是5
}

private int[] bucketSort(int[] arr, int bucketSize){
if (arr.length == 0) {
return arr;
}

//选出最大值和最小值
int minValue = arr[0];
int maxValue = arr[0];
for (int value : arr) {
if (value < minValue) {
minValue = value;
} else if (value > maxValue) {
maxValue = value;
}
}

int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1;
int[][] buckets = new int[bucketCount][0];

// 利用映射函数将数据分配到各个桶中
for (int i = 0; i < arr.length; i++) {
int index = (int) Math.floor((arr[i] - minValue) / bucketSize);
buckets[index] = arrAppend(buckets[index], arr[i]);
}

int arrIndex = 0;
for (int[] bucket : buckets) {
if (bucket.length <= 0) {
continue;
}
// 对每个桶进行排序,这里的XxxSort根据情况而定
bucket = XxxSort.sort(bucket);
for (int value : bucket) {
arr[arrIndex++] = value;
}
}
return arr;
}

//(自动扩容)将元素加入到桶中
private int[] arrAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}

基数排序

基数排序是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。

下面三种排序算法都利用了桶的概念,但对桶的使用方法上有明显差异:

  • 基数排序:根据键值的每位数字来分配桶;
  • 计数排序:每个桶只存储单一键值;
  • 桶排序:每个桶存储一定范围的数值;

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class RadixSort {
public int[] sort(int[] arr) {
int maxDigit = getMaxDigit(arr);
return radixSort(arr, maxDigit);
}

//获取最高位数
private int getMaxDigit(int[] arr) {
int maxValue = getMaxValue(arr);
return getNumLenght(maxValue);
}

//获取最大值
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}

//获取位数
protected int getNumLenght(long num) {
if (num == 0) {
return 1;
}
int lenght = 0;
for (long temp = num; temp != 0; temp /= 10) {
lenght++;
}
return lenght;
}

private int[] radixSort(int[] arr, int maxDigit) {
int mod = 10;
int dev = 1;

for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
// 考虑负数的情况,这里扩展一倍队列数,其中 [0-9]对应负数,[10-19]对应正数 (bucket + 10)
int[][] counter = new int[mod * 2][0];

for (int j = 0; j < arr.length; j++) {
int bucket = (arr[j] % mod) / dev;
if (arr[j] > 0) //考虑正负数
bucket += 10;
else
bucket = -bucket;
counter[bucket] = arrayAppend(counter[bucket], arr[j]);
}

int pos = 0;
for (int[] bucket : counter) {
for (int value : bucket) {
arr[pos++] = value;
}
}
}
return arr;
}

//自动扩容,并保存数据
private int[] arrayAppend(int[] arr, int value) {
arr = Arrays.copyOf(arr, arr.length + 1);
arr[arr.length - 1] = value;
return arr;
}
}

外排序

对磁盘中的数据进行排序。

归并排序

最少只需要在内存中开辟2个输入缓冲区和一个输出缓冲区,每次将磁盘的数据块 输入到输入缓冲区,然后按 归并排序的规则,从2个输入缓冲区 每次跳出最小的数 放到输出缓冲区,当有输入缓冲区为空了,应该立刻将和它排好序的下一个数据块放到输入缓冲区中。当输出缓冲区满时,写出外存。

减少时间开销:进行K路归并排序,需要在内存中分配k个输入缓冲区和一个输出缓冲区。

因为读写磁盘时间开销大,所以多几路可以提高效率,但也不能太多,因为需要在内存中开多个缓冲区,且每次比较都要比较这k个缓冲区的最小值

时间开销:读写外存时间+内部排序时间+内部归并时间

优化:

  • 增加归并路数k
    • 代价1:需要增加相应的输入缓冲区
    • 代价2:每次从k个归并段总选一个最小值需要(k-1)次比较。——可用败者树减少关键字比较次数
  • 减少初始归并段数量r
    • 若有N个记录,内存工作区可放L个记录,则初始归并段数量 r = N / L ——可用 置换-选择排序 减少初始归并段

败者树

败者树解决的问题:使用多路平衡归并可減少归并趟数,但是用老土方法从k个归并段选出一个最小/最大元素需要对比关键字k-1次,构造败者树可以使关键字对比次数减少到 log2k。

败者树可视为一棵完全二叉树(多了一个在根节点之上的头节点)。k个叶结点分别对应k个归并段中当前参加比较的元素,非叶子结点用来记忆左右子树中的“失败者”,而让胜者往上继续进行比较,一直到根结点。

image-20211227202553354

image-20211227202243956

置换-选择排序

设初始待排文件为FI,初始归并段输出文件为FO,内存工作区为WA,FO和WA的初始状态为空,WA可容纳w个记录。置換-选择算法的步骤如下:

  1. 从FI输入w个记录到工作区WA
  2. 从WA中选出其中关键字取最小值的记录,记为 MIINIMAX记录。(MIINIMAX指不断选出的最小指中的最大值)
  3. 将 MINIMAX记录输出到FO中去。
  4. 若FI不空,则从FI输入下一个记录到WA中。
  5. 从WA中所有关键字比 MIINIMAX记录的关键字大的记录中选出最小关键字记录,作为新的MINIMAX记录。
  6. 重复(3)~(5),直至在WA中选不出新的 MINIMAX记录为止,由此得到一个初始归并段,输出一个归并段的结束标志到FO中去。
  7. 重复(2)~(6),直至WA为空。由此得到全部初始归并段。

image-20211227203606666

r

试题

选择题

  1. 设指针变量p指向单链表中结点A,若删除单链表中结点A,则需要修改指针的操作序列为( )。
    (A) q=p->next;p->data=q->data;p->next=q->next;free(q);
    (B) q=p->next;q->data=p->data;p->next=q->next;free(q);
    (C) q=p->next;p->next=q->next;free(q);
    (D) q=p->next;p->data=q->data;free(q);

    答案:A

    先用指针变量q指向结点A的后继结点B,然后将结点B的值复制到结点A中,最后删除结点B。

  2. 设有n个待排序的记录关键字,则在堆排序中需要( )个辅助记录单元。

    答案:1个。

    堆排序只需一个辅助空间,用来记录当前操作的二叉树上的根节点的数字,来与其左右孩子节点比较。

  3. 设某二叉树中度数为0的结点数为N0,度数为1的结点数为Nl,度数为2的结点数为N2,则下列等式成立的( )。

    答案:N0 = N2 + 1

  4. 设有序顺序表中有n个数据元素,则利用二分查找法查找数据元素X的最多比较次数不超过( )。

    答案:log2n + 1

  5. 1、数据的最小单位是( A )。
    (A) 数据项
    (B) 数据类型
    (C) 数据元素
    (D) 数据变量

  6. 设一棵m叉树中度数为0的结点数为N0,度数为1的结点数为N1,……,度数为m的结点数为Nm,则N0 =( B )。
    (A) Nl+N2+……+Nm
    (B) 1+N2+2N3+3N4+……+(m-1)Nm
    (C) N2+2N3+3N4+……+(m-1)Nm
    (D) 2Nl+3N2+……+(m+1)Nm

  7. 设在一棵度数为3的树中,度数为3的结点数有2个,度数为2的结点数有1个,度数为1的结点数有2个,那么度数为0的结点数有( )个。

    解:6个

    除了根节点外,其他节点均为孩子节点,而孩子节点等于总的分支数,即 孩子节点 = 1 * n1 + 2 * n2+3 * n3 = 10;

    所以总节点数 = 10 + 1 = 11(1是指根节点),度数为0的节点数 = 11 - n1 - n2 - n3 = 6;

  8. 设顺序表的长度为n,则顺序查找的平均比较次数为()。

    解:(n+1)/ 2

    若第一个为查找元素,则查找次数为1,。。。。。所以 平均查找次数为 (1+2+3 + ….+ n) / n= (1+n) / 2;

填空题

  1. 通常从四个方面评价算法的质量:正确性、易读性、强壮性、高效性。

  2. 数据结构从逻辑上划分为三种基本类型:线性结构,树型结构,图型结构。

  3. 用链表存储二叉树,则有n个节点的二叉树共有 2n 个指针域,其中 n-1 个存放了地址,剩下的是空指针(n+1个)。

  4. AOV网是一种 有向无回路 的图。

  5. 对于具有n个顶点和e条边的有向图和无向图,其对应的邻接表中,所含边节点分别有 e 和 2e 个。

  6. 在一个具有n个顶点的无向完全图中,包含有 n(n-1)/2 条边,有n个顶点的有向完全图中,包含有 n(n-1) 条边。

  7. 设某二叉树中度数为0的节点数为 N,度数为1的节点数为 M,可得该二叉树中度数为2的节点数为 N - 1 ,。

  8. 数据的物理结构主要包括 顺序存储结构 和 链式存储结构。

  9. 设满二叉树的深度为k, 则该树的节点数为 2^k^ - 1; 若用二叉链表作为二叉树的存储结构,设该树有 n 个节点,则一共有 n + 1个空指针域。

  10. 一个序列,经过栈的作用可以得到 C^n^2n/ (n+1) 中不同的输出序列。(卡特兰数)

如:序列有3个数,则有5中不同的输出序列。

  1. 设哈夫曼树有n个节点,则该树有 0 个度数为1的节点。

  2. 设有n个结点的完全二叉树,如果按照从自上到下、从左到右从1开始顺序编号,则第 i 个结点的双亲结点编号为 i / 2 ; 右孩子结点的编号为 2i + 1。

  3. 设一组初始记录关键字为(72,73,71,23,94,16,5),则以记录关键字72为基准的一趟快速排序结果为

    答案:(5,16,71,23,72,94,73)

    两个指针,从左开始,和从右开始,先从右开始选第一个遇到的小于基准的数,和基准数换位;换 从左开始,遇到的第一个大于基准的数,和基准换位;再从右继续……循环,直至小于基准的在左边,大于基准的在右边。

  4. 在二叉排序树中插入一个结点的时间复杂度为 O(n),如果该排序树即不平衡,需要遍历n个节点,如果该树很平衡,则只需log2n。

  5. 设初始记录关键字序列为(K1,K2,…,Kn),则用筛选法思想建堆必须从第 n/2 个元素开始进行筛选。

  6. 设哈夫曼树中共有99个结点,则该树中有50 个叶子结点;若采用二叉链表作为存储结构,则该树中有_100_个空指针。

  7. 设有一个顺序循环队列中有M个存储单元,则该循环队列中最多能够存储______m-1个队列元素;当前实际存储_____(R - F + M) % M_个队列元素(设头指针F指向当前队头元素的前一个位置,尾指针指向当前队尾元素的位置)。

  8. 设顺序线性表中有n个数据元素,则第i个位置上插入一个数据元素需要移动表中 n-i+1 个数据元素;删除第i个位置上的数据元素需要移动表中 n-i 个元素。

  9. 根据二叉树的先序和中序遍历、或中序和后序遍历,可以唯一确定二叉树的形状。

  10. 筛选法建初始堆必须从第 n / 2 个元素开始进行筛选。

  11. 快速排序在序列基本有序时为 O(n^2^),在序列基本无序时为 O(nlog2n)。

  12. 设一棵m叉树的结点数为n,用多重链表表示其存储结构,则该树中有 n(m-1)+1 个空指针域。