アルゴリズムの並べ替え: 再帰、バックトラッキング、数論、挿入、ヒル、マージ、選択、バブリング、クイック ソート、貪欲、動的プログラミング
数論的思考: 問題を解決するために数式、定理、法則を使用します。
アルゴリズム思考の最大の難所:再帰+動的計画法、木の理論:二分木、赤黒木
考えるための質問:
-
WeChat 配信システムにはリベートがあります。たとえば、B は A のオフラインであり、C は B のオフラインであることを誰もが知っておく必要があります。その後、A はリベート中に B と C のお金を共有できます。この時点では、 BとCそれぞれの最後の上位者を見つける。通常、この問題をどのように解決しますか?
-
フィボナッチ数列: 1 1 2 3 5 8 13 21 特徴は何ですか? 3 番目の数値から開始することは、前の 2 つの数値を加算したことに等しい; 解の公式: f(n)=f(n-1)+f(n-2) 終了条件: n<=2 f(n)=1
再帰
再帰的定義
たとえば、ある窓口に並んでいる人が多すぎて、自分がどこにいるのかわからないので、前に並んでいる人に誰がいるのかを尋ねます。なぜなら、誰がいるのかがわかれば、自分がどこにいるのかがわかるからです。並んでいます
。でも、前にいる人たちは自分が何位なのか分からないので、どうすればいいでしょうか?彼は、最初の人が尋ねられるまで質問を続けることもでき、その後、最初の人から私まで、私が何番目であるかを正確に知ることができます。上記のシナリオは典型的な再帰です。このプロセスで法則性は見つかりましたか? 次に、質問するプロセスがあり、最初の質問を終えて戻るプロセスがあります。これはパス(尋ねる)プラスリターン(返す)です。では、このプロセスを解決するために数式を使用できるでしょうか? では、この数式は何でしょうか?
f(n)=f(n-1)+1
f(n): 私の位置を示します
f(n-1): 私の目の前の人を意味します。
自分に電話してください
再帰的なアプリケーションのシナリオ
-
一个问题的解可以分解为几个子问题的解
: 部分問題、分割統治の考え方を通じて、大規模なデータ問題を多くの小さな問題に分解できます。
ただいまの質問者は副質問と考えてよろしいかと思います。大きいものから小さいものまで -
この問題と分解後の副問題、
求解思路完全一样
-
一定有一个最后确定的答案,即递归的终止条件
:今の質問は一人称の方でした。最初の人は、自分がどこにランク付けされているか、つまり n=1 のときを知っている必要がありますが、そのような機能がなければ、再帰は無限ループになり、最終的にプログラムはスタックからオーバーフローしてしまいます。
再帰の時間と空間の複雑さの分析
再帰ツリーを分析するための例としてフィボナッチ数列を取り上げます: f(n)=f(n-1)+f(n-2)
時間計算量と空間計算量の両方: O(2^n)=>O(n) または O(nlogn)
再帰的最適化
- 非再帰を使用する: すべての再帰コードは理論的には非再帰コードに変換できます。
- キャッシュに参加します。再帰を o(n) に減らすことができるように、中間の操作の結果を保存します。
- 末尾再帰: 末尾再帰とは何ですか? 末尾再帰とは、呼び出し関数が他の操作を行わずに最後に現れる必要があることを意味します。コンパイラーがコードをコンパイルするときに、関数の最後に操作がないことが判明すると、その時点では新しいスタックを作成せず、先頭に上書きします。逆算すると、毎回中間結果が得られるため、後戻りする必要はありません。
// 斐波纳契/微信分销系统
// 1 1 2 3 5 8 13
// f(n) = f(n-1) + f(n-2)
// 递归
// 递归优化
// 1. 使用非递归
// 2. 加入缓存
// 3. 尾递归
// 递归注意事项----栈溢出和时间问题
public class FibonacciSeq {
//递归 40 : 102334155 耗时:316 ms
public static int fab(int n) {
// 时间复杂度/空间复杂度 O(2^n) => 如何优化
if (n <= 2) return 1;
return fab(n - 1) + fab(n - 2);
}
//尾递 第一个优化 40 : 102334155 耗时:0 ms
public static int noFab(int n) {
// 不使用递归-节约空间
if (n <= 2) return 1;
int a = 1;
int b = 1;
int c = 0;
for (int i = 3; i <= n; i ++) {
c = a + b;
a = b;
b = c;
}
return c;
}
//尾递 40 : 102334155 耗时:0 ms
public static int noFabSimple(int data[], int n) {
// 不使用递归--占用空间
if (n <= 2) return 1;
data[1] = 1;
data[2] = 1;
for (int i = 3; i <= n; i ++) {
data[i] = data[i - 1] + data[i - 2];
}
return data[n];
}
//尾递 40 : 102334155 耗时:0 ms
public static int fab2(int data[], int n) {
if (n <= 2) return 1;
if (data[n] > 0) return data[n];
int res = fab2(data,n-1) + fab2(data,n-2);
data[n] = res;
return res;
}
//尾递: 改成尾递归 prepre 上上一次运行结果 pre 上次运行结果
public static int tailFab2(int n, int prepre, int pre) {
if (n <= 2) return pre;
return tailFab2(n - 1, pre, pre + prepre);
}
//求N的阶乘 用普通的递归怎么写 5=5*4*3*2*1 => f(n) = n * f(n-1)
public static int fac(int n) {
if (n <= 1) return 1;
return n * fac(n - 1);
}
//改成尾递归 求N的阶乘 用普通的递归怎么写 5=5*4*3*2*1 => f(n) = n * f(n-1)
public static int tailFac(int n, int res) {
//尾递归
if (n <= 1) return res;
return tailFac(n-1, n * res);
}
public static void main(String[] args) {
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + fab(i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + noFab(i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// data = new int[41];
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + noFabSimple(data, i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// data = new int[41];
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + fab2(data, i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// for (int i = 1; i <= 40; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + tailFab2(i, 1, 1) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
//
// for (int i = 1; i <= 10; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + tailFac(i, 1) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
//
// for (int i = 1; i <= 10; i++) {
// long start = System.currentTimeMillis();
// System.out.println(i + " : " + fac(i) + " 耗时:" + (System.currentTimeMillis() - start) + " ms");
// }
}
}
/**
递归
1 : 1 耗时:0 ms
2 : 1 耗时:0 ms
3 : 2 耗时:0 ms
4 : 3 耗时:0 ms
5 : 5 耗时:0 ms
6 : 8 耗时:0 ms
7 : 13 耗时:0 ms
8 : 21 耗时:0 ms
9 : 34 耗时:0 ms
10 : 55 耗时:0 ms
11 : 89 耗时:0 ms
12 : 144 耗时:0 ms
13 : 233 耗时:0 ms
14 : 377 耗时:0 ms
15 : 610 耗时:0 ms
16 : 987 耗时:1 ms
17 : 1597 耗时:0 ms
18 : 2584 耗时:0 ms
19 : 4181 耗时:0 ms
20 : 6765 耗时:0 ms
21 : 10946 耗时:0 ms
22 : 17711 耗时:0 ms
23 : 28657 耗时:1 ms
24 : 46368 耗时:0 ms
25 : 75025 耗时:0 ms
26 : 121393 耗时:1 ms
27 : 196418 耗时:1 ms
28 : 317811 耗时:1 ms
29 : 514229 耗时:1 ms
30 : 832040 耗时:4 ms
31 : 1346269 耗时:4 ms
32 : 2178309 耗时:7 ms
33 : 3524578 耗时:11 ms
34 : 5702887 耗时:17 ms
35 : 9227465 耗时:30 ms
36 : 14930352 耗时:50 ms
37 : 24157817 耗时:90 ms
38 : 39088169 耗时:145 ms
39 : 63245986 耗时:242 ms
40 : 102334155 耗时:316 ms
不使用递归
0 : 1 耗时:0 ms
1 : 1 耗时:0 ms
2 : 1 耗时:0 ms
3 : 2 耗时:0 ms
4 : 3 耗时:0 ms
5 : 5 耗时:0 ms
6 : 8 耗时:0 ms
7 : 13 耗时:0 ms
8 : 21 耗时:0 ms
9 : 34 耗时:0 ms
10 : 55 耗时:0 ms
11 : 89 耗时:0 ms
12 : 144 耗时:0 ms
13 : 233 耗时:0 ms
14 : 377 耗时:0 ms
15 : 610 耗时:0 ms
16 : 987 耗时:0 ms
17 : 1597 耗时:0 ms
18 : 2584 耗时:0 ms
19 : 4181 耗时:0 ms
20 : 6765 耗时:0 ms
21 : 10946 耗时:0 ms
22 : 17711 耗时:0 ms
23 : 28657 耗时:0 ms
24 : 46368 耗时:0 ms
25 : 75025 耗时:0 ms
26 : 121393 耗时:0 ms
27 : 196418 耗时:0 ms
28 : 317811 耗时:0 ms
29 : 514229 耗时:0 ms
30 : 832040 耗时:0 ms
31 : 1346269 耗时:0 ms
32 : 2178309 耗时:0 ms
33 : 3524578 耗时:0 ms
34 : 5702887 耗时:0 ms
35 : 9227465 耗时:0 ms
36 : 14930352 耗时:0 ms
37 : 24157817 耗时:0 ms
38 : 39088169 耗时:0 ms
39 : 63245986 耗时:0 ms
40 : 102334155 耗时:0 ms
*/
ソートパフォーマンス分析
- 時間効率: アルゴリズムの実行時間を決定します (O(1))。
- 空間の複雑さ
- 比較と交換
- 安定性
1 9 *3 5 3
第一种:1 *3 3 5 9
第二种:1 3 *3 5 9
同じ、相対的な位置は変化しません。安定したソートのポイントは何ですか? 申請書はどこにありますか? 例:EC注文の仕分け(小口から大口まで、注文時間に応じて同量)
挿入ソート
次の例を見てください: 7 8
9 0 4 3 9での挿入ソート
public static int[] insertSort(int arr[]) {
for (int i = 1; i < arr.length; i++) {
int curr = arr[i];
int pre = i - 1;
for (; pre >= 0 ; pre--) {
if (curr < arr[pre]) {
arr[pre+1] = arr[pre];
} else {
break;
}
}
arr[pre+1] = curr;
}
return arr;
}
ヒルソート
インクリメンタルセグメンテーション: add=n/2 n=10 =>5,2,1
public static void shellSort(int data[]) {
int n = data.length;
for (int add = n/2; add >= 1 ; add/=2) {
for (int i = add; i < n; i++) {
int temp = data[i];
int j = i - add;
for (; j >= 0; j-=add) {
if (data[j] > temp) {
data[j + add] = data[j];
} else {
break;
}
}
data[j+add] = temp;
}
}
}
マージソート
public static void mergeSort(int[] arr, int left, int right) {
if (left < right) {
int mid = (left + right)/2;
mergeSort(arr, left, mid);
mergeSort(arr, mid+1, right);
merge(arr, left, mid, right);
}
}
public static void merge(int[] arr, int left, int mid, int right) {
int temp[] = new int[arr.length];
int leftPoint = left;
int rightPoint = mid + 1;
int cur = left; //当前位置
while (leftPoint <= mid && rightPoint <= right) {
if (arr[leftPoint] < arr[rightPoint]) {
temp[cur] = arr[leftPoint];
leftPoint++;
} else {
temp[cur] = arr[rightPoint];
rightPoint++;
}
cur++;
}
while (leftPoint <= mid) {
temp[cur++] = arr[leftPoint++];
}
while (rightPoint <= right) {
temp[cur++] = arr[rightPoint++];
}
for (int i = left; i <= right; i++) {
arr[i] = temp[i];
}
}
選ぶ
選択ソートの考え方は挿入ソートと非常に似ており、ソートされた間隔とソートされていない間隔にも分けられます。ただし、選択並べ替えでは、毎回未並べ替えの間隔から最小の要素が検索され、並べ替えられた間隔の最後に配置されます。ただし、挿入ソートとは異なり、配列は移動され、選択ソートは毎回交換されます。
public static int[] selectSort(int data[]) {
for (int i = 0; i < data.length -1; i++) {
int minloc = i;
for (int j = i+1; j < data.length; j++) {
if (data[j] < data[minloc]) {
minloc = j;
}
}
int minTemp = data[minloc];
data[minloc] = data[i];
data[i] = minTemp;
}
return data;
}
バブル
中心的なアイデア: バブル ソートは 2 つの隣接するデータに対してのみ機能します。各バブリング操作では、隣接する 2 つの要素を比較して、サイズ関係の要件を満たしているかどうかを確認します。そうでない場合は、2 つを交換します。1 回のバブリングにより、少なくとも 1 つの要素が本来あるべき場所に移動し、これを n 回繰り返すと、n データのソートが完了します。
最初のバブリングの結果: 4 5 6 3 2 1 -> 4 5 3 6 2 1 -> 4 5 3 2 6 1 -> 4 5 3 2 1 6、どの要素の位置が決定されるか、2 番目のバブリングは 6
time バブリングの結果: 4 5 3 2 1 6 -> 4 3 5 2 1 6 -> 4 3 2 5 1 6 -> 4 3 2 1 5 6 3 回目のバブリングの結果: 4 3 2 1 5
6- >3 4 2 1 5 6 ->
3 2 4 1 5 6 -> 3 2 1 4 5 6 4 番目のバブルの結果: 3 2 1 4 5 6 -> 2 3 1 4 5 6 -> 2 1 3 4 5 6 5
回目のバブリングの結果: 2 1 3 4 5 6 -> 1 2 3 4 5 6
public static int[] dubbleSort(int data[]) {
for (int i = 0; i < data.length; i++) {
for (int j = i+1; j < data.length; j++) {
if (data[j] < data[i]) {
int temp = data[j];
data[j] = data[i];
data[i] = temp;
}
}
}
return data;
}
public static void dubbleSortTest(int arr[]) {
for (int i = 0; i < arr.length-1; i++) {
boolean flag = false;
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
flag = true;
}
}
if (!flag) break;
}
}
クイックロー
クイックソートとマージの比較:
- マージ ソートの処理はボトムアップで、最初にサブ問題を処理してからマージします。
- クイックソートは実際には上から下に行われ、最初に分割し、次にマージせずにサブ問題に対処します。
最適化とは、ベンチマーク数値を最適化し、3 つの数値の中央を取るというアイデアを提供することです。
45 28 80 90 50 16 100 10
ベンチマーク番号: 通常、これは並べ替えられるシーケンスの最初の番号です。
最初のソート基準番号:45
後ろから前へ基準番号より小さい番号を探して入れ替える:
*10 28 80 90 50 16 100 *45
前から後ろへ基準番号より大きい番号を探して入れ替える:
10 28 * 45 90 50 16 100 * 80
。。。
基準番号で 3 つの部分に分割し、左側の比率が小さく、右側の比率が大きくなります: {
10 28 16} 45 {50 90 100 80}
45桁の参照番号。
public static void quickSort(int data[], int left, int right) {
int base = data[left]; //基准数
int ll = left; //从左边找的位置
int rr = right; //从右边找的位置
while (ll < rr) {
//从右边找比基数小的数
while (ll < rr && data[rr] >= base) {
rr--;
}
if (ll < rr) {
//表示有找到比之大的
int temp = data[rr];
data[rr] = data[ll];
data[ll] = temp;
ll++;
}
while (ll < rr && data[ll] <= base) {
ll++;
}
if (ll < rr) {
int temp = data[rr];
data[rr] = data[ll];
data[ll] = temp;
rr--;
}
}
if (left < ll)
quickSort(data, left, ll-1);
if (ll < right)
quickSort(data, ll+1, right);
}
比較した
貪欲なアルゴリズム
コンセプト
概念: 貪欲アルゴリズムは貪欲アルゴリズムとも呼ばれ、特定の問題を解決すると、常に最も直接的な利益が得られます。
つまり、現在だけを考えて全体の状況を無視するため、局所最適解になります。核心点: 局所最適から大域最適を導き出す
貪欲なアルゴリズムのルーチン: ソートが必要です。ハフマン符号化、貪欲アルゴリズム、圧縮アルゴリズム。最短経路
考える質問
1. ある朝、会社のリーダーから問題を解決してほしいと頼まれました。明日、会社では同じ会議室を使用する必要がある同じレベルの会議が N 件あります。今、あなたには N 件の会議の開始時刻と終了時刻が与えられています。 . 会議室をどのように配置して
最大限に活用しますか? つまり、最も多くのセッションがスケジュールされていますか? 映画の場合は、チケットが最も多く、入場率も最も高い場所でなければなりません。包括的なアルゴリズム
2. もうすぐダブルイレブンが近づいてきます。Xiao C の心の中の女神は N 個のアイテムをショッピング カートに追加しました。そして突然、彼女は 5,000 元のアイテム (お釣りなし) でショッピング カートを空にする賞品を獲得しました。各アイテムは購入できる場合のみです。 1 つ目は、賞金を最大化するにはどのようにアイテムを選択すればよいでしょうか? 最適な組み合わせが複数ある場合は 1 つだけ答えればいいのですが、さあ、女神があなたに問いかけています、どうすればいいですか?
/**
* 贪心算法
* 最优
* 最短
* 最好
*
* 先按照开始时间排序, 之后按照当前开始时间,比较开始时间是否大于结束时间
*/
public class Meeting implements Comparable<Meeting> {
int meNum;
int startTime;
int endTime;
public Meeting(int meNum, int startTime, int endTime) {
super();
this.meNum = meNum;
this.startTime = startTime;
this.endTime = endTime;
}
@Override
public int compareTo(Meeting o) {
if (this.endTime > o.endTime) {
return 1;
}
return -1;
}
@Override
public String toString() {
return "GreedySort{" +
"meNum=" + meNum +
", startTime=" + startTime +
", endTime=" + endTime +
'}';
}
/**
* 4
* 0 9
* 8 10
* 10 12
* 8 20
* GreedySort{meNum=1, startTime=0, endTime=9}
* GreedySort{meNum=3, startTime=10, endTime=12}
*/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
List<Meeting> meetings = new ArrayList<>();
int n = scanner.nextInt(); //会议
for (int i = 0; i < n; i++) {
int start = scanner.nextInt();
int end = scanner.nextInt();
Meeting meeting = new Meeting(i+1, start, end);
meetings.add(meeting);
}
meetings.sort(null);
int curTime = 0; //当前时间,从一天的0点开始
for (int i = 0; i < n; i++) {
Meeting meeting = meetings.get(i);
if (meeting.startTime >= curTime) {
System.out.println(meeting.toString());
curTime = meeting.endTime;
}
}
}
}
動的プログラミング
思考の問題 - バックパックの問題: ある泥棒が、容量 50kg のバックパックを盗もうと店に行きました。現在、彼は次のアイテムを持っています (アイテムは分割できず、アイテムは 1 つだけです)。最大値を取得しますか?
もの | 重さ | 価値 |
---|---|---|
項目 1 | 10kg | 60ドル |
項目 2 | 20kg | 100元 |
項目 3 | 40kg | 120元 |
5kg袋
もの | 重さ | 価値 |
---|---|---|
項目 1 | 1 | 6 |
項目 2 | 2 | 10 |
項目 3 | 4 | 12 |
5kgの袋を1kgに分けてこのように計算すると、中の表は現在の重量で積み込める最大のお金を示しています。表内の列の番号はロードされるアイテムを表します。
もの | 1kg | 2kg | 3kg | 4kg | 5kg |
---|---|---|---|---|---|
アイテム1を追加 | 6 | 6 | 6 | 6 | 6 |
項目2を追加 | 6 | 10 | 10+6=16 | 10+6=16 | 16 |
項目3を追加 | 6 | 10 | 16 | 16 | 18 |
/**
* 背包算法
* 购物车问题保存价值一样就可以
*/
public class Backpack {
public static List<Integer> group(int dp[][], int good_list[]) {
int value_max = dp[0].length - 1;
int good_max = dp.length - 1;
List<Integer> good_group = new ArrayList();
while (value_max > 0 && good_max > 0) {
if (dp[good_max][value_max] <= dp[good_max-1][value_max]) {
good_max -= 1;
} else {
good_group.add(good_max);
value_max -= good_list[good_max-1];
good_max -= 1;
}
}
return good_group;
}
public static int cart(int weight[], int lw) {
int n = weight.length;
int dp[][] = new int[n+1][lw+1]; //n表示物品、w表示重量,初始化全是0
for (int i = 1; i <= n; i++) {
//每次加的物品
for (int w = 1; w <= lw; w++) {
if (weight[i-1] <= w) {
//当前物品重量小于分割重量 表示这个物品可以装进去
dp[i][w] = Math.max(weight[i-1] + dp[i-1][w-weight[i-1]], dp[i-1][w]);
} else {
dp[i][w] = dp[i-1][w];
}
}
}
List<Integer> good_group = group(dp, weight);
System.out.print("组合:");
for (Integer integer : good_group) {
System.out.print(integer + "\t");
}
System.out.println();
return dp[n][lw];
}
public static int backpack(int value[], int weight[], int lw) {
int n = weight.length;
int dp[][] = new int[n+1][lw+1]; //n表示物品、w表示重量,初始化全是0
for (int i = 1; i <= n; i++) {
//每次加的物品
for (int w = 1; w <= lw; w++) {
if (weight[i-1] <= w) {
//当前物品重量小于分割重量 表示这个物品可以装进去
dp[i][w] = Math.max(value[i-1] + dp[i-1][w-weight[i-1]], dp[i-1][w]);
} else {
dp[i][w] = dp[i-1][w];
}
}
}
List<Integer> good_group = group(dp, weight);
System.out.print("组合:");
for (Integer integer : good_group) {
System.out.print(integer + "\t");
}
System.out.println();
return dp[n][lw];
}
/**
* 组合:4 3 1
* 8
* 组合:3 1
* 180
*/
public static void main(String[] args) {
System.out.println(cart(new int[]{
1,2,3,4,5,9},8));
System.out.println(backpack(new int[]{
60, 100, 120},new int[]{
10, 20, 30},40));
}
}