資料結構與演算法——基礎篇(一)
前置問題
經典問題與演算法
-
8皇后問題(92種擺法)——回溯演算法
-
字串匹配問題——KMP演算法(取代暴力匹配)
-
漢諾塔遊戲問題——分治演算法
-
馬踏棋盤演算法也稱騎士周遊問題——圖的深度優化遍歷演算法(DFS)+貪心演算法優化
-
Josephu——約瑟夫問題(丟手帕問題)
-
修路問題——最小生成樹(普里姆演算法)
-
最短路徑問題——圖+弗洛伊德演算法
程式設計師常用十大演算法——必會
- 二分查詢演算法(非遞迴)
- 分治演算法
- 動態規劃演算法
- KMP演算法
- 貪心演算法
- 普里姆演算法
- 克魯斯卡爾演算法
- 迪傑斯特拉演算法
- 弗洛伊德演算法
- 馬踏棋盤演算法
學習步驟
- 應用場景或者說提出問題
- 引出資料結構或演算法
- 剖析原理
- 分析問題實現步驟
- 程式碼實現
概述
程式=資料結構+演算法
演算法是程式的靈魂,資料結構是演算法的基礎。
資料結構——data structure
- 稀疏陣列
- 單連結串列
- 單向環形連結串列
資料結構包括:線性結構和非線性結構。
線性結構——一維
線性結構作為最常用的資料結構,其特點是資料元素之間存在一對一的線性關係。
線性結構有兩種不同的儲存結構,即順序儲存結構和鏈式儲存結構。
一對一的線性關係解釋:
節點與對應的儲存元素是一對一關係,如陣列的下標和儲存的元素,而像樹的節點分支下面有可能有一個或兩個甚至多個的節點,因此不少一對一關係,也就不是線性結構。
順序儲存——陣列
順序儲存的線性表稱為順序表,順序表中的儲存元素是連續的(指記憶體空間中的地址是連續的)
鏈式儲存——連結串列
鏈式儲存的線性表稱為連結串列,連結串列中的儲存元素不一定是連續的(指記憶體空間中的地址不一定是連續的,元素之間通過儲存指標如下一個元素的地址聯絡),元素節點中存放資料元素以及相鄰元素的地址資訊。連結串列的好處的可以充分利用碎片記憶體,不需要整塊完整的記憶體空間。
常見線性結構
陣列、佇列、連結串列和棧。
非線性結構——二維及以上
常見非線性結構
二維陣列,多維陣列,廣義表,樹結構,圖結構
稀疏陣列——sparse array
當一個陣列中大部分元素為0或者為同一個值的陣列時(很多是沒有意義的資料),可以使用稀疏陣列來儲存該陣列。
稀疏陣列的處理方法是:
- 第一行記錄陣列一共有幾行幾列,有多少個不同的值。
- 從第二行開始,把具有不同值的元素的行列及值記錄在一個小規模的陣列中,從而縮小程式的規模。
圖解如下:
稀疏陣列思路分析
應用示例——五子棋壓縮成稀疏陣列儲存
public class SparseArray {
//有效總數
static AtomicInteger sum = new AtomicInteger();
public static void main(String[] args) {
testSparseAarray(buildArr(11, 11));
}
public static int[][] buildArr(int row, int col) {
int[][] chessArr = new int[row][col];
//待完善
//預設0表示沒有棋子,1表示黑子 2表示白子
chessArr[1][2] = 1;
chessArr[2][3] = 2;
chessArr[4][7] = 2;
return chessArr;
}
public static void testSparseAarray(int[][] chessArr) {
System.out.println("原始陣列:");
Arrays.stream(chessArr).forEach(chessrow -> {
//取得每行資料chessrow繼續遍歷
Arrays.stream(chessrow).forEach(chesscol -> {
System.out.printf("%d\t", chesscol);
if (chesscol != 0) {
sum.incrementAndGet();
}
});
//換行列印
System.out.println();
});
//壓縮成稀疏陣列
int[][] sparseArr = new int[sum.get() + 1][3];
sparseArr[0][0] = 11;
sparseArr[0][1] = 11;
sparseArr[0][2] = sum.get();
//第幾個有效資料
int count = 1;
for (int i = 0; i < chessArr.length; i++) {
for (int j = 0; j < chessArr.length; j++) {
if (chessArr[i][j] != 0) {
sparseArr[count][0] = i;
sparseArr[count][1] = j;
sparseArr[count][2] = chessArr[i][j];
count++;
}
}
}
System.out.println("稀疏陣列:");
//列印稀疏陣列
printArr(sparseArr);
//儲存到磁碟中
try {
Writer writer = new BufferedWriter(new FileWriter(new File("E:\\array.text")));
writer.write(JSON.toJSONString(sparseArr));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
//從磁碟中讀取陣列
try {
BufferedInputStream reader = new BufferedInputStream(new FileInputStream("E:\\array.text"));
byte[] buffer = new byte[1024 * 1024];
int read = reader.read(buffer);
reader.close();
String s = new String(buffer);
int[][] storeArr = JSON.parseObject(s, int[][].class);
System.out.println("獲取儲存在磁碟中的稀疏陣列:");
printArr(storeArr);
} catch (Exception e) {
e.printStackTrace();
}
//還原陣列
int[][] reduceArr = new int[sparseArr[0][0]][sparseArr[0][1]];
for (int i = 1; i < sparseArr.length; i++) {
reduceArr[sparseArr[i][0]][sparseArr[i][1]] = sparseArr[i][2];
}
System.out.println("還原陣列:");
printArr(reduceArr);
}
//列印陣列
private static void printArr(int[][] sparseArr) {
Arrays.stream(sparseArr).forEach(sparserow -> {
//取得每行資料chessrow繼續遍歷
Arrays.stream(sparserow).forEach(sparsecol -> {
System.out.printf("%d\t", sparsecol);
});
//換行列印
System.out.println();
});
}
}
輸出:
原始陣列:
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 2 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
稀疏陣列:
11 11 3
1 2 1
2 3 2
4 7 2
獲取儲存在磁碟中的稀疏陣列:
11 11 3
1 2 1
2 3 2
4 7 2
還原陣列:
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 2 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
佇列——FIFO
佇列的特點就是先進先出。
佇列是一個有序列表,可以用陣列(順序儲存)或是連結串列(鏈式儲存)來實現。
場景
涉及排隊的場景,如銀行排隊辦理業務取號。
陣列實現佇列
陣列實現一次性佇列
思路分析:
程式碼實現:
//一次性佇列,無法重複利用佇列,需要使用環形佇列優化
public class ArrayQueue<T> {
//陣列最大容量大小
private int maxSize;
//模擬佇列的陣列,存放元素
private Object[] arr;
//佇列頭
private int front;
//佇列尾
private int rear;
//建構函式初始化佇列
public ArrayQueue(int maxSize) {
this.maxSize = maxSize;
this.arr = new Object[maxSize];
//佇列頭和佇列尾初始化時都指向的是佇列的初始位置0的前一個位置,我們用-1表示
this.front = -1;
this.rear = -1;
}
//新增資料到佇列中
public void add(T t) throws Exception {
if (isFull()) {
System.out.println("佇列已滿!");
return;
}
rear++;
arr[rear] = t;
}
public T remove() throws Exception {
if (isEmpty()) {
System.out.println("佇列為空,取不到元素!");
return null;
}
front++;
T value = (T) arr[front];
return value;
}
public T peek() throws Exception {
if (isEmpty()) {
System.out.println("佇列為空,取不到元素!");
return null;
}
return (T) arr[front + 1];
}
public void list() throws Exception {
if (isEmpty()) {
System.out.println("佇列為空,取不到元素!");
}
for (int i = 0; i < arr.length; i++) {
System.out.printf("array[%d]:%d\n", i, (T) arr[i]);
}
System.out.println();
}
//判斷佇列是否滿了
private boolean isFull() {
return rear == (maxSize - 1);
}
//判斷佇列是否為空
private boolean isEmpty() {
return rear == front;
}
}
public class ArrayQueueTest {
public static void main(String[] args) throws Exception {
ArrayQueue<Integer> queue = new ArrayQueue<Integer>(3);
Scanner scanner = new Scanner(System.in);
boolean runFlag = true;
while (runFlag) {
String next = scanner.next();
switch (next) {
case "a":
System.out.println("請輸入新增的元素:");
//避免因輸入不是整數而中斷測試程式
try {
queue.add(scanner.nextInt());
} catch (Exception e) {
System.out.println("新增元素失敗," + e.getMessage());
}
break;
case "r":
System.out.println("取出元素:" + queue.remove());
break;
case "p":
System.out.println("檢視頭元素:" + queue.peek());
break;
case "l":
queue.list();
break;
case "e":
System.out.println("退出!");
//關閉讀取流
scanner.close();
runFlag = false;
break;
}
}
}
}
陣列實現環形佇列——通過取模優化
思路分析:
程式碼實現:
//環形佇列,核心在於取模
public class CircleQueue<T> {
//陣列最大容量大小(因為預留一個空間約定,所以最大容量為maxSize-1)
private int maxSize;
//模擬佇列的陣列,存放元素
private Object[] arr;
//佇列頭 初始化0
private int front;
//佇列尾 初始化0
private int rear;
//建構函式初始化佇列
public CircleQueue(int maxSize) {
this.maxSize = maxSize;
this.arr = new Object[maxSize];
//佇列頭和佇列尾初始化0,front指向佇列第一個元素,arr[0],rear指向最後一個元素的後一個位置,也就是當我們有一個元素arr[0]時,rear為1
this.front = 0;
this.rear = 0;
}
//新增資料到佇列中
public void add(T t) throws Exception {
if (isFull()) {
System.out.println("佇列已滿!");
return;
}
arr[rear] = t;
//rear後移並要求取模,否則陣列角標越界導致不能迴圈利用
rear = (rear + 1) % maxSize;
}
public T remove() throws Exception {
if (isEmpty()) {
System.out.println("佇列為空,取不到元素!");
return null;
}
T value = (T) arr[front];
front = (front + 1) % maxSize;
return value;
}
public T peek() throws Exception {
if (isEmpty()) {
System.out.println("佇列為空,取不到元素!");
return null;
}
return (T) arr[front];
}
public void list() throws Exception {
if (isEmpty()) {
System.out.println("佇列為空,取不到元素!");
}
for (int i = front; i < size() + front; i++) {
System.out.printf("array[%d]:%d\n", i%maxSize, (T) arr[i%maxSize]);
}
System.out.println();
}
//判斷佇列是否滿了
private boolean isFull() {
// (7+1)%4==0
//注意當rear的最大值為maxSize-1,因為陣列的下標是從0開始的。因此只要rear+1對maxSize取模得到的值為front=0,則表示滿了
return (rear + 1) % maxSize == front;
}
//判斷佇列是否為空
private boolean isEmpty() {
return rear == front;
}
//當前有效元素個數
private int size() {
//當前的有效個數就是rear-front,因為有一個有效元素,rear就是1,剛好作為計數單位。取模
return (rear + maxSize - front) % maxSize;
}
}
public class CircleQueueTest {
public static void main(String[] args) throws Exception {
//切換成環形佇列
CircleQueue<Integer> queue = new CircleQueue<Integer>(4);//有效空間是3,因為有一個空間作為約定
Scanner scanner = new Scanner(System.in);
boolean runFlag = true;
while (runFlag) {
String next = scanner.next();
switch (next) {
case "a":
System.out.println("請輸入新增的元素:");
//避免因輸入不是整數而中斷測試程式
try {
queue.add(scanner.nextInt());
} catch (Exception e) {
System.out.println("新增元素失敗," + e.getMessage());
}
break;
case "r":
System.out.println("取出元素:" + queue.remove());
break;
case "p":
System.out.println("檢視頭元素:" + queue.peek());
break;
case "l":
queue.list();
break;
case "e":
System.out.println("退出!");
//關閉讀取流
scanner.close();
runFlag = false;
break;
}
}
}
}
待優化:Java中的陣列實現佇列是如何實現的呢?ArrayBlockingQueue
連結串列
連結串列是有序的列表,但是它在記憶體中是儲存如下圖,也就是說連結串列在記憶體中的各個節點不一定是連續的儲存地址空間。
連結串列邏輯圖:
小結:
- 連結串列是以節點的方式來儲存,是鏈式儲存
- 每個節點包含 data 域, next 域:指向下一個節點.
- 如圖:發現連結串列的各個節點不一定是連續儲存(儲存空間地址上).
- 連結串列分帶頭節點的連結串列和沒有頭節點的連結串列,根據實際的需求來確定
單連結串列——單向連結串列
單連結串列增刪改查思路分析——注意temp節點的位置
單連結串列面試題
單連結串列的常見面試題有如下:
求單連結串列中有效節點的個數
查詢單連結串列中的倒數第k個結點 【新浪面試題】
單連結串列的反轉【騰訊面試題,有點難度】
從尾到頭列印單連結串列 【百度,要求方式1:反向遍歷 。 方式2:Stack棧】
合併兩個有序的單連結串列,合併之後的連結串列依然有序
單連結串列的反轉思路分析
從尾到頭列印單連結串列思路分析
單連結串列的增刪改查及常見面試題示例
//實現Comparable是為了比較用
public class City implements Comparable<City> {
private int no;
private String name;
private String nickName;
public City(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
@Override
public String toString() {
return "City{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
@Override
public int compareTo(City o) {
return this.no - o.no;
}
}
//單連結串列
//E extends Comparable 用於排序對比
public class SingleLinkedList<E extends Comparable> {
//頭節點定義
private Node<E> head;
//統計有效元素個數,不包含頭節點
private int count;
public Node<E> getHead() {
return head;
}
public SingleLinkedList() {
//標記頭結點
head = new Node<E>(null);
}
//從尾到頭列印單連結串列 【百度,要求方式1:反向遍歷(不建議,會破壞原來的單連結串列的結構,除非又執行一次反轉再反轉回去) 。 方式2:Stack棧】
//此處採用Stack棧,不會改變原來的單連結串列的結構
public void printReverseLinkedList(SingleLinkedList.Node<E> head) {
if (head.next == null) {
return;
}
Stack<Node<E>> stack = new Stack<>();
Node<E> cur = head.next;
while (cur != null) {
stack.push(cur);
cur = cur.next;
}
while (!stack.empty()) {
System.out.println(stack.pop());
}
// while (stack.size() > 0) {
// System.out.println(stack.pop());
// }
}
//單連結串列的反轉
public void reverseLinkedList(SingleLinkedList.Node<E> head) {
if (head.next == null) {
return;
}
Node<E> reverseHead = new Node<E>(null);
Node<E> cur = head.next;
Node<E> next = null;
while (cur != null) {
//暫存當前節點的下一個節點,避免被覆蓋
next = cur.next;
//把當前節點的下一個節點替換為反轉後的頭結點的第一個元素,這一步相當於連結串列的頭插入的前一步
//把cur的下一個節點指向新的連結串列的最前端,相當於實現了反轉
cur.next = reverseHead.next;
//把當前節點作為頭節點賦值給反轉後的頭結點,而上一步中我們已經將之前反轉後的頭結點賦值給了當前節點的下一個節點了
//把cur賦值到新的連結串列頭節點上
reverseHead.next = cur;
//繼續遍歷,把暫存的下一個節點賦值作為當前節點繼續迴圈
cur = next;
}
head.next = reverseHead.next;
}
//查詢單連結串列中的倒數第k個結點,找不到返回null
//k表示倒數第k個結點
public <E> SingleLinkedList.Node<E> getReverseNodeByK(int k, SingleLinkedList.Node<E> head) {
if (head.next == null) {
return null;
}
//倒數第k個結點 = 總的個數-K
//temp作為遍歷輔助變數
Node<E> temp = head.next;
int count = getListCount(head);
//校驗傳入的k值的正確性
if (k <= 0 || k > count) {
return null;
}
for (int i = 0; i < count - k; i++) {
temp = temp.next;
}
return temp;
}
//統計有效元素個數,不包含頭節點
public <E> int getListCount(SingleLinkedList.Node<E> head) {
if (head.next == null) {
return 0;
}
//定義輔助變數
Node<E> temp = head.next;
int count = 0;
while (temp != null) {
count++;
temp = temp.next;
}
return count;
}
public int getCount() {
return count;
}
/**
* 根據no序號刪除元素
*
* @param newElement
*/
public void delete(E newElement) {
//連結串列為空則無資料刪除
if (head.next == null) {
System.out.println("連結串列為空!");
return;
}
//這裡的temp節點設定位置為要刪除節點的前一個節點,這樣才能做到刪除
Node<E> temp = head;
boolean delete = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.item.compareTo(newElement) == 0) {
delete = true;
break;
}
temp = temp.next;
}
if (delete) {
temp.next = temp.next.next;
count--;
} else {
System.out.println("找不到對應的元素可以刪除!");
}
}
/**
* 根據no序號修改元素名稱等資訊
*
* @param newElement
*/
public void update(E newElement) {
//連結串列為空則無需修改
if (head.next == null) {
System.out.println("連結串列為空!");
return;
}
Node<E> temp = head.next;
boolean update = false;
while (true) {
if (temp == null) {
break;
}
if (temp.item.compareTo(newElement) == 0) {
update = true;
break;
}
temp = temp.next;
}
if (update) {
temp.item = newElement;
} else {
System.out.println("%d找不到對應的元素可以修改!\n" + newElement);
}
}
/**
* 按新增順序新增元素
*
* @param e
*/
public void add(E e) {
//head節點不能動,建立一個區域性變數輔助遍歷
Node temp = head;
while (true) {
//找到連結串列的最後,新的元素跟在後面
if (temp.next == null) {
break;
}
//沒到最後,繼續將temp後移
temp = temp.next;
}
//當退出while迴圈時,表示temp指向了連結串列的最後一個元素
temp.next = new Node<E>(e);
count++;
}
/**
* 按no大小順序新增元素,我們讓泛型E實現comparable介面,方便我們實現比較判斷
* 元素no大小相等表示存在,則新增失敗列印提示
*
* @param e
*/
public void addByOrder(E e) {
//head節點不能動,建立一個區域性變數輔助遍歷
Node<E> temp = head;
//標記條件的元素是否存在,存在則新增失敗列印提示
boolean isExist = false;
while (true) {
//主要,因為是單連結串列,所以我們找的temp節點位置為要新增位置的前一個節點,否則新增不了
//已到連結串列最後
if (temp.next == null) {
break;
}
//找到了順序儲存的位置,跳出
if (temp.next.item.compareTo(e) > 0) {
break;
}
//相同元素表示存在,也退出
if (temp.next.item.compareTo(e) == 0) {
isExist = true;
break;
}
//沒到最後,繼續將temp後移
temp = temp.next;
}
if (isExist) {
System.out.printf("%d元素已存在,不能再新增!\n", e);
}
//插入新元素到temp後面,temp.next元素改為跟在newNode節點元素後面
Node<E> newNode = new Node<>(e);
newNode.next = temp.next;
temp.next = newNode;
count++;
}
//遍歷
public void list() {
//先判斷連結串列是否有資料
if (head.next == null) {
System.out.println("連結串列為空!");
return;
}
Node temp = head.next;
while (true) {
if (temp == null) {
break;
} else {
System.out.println(temp);
}
temp = temp.next;
}
}
public static class Node<E> {
private E item;
private Node<E> next;
public Node(E e) {
this.item = e;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
'}';
}
}
}
//測試
public class SingleLinkedListTest {
public static void main(String[] args) {
reverseLinkedListTest();
}
//從尾到頭列印單連結串列
public static void printReverseLinkedListTest() {
SingleLinkedList<City> linkedList = new SingleLinkedList<>();
linkedList.add(new City(1, "北京", "帝都"));
linkedList.add(new City(4, "深圳", "自由天堂"));
linkedList.add(new City(3, "廣州", "南都"));
linkedList.add(new City(2, "上海", "魔都"));
linkedList.list();
System.out.println("============反轉後===================");
linkedList.printReverseLinkedList(linkedList.getHead());
/**
* Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* ============反轉後===================
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
* Node{item=City{no=1, name='北京', nickName='帝都'}}
*/
}
//單連結串列的反轉
public static void reverseLinkedListTest() {
SingleLinkedList<City> linkedList = new SingleLinkedList<>();
linkedList.add(new City(1, "北京", "帝都"));
linkedList.add(new City(4, "深圳", "自由天堂"));
linkedList.add(new City(3, "廣州", "南都"));
linkedList.add(new City(2, "上海", "魔都"));
linkedList.list();
System.out.println("============反轉後===================");
linkedList.reverseLinkedList(linkedList.getHead());
linkedList.list();
/**
* Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* ============反轉後===================
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
* Node{item=City{no=1, name='北京', nickName='帝都'}}
*/
}
//查詢單連結串列中的倒數第k個結點
public static void getReverseNodeByKTest() {
SingleLinkedList<City> linkedList = new SingleLinkedList<>();
linkedList.add(new City(1, "北京", "帝都"));
linkedList.add(new City(4, "深圳", "自由天堂"));
linkedList.add(new City(3, "廣州", "南都"));
linkedList.add(new City(2, "上海", "魔都"));
System.out.println(linkedList.getReverseNodeByK(1,linkedList.getHead()));
//Node{item=City{no=2, name='上海', nickName='魔都'}}
}
//求單連結串列中有效節點的個數
public static void getListCountTest() {
SingleLinkedList<City> linkedList = new SingleLinkedList<>();
linkedList.add(new City(1, "北京", "帝都"));
linkedList.add(new City(4, "深圳", "自由天堂"));
linkedList.add(new City(3, "廣州", "南都"));
linkedList.add(new City(2, "上海", "魔都"));
//方式1:在SingleLinkedList中定義計數count,在新增和刪除時做對應操作
System.out.println(linkedList.getCount());
//方式2:
SingleLinkedList.Node<City> head = linkedList.getHead();
System.out.println("單連結串列中有效節點的個數:" + linkedList.getListCount(head));
/**
* 4
* 單連結串列中有效節點的個數:4
*/
}
public static void testAdd() {
SingleLinkedList<City> linkedList = new SingleLinkedList<>();
linkedList.add(new City(1, "北京", "帝都"));
linkedList.add(new City(4, "深圳", "自由天堂"));
linkedList.add(new City(3, "廣州", "南都"));
linkedList.add(new City(2, "上海", "魔都"));
linkedList.list();
/**
*Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
*/
}
public static void testAddByOrder() {
//測試按照編號插入連結串列
SingleLinkedList<City> linkedList = new SingleLinkedList<>();
linkedList.addByOrder(new City(1, "北京", "帝都"));
linkedList.addByOrder(new City(4, "深圳", "自由天堂"));
linkedList.addByOrder(new City(3, "廣州", "南都"));
linkedList.addByOrder(new City(2, "上海", "魔都"));
linkedList.list();
/**
* Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
*/
}
public static void testUpdate() {
SingleLinkedList<City> linkedList = new SingleLinkedList<>();
linkedList.addByOrder(new City(1, "北京", "帝都"));
linkedList.addByOrder(new City(4, "深圳", "自由天堂"));
linkedList.addByOrder(new City(3, "廣州", "南都"));
linkedList.addByOrder(new City(2, "上海", "魔都"));
linkedList.list();
linkedList.update(new City(4, "深圳特區", "勇敢自由天堂"));
System.out.println("修改後遍歷:");
linkedList.list();
linkedList.update(new City(5, "杭州", "奮鬥之都"));
/**
Node{item=City{no=1, name='北京', nickName='帝都'}}
Node{item=City{no=2, name='上海', nickName='魔都'}}
Node{item=City{no=3, name='廣州', nickName='南都'}}
Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
修改後遍歷:
Node{item=City{no=1, name='北京', nickName='帝都'}}
Node{item=City{no=2, name='上海', nickName='魔都'}}
Node{item=City{no=3, name='廣州', nickName='南都'}}
Node{item=City{no=4, name='深圳特區', nickName='勇敢自由天堂'}}
找不到對應的元素可以修改!
*/
}
public static void testDelete() {
SingleLinkedList<City> linkedList = new SingleLinkedList<>();
linkedList.addByOrder(new City(1, "北京", "帝都"));
linkedList.addByOrder(new City(4, "深圳", "自由天堂"));
linkedList.addByOrder(new City(3, "廣州", "南都"));
linkedList.addByOrder(new City(2, "上海", "魔都"));
linkedList.list();
linkedList.delete(new City(4, "深圳", "自由天堂"));
linkedList.delete(new City(1, "北京", "帝都"));
System.out.println("刪除後遍歷:");
linkedList.list();
linkedList.delete(new City(5, "杭州", "奮鬥之都"));
System.out.println(linkedList.getCount());
/**
*Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
* 刪除後遍歷:
* Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* 找不到對應的元素可以刪除!
*
* Process finished with exit code 0
*/
}
}
雙向連結串列
單向連結串列的缺點:
單向連結串列,查詢的方向只能是一個方向,而雙向連結串列可以向前或者向後查詢。
單向連結串列不能自我刪除,需要靠輔助節點找到要刪除的前一個節點 ,而雙向連結串列可以自我刪除,所以前面我們單連結串列刪除時節點,總是找到temp,temp是待刪除節點的上一個節點。
雙向連結串列增刪改查思路分析
程式碼示例
public class DDoubleLinkedList<E extends Comparable> {
private Node<E> head;
public DDoubleLinkedList() {
head = new Node<E>(null);
}
public static class Node<E> {
private E item;
//指向下一個節點,預設為null
private Node<E> next;
//指向上一個節點,預設為null
private Node<E> pre;
public Node(E e) {
this.item = e;
}
@Override
public String toString() {
return "Node{" +
"item=" + item +
'}';
}
}
//遍歷
public void list() {
if (head.next == null) {
System.out.println("連結串列為空!");
return;
}
Node<E> temp = head.next;
while (temp != null) {
System.out.println(temp);
temp = temp.next;
}
}
//在連結串列尾新增元素
public void add(E e) {
Node<E> temp = head;
while (temp.next != null) {
temp = temp.next;
}
Node<E> newNode = new Node<>(e);
//形成雙向連結串列
temp.next = newNode;
newNode.pre = temp;
}
/**
* 根據no序號修改元素名稱等資訊
*
* @param newElement
*/
public void update(E newElement) {
//連結串列為空則無需修改
if (head.next == null) {
System.out.println("連結串列為空!");
return;
}
Node<E> temp = head.next;
boolean update = false;
while (true) {
if (temp == null) {
break;
}
if (temp.item.compareTo(newElement) == 0) {
update = true;
break;
}
temp = temp.next;
}
if (update) {
temp.item = newElement;
} else {
System.out.println("%d找不到對應的元素可以修改!\n" + newElement);
}
}
//雙向連結串列刪除節點,只需要直接找到要刪除的節點,而不是找到要刪除節點的前一個節點,因為雙向連結串列可以自我刪除
public void delete(E e) {
if (head.next == null) {
System.out.println("連結串列為空!");
return;
}
Node<E> temp = head.next;
boolean deleteFlag = false;
while (temp != null) {
//如果要用equals進行判斷,則需要重寫新增元素的equals方法
if (Objects.equals(temp.item, e)) {
deleteFlag = true;
break;
}
temp = temp.next;
}
if (deleteFlag) {
//刪除最後一個節點時不需要執行
if (temp.next != null) {
temp.next.pre = temp.pre;
}
temp.pre.next = temp.next;
} else {
System.out.println("找不到對應的元素可以刪除!");
}
}
}
//實現Comparable是為了比較用
public class City implements Comparable<City> {
private int no;
private String name;
private String nickName;
public City(int no, String name, String nickName) {
this.no = no;
this.name = name;
this.nickName = nickName;
}
@Override
public String toString() {
return "City{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
@Override
public int compareTo(City o) {
return this.no - o.no;
}
//重寫equals和hashCode方法比較相等,用於刪除和修改
//我們這裡只有no相等就是相等的
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
City city = (City) o;
return no == city.no;
}
@Override
public int hashCode() {
return Objects.hash(no);
}
}
//測試
public class DoubleLinkedListTest {
public static void main(String[] args) {
testUpdate();
}
public static void testUpdate() {
DDoubleLinkedList<City> linkedList = new DDoubleLinkedList<>();
linkedList.add(new City(1, "北京", "帝都"));
linkedList.add(new City(4, "深圳", "自由天堂"));
linkedList.add(new City(3, "廣州", "南都"));
linkedList.add(new City(2, "上海", "魔都"));
linkedList.list();
linkedList.update(new City(4, "深圳特區", "勇敢自由天堂"));
System.out.println("修改後遍歷:");
linkedList.list();
linkedList.update(new City(5, "杭州", "奮鬥之都"));
/**
* Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* 修改後遍歷:
* Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=4, name='深圳特區', nickName='勇敢自由天堂'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* %d找不到對應的元素可以修改!
* City{no=5, name='杭州', nickName='奮鬥之都'}
*/
}
public static void testAdd() {
DDoubleLinkedList<City> linkedList = new DDoubleLinkedList<>();
linkedList.add(new City(1, "北京", "帝都"));
linkedList.add(new City(4, "深圳", "自由天堂"));
linkedList.add(new City(3, "廣州", "南都"));
linkedList.add(new City(2, "上海", "魔都"));
linkedList.list();
/**
*Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
*/
}
public static void testDelete() {
DDoubleLinkedList<City> linkedList = new DDoubleLinkedList<>();
linkedList.add(new City(1, "北京", "帝都"));
linkedList.add(new City(4, "深圳", "自由天堂"));
linkedList.add(new City(3, "廣州", "南都"));
linkedList.add(new City(2, "上海", "魔都"));
linkedList.list();
linkedList.delete(new City(4, "深圳", "自由天堂"));
linkedList.delete(new City(2, "上海", "魔都"));
// linkedList.delete(new City(4, "深圳1", "自由天堂1"));
// linkedList.delete(new City(1, "北京1", "帝都1"));
System.out.println("刪除後遍歷:");
linkedList.list();
linkedList.delete(new City(5, "杭州", "奮鬥之都"));
/**
* Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=4, name='深圳', nickName='自由天堂'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* Node{item=City{no=2, name='上海', nickName='魔都'}}
* 刪除後遍歷:
* Node{item=City{no=1, name='北京', nickName='帝都'}}
* Node{item=City{no=3, name='廣州', nickName='南都'}}
* 找不到對應的元素可以刪除!
*/
}
}
環形連結串列——單向環形連結串列——約瑟夫環——Josephu問題——丟手帕問題
應用場景
Josephu(約瑟夫、約瑟夫環) 問題Josephu 問題為:設編號為1,2,… n的n個人圍坐一圈,約定編號為k(1<=k<=n)的人從1開始報數,數到m 的那個人出列,它的下一位又從1開始報數,數到m的那個人又出列,依次類推,直到所有人出列為止,由此產生一個出隊編號的序列。
提示:用一個不帶頭結點的迴圈連結串列來處理Josephu 問題:先構成一個有n個結點的單迴圈連結串列,然後由k結點起從1開始計數,計到m時,對應結點從連結串列中刪除,然後再從被刪除結點的下一個結點又從1開始計數,直到最後一個結點從連結串列中刪除演算法結束。
約瑟夫環示意圖
出圈思路分析圖
程式碼示例——使用單向環形連結串列解決約瑟夫問題
注:也可以用陣列取模來解決約瑟夫問題
//單向環形連結串列
public class CircleSingleLinkedList<E> {
private Node<E> first;
public Node<E> getFirst() {
return first;
}
public CircleSingleLinkedList() {
}
/**
* @param num 表示一個多少個人
* @param k 從第幾個人開始數
* @param m 每次數幾個人
*/
public static void Josephu(int num, int k, int m) {
if (num < 1 || k > num) {
System.out.println("輸入的資料不對!");
}
//建立指定大小num的約瑟夫環
CircleSingleLinkedList linkedList = new CircleSingleLinkedList();
for (int i = 1; i <= num; i++) {
linkedList.add(new Person(i, "小孩" + i));
}
System.out.println("原始環形單向連結串列初始值:");
linkedList.list();
//建立輔助變數helper,使其指向最後一個節點,相當於單連結串列中要刪除的節點的前一個節點作用
Node helper = linkedList.getFirst();
Node first = linkedList.getFirst();
while (helper.getNext() != linkedList.getFirst()) {
helper = helper.getNext();
}
//從第k個人開始數
for (int i = 0; i < k - 1; i++) {
first = first.next;
helper = helper.next;
}
//開始報數時,讓first和helper同時移動m-1次,然後出圈,知道圈內只有一個節點,這時候helper=first
while (true) {
if (first == helper) {
break;
}
for (int j = 0; j < m - 1; j++) {
first = first.next;
helper = helper.next;
}
//每次數完m次報數,這時候first就是要出圈的人
//我們讓其出圈
System.out.println("出圈人:" + first);
first = first.next;
helper.next = first;
}
System.out.println("最後出圈人:" + first);
}
public void add(E e) {
if (first == null) {
Node<E> node = new Node<>(e);
//第一個節點自己指向自己,構成環狀結構
first = node;
node.next = first;
} else {
Node<E> temp = first;
while (temp != null) {
if (temp.next == first) {
break;
}
temp = temp.next;
}
Node<E> node = new Node<>(e);
temp.next = node;
node.next = first;
}
}
public void list() {
if (first == null) {
System.out.println("連結串列為空!");
return;
}
Node<E> temp = this.first;
while (temp.next != first) {
System.out.println(temp);
temp = temp.next;
}
//輸出最後一個或者只有一個的情況
System.out.println(temp);
}
public static class Node<E> {
private E e;
private Node<E> next;
public E getE() {
return e;
}
public Node<E> getNext() {
return next;
}
public Node(E e) {
this.e = e;
}
@Override
public String toString() {
return "Node{" +
"e=" + e +
'}';
}
}
}
//物件
public class Person {
private int no;
private String name;
public Person(int no, String name) {
this.no = no;
this.name = name;
}
public int getNo() {
return no;
}
public void setNo(int no) {
this.no = no;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
//測試
public class CircleSingleLinkedListTest {
public static void main(String[] args) {
CircleSingleLinkedList.Josephu(5,1,2);
}
/**
* 原始環形單向連結串列初始值:
* Node{e=Person{no=1, name='小孩1'}}
* Node{e=Person{no=2, name='小孩2'}}
* Node{e=Person{no=3, name='小孩3'}}
* Node{e=Person{no=4, name='小孩4'}}
* Node{e=Person{no=5, name='小孩5'}}
* 出圈人:Node{e=Person{no=2, name='小孩2'}}
* 出圈人:Node{e=Person{no=4, name='小孩4'}}
* 出圈人:Node{e=Person{no=1, name='小孩1'}}
* 出圈人:Node{e=Person{no=5, name='小孩5'}}
* 最後出圈人:Node{e=Person{no=3, name='小孩3'}}
*/
}
棧——stack——FILO
棧是一個先入後出(FILO-First In Last Out)的有序列表。
棧(stack)是限制線性表中元素的插入和刪除只能線上性表的同一端進行的一種特殊線性表。允許插入和刪除的一端,為變化的一端,稱為棧頂(Top),另一端為固定的一端,稱為棧底(Bottom)。
根據棧的定義可知,最先放入棧中元素在棧底,最後放入的元素在棧頂,而刪除元素剛好相反,最後放入的元素最先刪除,最先放入的元素最後刪除。
出棧(pop)和入棧(push)
運用場景
請輸入一個表示式 -> 計算式:[7*2*2-5+1-5+3-3]
對計算機而言接受到的是個字串,其計算機底層就是通過棧運算得到結果 。
-
子程式的呼叫(函式方法的呼叫):在跳往子程式前,會先將下個指令的地址存到堆疊中,直到子程式執行完後再將地址取出,以回到原來的程式中。
-
處理遞迴呼叫:和子程式的呼叫類似,只是除了儲存下一個指令的地址外,也將引數、區域變數等資料存入堆疊中。
表示式的轉換[中綴表示式轉字尾表示式]與求值(實際解決)。 -
二叉樹的遍歷。
-
圖形的深度優先(depth一first)搜尋法。
-
子彈夾
棧的增刪改查示例——陣列實現棧
思路分析
程式碼實現
public class ArrayStack<E> {
//棧的大小
private int maxSize;
//陣列模擬棧,資料放在陣列中
private Object[] stack;
//top表示棧頂,初始化為-1
private int top = -1;
public <E> ArrayStack(int maxSize) {
this.maxSize = maxSize;
stack = new Object[maxSize];
}
//棧滿
private boolean isFull() {
return top == maxSize - 1;
}
//棧空
public boolean isEmpty() {
return top == -1;
}
//遍歷
public void list() {
if (isEmpty()) {
System.out.println("棧是空的,沒有資料");
return;
}
for (int i = top; i >= 0; i--) {
System.out.printf("stack[%d]=%d\t", i, stack[i]);
}
/* while (true) {
if (top == -1) {
break;
}
System.out.println(stack[top]);
top--;
}*/
}
//入棧
public E peek() {
if (isEmpty()) {
System.out.println("棧是空的,沒有資料");
return null;
}
return (E) stack[top];
}
//入棧
public void push(E e) {
if (isFull()) {
System.out.println("棧滿了");
return;
}
top++;
stack[top] = e;
}
//出棧
public E pop() {
if (isEmpty()) {
System.out.println("棧是空的,沒有資料");
return null;
}
E val = (E) stack[top];
top--;
return val;
}
}
public class ArrayStackTest {
public static void main(String[] args) {
testArrayStack();
}
public static void testArrayStack() {
ArrayStack<Integer> stack = new ArrayStack<>(10);
for (int i = 1; i <= 15; i++) {
stack.push(i);
}
//列印stack
stack.list();
}
/**
* 棧滿了
* 棧滿了
* 棧滿了
* 棧滿了
* 棧滿了
* stack[9]=10 stack[8]=9 stack[7]=8 stack[6]=7 stack[5]=6 stack[4]=5 stack[3]=4 stack[2]=3 stack[1]=2 stack[0]=1
*/
}
注:也可以用個連結串列實現,頭插法實現LIFO。
public class LinkedStack<E> {
private LinkedList<E> linkedList;
public LinkedStack() {
this.linkedList = new LinkedList<E>();
}
public void push(E e) {
linkedList.addFirst(e);
}
public E pop() {
return linkedList.getFirst();
}
public void list(){
while (linkedList.peekFirst()!= null) {
System.out.printf("%d\t",linkedList.poll());
}
}
}
public class ArrayStackTest {
public static void main(String[] args) {
testLinkedStack();
}
public static void testLinkedStack() {
LinkedStack<Integer> stack = new LinkedStack<>();
for (int i = 1; i <= 15; i++) {
stack.push(i);
}
//列印stack
stack.list();
//15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
}
}
棧實現綜合計算器——中綴表示式
思路分析
程式碼實現——多位數計算器
public class Calculator {
//資料棧
private ArrayStack<Integer> numStack;
//符合棧
private ArrayStack<Character> operStack;
//擴充套件通過正規表示式匹配資料
private String numRegex = "^([1-9][0-9]*)$";
public Calculator() {
numStack = new ArrayStack<Integer>(20);
operStack = new ArrayStack<Character>(20);
}
//計算器,先乘除後加減,從左到右依次運算
public Integer calculate(String expression) {
if (StringUtils.isEmpty(expression)) {
System.out.println("輸入的表示式不正確");
return null;
}
int num1 = 0;
int num2 = 0;
int result = 0;
char oper = 0;
String keepNum = "";
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
if (isOperate(c)) {
if (operStack.isEmpty()) {
operStack.push(c);
} else {
//當操作符優先順序小於等於棧中優先順序時,先計算棧中的操作符
//注意要用while迴圈判斷,因為可能不止一個
while (operStack.peek() != null && priority(c) <= priority(operStack.peek())) {
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
result = cal(num1, num2, oper);
numStack.push(result);
}
operStack.push(c);
}
} else {
// 個位計算器用, -48或者-'0'字元都是為了得到正確的int值
// numStack.push((int) c - 48);
// numStack.push(c - '0');
if (i + 1 < expression.length() && !isOperate(expression.charAt(i + 1))) {
keepNum = keepNum + c;
continue;
} else {
keepNum = keepNum + c;
numStack.push(Integer.parseInt(keepNum));
keepNum = "";
}
}
}
//當操作符棧不為空時進行運算
while (!operStack.isEmpty()) {
num1 = numStack.pop();
num2 = numStack.pop();
oper = operStack.pop();
result = cal(num1, num2, oper);
numStack.push(result);
}
return numStack.pop();
}
//判斷是否是操作符
private boolean isOperate(char ch) {
return ch == '+' || ch == '-' || ch == '*' || ch == '/';
}
//操作符優先順序判斷,乘除是1,加減是0
private int priority(char ch) {
if (ch == '*' || ch == '/') {
return 1;
} else if (ch == '+' || ch == '-') {
return 0;
} else {
return -1;
}
}
//資料運算
private int cal(int num1, int num2, char oper) {
switch (oper) {
case '+':
return num1 + num2;
case '-':
return num2 - num1;
case '*':
return num1 * num2;
case '/':
return num2 / num1;
default:
throw new RuntimeException("操作符有誤!");
}
}
}
//ArrayStack 參考上面
//測試
public class CalculatorTest {
public static void main(String[] args) {
Calculator calculator = new Calculator();
// Integer calculate = calculator.calculate("3+2*6-2");//13
// Integer calculate = calculator.calculate("113+2*6-2");//123
Integer calculate = calculator.calculate("1+2*6*2-2");//23
System.out.println("計算結果為:" + calculate);
}
}
棧的三種表示式——字首表示式、中綴表示式、字尾表示式
字首表示式和字尾表示式之所以適合計算機運算,就是它不包含括號,不需要像中綴表示式那樣判斷運算子的優先順序,這種需要判斷對計算機來說並不方便,而是直接從左到右或者從右到左依次運算即可。因此,在計算結果時,往往會將中綴表示式轉成其它表示式來操作(一般轉成字尾表示式.)
字首表示式——Prefix Expression——波蘭表示式—— Polish Expression
字首表示式又稱波蘭式,字首表示式的運算子位於運算元之前。
比如: - × + 3 4 5 6
字首表示式的計算機求值
從右至左掃描表示式,遇到數字時,將數字壓入堆疊,遇到運算子時,彈出棧頂的兩個數,用運算子對它們做相應的計算(棧頂元素 op 次頂元素),並將結果入棧;重複上述過程直到表示式最左端,最後運算得出的值即為表示式的結果
- 例如: - × + 3 4 5 6
- 從右至左掃描,將6、5、4、3壓入堆疊
- 遇到+運算子,因此彈出3和4(3為棧頂元素,4為次頂元素,注意與字尾表示式做比較),計算出3+4的值,得7,再將7入棧
- 接下來是×運算子,因此彈出7和5,計算出7×5=35,將35入棧
- 最後是-運算子,計算出35-6的值,即29,由此得出最終結果
中綴表示式——Inffix Expression
中綴表示式就是常見的運算表示式,如(3+4)×5-6 。
也就是我們生活中寫的四則運算表示式,是我們最熟悉的運算表示式,是包含括號的。
字尾表示式——Suffix Expression——逆波蘭表示式—— Inverse Polish Expression——適合計算機計算
字尾表示式又稱逆波蘭表示式,與字首表示式相似,只是運算子位於運算元之後
比如:3 4 + 5 × 6 -
字尾表示式計算機求值
與字首表示式類似,只是順序是從左至右:
從左至右掃描表示式,遇到數字時,將數字壓入堆疊,遇到運算子時,彈出棧頂的兩個數,用運算子對它們做相應的計算(次頂元素 op 棧頂元素),並將結果入棧;重複上述過程直到表示式最右端,最後運算得出的值即為表示式的結果
例如字尾表示式“3 4 + 5 × 6 -”:
- 從左至右掃描,將3和4壓入堆疊;
- 遇到+運算子,因此彈出4和3(4為棧頂元素,3為次頂元素,注意與字首表示式做比較),計算出3+4的值,得7,再將7入棧;
- 將5入棧;
- 接下來是×運算子,因此彈出5和7,計算出7×5=35,將35入棧;
- 將6入棧;
- 最後是-運算子,計算出35-6的值,即29,由此得出最終結果。
注意:在做減法或者除法時,彈出棧中的兩個數,第二個數或者說後彈出來的數是被減數和被除數,與字首表示式相反。
將中綴表示式轉換為字尾表示式
與轉換為字首表示式相似,步驟如下:
- 初始化兩個棧:運算子棧s1和儲存中間結果的棧s2;
- 從左至右掃描中綴表示式;
- 遇到運算元時,將其壓s2;
- 遇到運算子時,比較其與s1棧頂運算子的優先順序:
- 如果s1為空,或棧頂運算子為左括號“(”,則直接將此運算子入棧;
- 否則,若優先順序比棧頂運算子的高,也將運算子壓入s1(注意轉換為字首表示式時是優先順序較高或相同,而這裡則不包括相同的情況);
- 否則,將s1棧頂的運算子彈出並壓入到s2中,再次轉到(4-1)與s1中新的棧頂運算子相比較;
- 遇到括號時:
- 如果是左括號“(”,則直接壓入s1;
- 如果是右括號“)”,則依次彈出s1棧頂的運算子,並壓入s2,直到遇到左括號為止,此時將這一對括號丟棄;
- 重複步驟2至5,直到表示式的最右邊;
- 將s1中剩餘的運算子依次彈出並壓入s2;
- 依次彈出s2中的元素並輸出,結果的逆序即為中綴表示式對應的字尾表示式(轉換為字首表示式時不用逆序)
例如,將中綴表示式“1+((2+3)×4)-5”轉換為字尾表示式的過程如下:
掃描到的元素 | s2(棧底->棧頂) | s1 (棧底->棧頂) | 說明 |
---|---|---|---|
1 | 1 | 空 | 數字,直接入棧 |
+ | 1 | + | s1為空,運算子直接入棧 |
( | 1 | + ( | 左括號,直接入棧 |
( | 1 | + ( ( | 同上 |
2 | 1 2 | + ( ( | 數字 |
+ | 1 2 | + ( ( + | s1棧頂為左括號,運算子直接入棧 |
3 | 1 2 3 | + ( ( + | 數字 |
) | 1 2 3 + | + ( | 右括號,彈出運算子直至遇到左括號 |
× | 1 2 3 + | + ( × | s1棧頂為左括號,運算子直接入棧 |
4 | 1 2 3 + 4 | + ( × | 數字 |
) | 1 2 3 + 4 × | + | 右括號,彈出運算子直至遇到左括號 |
- | 1 2 3 + 4 × + | - | -與+優先順序相同,因此彈出+,再壓入- |
5 | 1 2 3 + 4 × + 5 | - | 數字 |
到達最右端 | 1 2 3 + 4 × + 5 - | 空 | s1中剩餘的運算子 |
因此結果為“1 2 3 + 4 × + 5 -”
逆波蘭計算器程式碼示例
字尾計算器——逆波蘭計算器實現
//逆波蘭計算器
public class InversePolishExpressionCalculator {
public static void main(String[] args) {
// String suffixExpression = "1 2 3 + 4 * + 5 -";//計算結果為:16
// 50-3*4+(5-3)*2/4 轉化 50 3 4 * - 5 3 - 2 * 4 / +
String suffixExpression = "50 3 4 * - 5 3 - 2 * 4 / +";//計算結果為:39
List<String> expressionList = getInversePolishExpressionList(suffixExpression);
int result = InversePolishExpressionCalculate(expressionList);
System.out.println("計算結果為:" + result);
}
//約定大於配置
//約定逆波蘭表示式值用空格隔開 1 2 3 + 4 * + 5 -
//把逆波蘭表示式值轉為List
public static List<String> getInversePolishExpressionList(String suffixExpression) {
String[] expressionArr = suffixExpression.split(" ");
List<String> list = Arrays.asList(expressionArr);
return list;
}
/**
* 逆波蘭表示式計算
*
* @param list
* @return
*/
public static int InversePolishExpressionCalculate(List<String> list) {
Stack<String> stack = new Stack<>();
for (String item : list) {
//匹配到的是數字則直接入棧
if (item.matches("\\d+")) {
stack.push(item);
//否則為符號彈出兩個數進行運算
} else {
int num1 = Integer.valueOf(stack.pop());
int num2 = Integer.valueOf(stack.pop());
int result = 0;
switch (item) {
case "+":
result = num1 + num2;
break;
//在做減法或者除法時,彈出棧中的兩個數,第二個數或者說後彈出來的數是被減數和被除數
case "-":
result = num2 - num1;
break;
case "*":
result = num1 * num2;
break;
case "/":
result = num2 / num1;
break;
default:
throw new RuntimeException("運算子號有誤:" + item);
}
//運算結果入棧
stack.push(String.valueOf(result));
}
}
//stack棧頂的資料就是最後的運算結果
return Integer.valueOf(stack.pop());
}
}
中綴表示式轉字尾表示式示例
思路分析
程式碼示例
//中綴表示式轉字尾表示式
public class InfixExpression2SuffixExpression {
public static void main(String[] args) {
String infixExpression = "1 + ( ( 2 + 3 ) * 4 ) - 5";
List<String> suffixExpressionList = parseInfixExpression2SuffixExpression(getInfixExpressionList(infixExpression));
System.out.println("中綴表示式轉字尾表示式為:"+suffixExpressionList);
//中綴表示式轉字尾表示式為:[1, 2, 3, +, 4, *, +, 5, -]
}
//約定大於配置
//將中綴表示式字串轉List
//約定中綴表示式字串用空格隔開
public static List<String> getInfixExpressionList(String infixExpression) {
String[] expressionArr = infixExpression.split(" ");
List<String> list = Arrays.asList(expressionArr);
return list;
}
/**
* 1. 初始化兩個棧:運算子棧s1和儲存中間結果的棧s2;
* 2. 從左至右掃描中綴表示式;
* 3. 遇到運算元時,將其壓s2;
* 4. 遇到運算子時,比較其與s1棧頂運算子的優先順序:
* 1. 如果s1為空,或棧頂運算子為左括號“(”,則直接將此運算子入棧;
* 2. 否則,若優先順序比棧頂運算子的高,也將運算子壓入s1(**注意轉換為字首表示式時是優先順序較高或相同,而這裡則不包括相同的情況**);
* 3. 否則,將s1棧頂的運算子彈出並壓入到s2中,再次轉到(4-1)與s1中新的棧頂運算子相比較;
* 5. 遇到括號時:
* 1. 如果是左括號“(”,則直接壓入s1;
* 2. 如果是右括號“)”,則依次彈出s1棧頂的運算子,並壓入s2,直到遇到左括號為止,此時將這一對括號丟棄;
* 6. 重複步驟2至5,直到表示式的最右邊;
* 7. 將s1中剩餘的運算子依次彈出並壓入s2;
* 8. 依次彈出s2中的元素並輸出,**結果的逆序即為中綴表示式對應的字尾表示式(轉換為字首表示式時不用逆序)**
*
* @param infixExpressionList
* @return
*/
//中綴表示式轉字尾表示式
public static List<String> parseInfixExpression2SuffixExpression(List<String> infixExpressionList) {
//運算子棧
Stack<String> s1 = new Stack<>();
//存放結果的List
ArrayList<String> s2 = new ArrayList<>();
for (String item : infixExpressionList) {
//匹配到的是數字則直接入棧
if (item.matches("\\d+")) {
s2.add(item);
//如果是左括號“(”,則直接壓入s1;
} else if (item.equals("(")) {
s1.push(item);
//如果是右括號“)”,則依次彈出s1棧頂的運算子,並壓入s2,直到遇到左括號為止,此時將這一對括號丟棄;
} else if (item.equals(")")) {
while (!"(".equals(s1.peek())) {
s2.add(s1.pop());
}
s1.pop();//將(左括號彈出消除左括號
} else {
/**
* 4. 遇到運算子時,比較其與s1棧頂運算子的優先順序:
* 1. 如果s1為空,或棧頂運算子為左括號“(”,則直接將此運算子入棧;
* 2. 否則,若優先順序比棧頂運算子的高,也將運算子壓入s1(**注意轉換為字首表示式時是優先順序較高或相同,而這裡則不包括相同的情況**);
* 3. 否則,將s1棧頂的運算子彈出並壓入到s2中,再次轉到(4-1)與s1中新的棧頂運算子相比較;
*/
//對1,2,條件取反就進入3條件,進行while迴圈判斷
while (s1.size() != 0 && !"(".equals(s1.peek()) && OperatorPriorityEnum.getCodeFromDesc(item) <= OperatorPriorityEnum.getCodeFromDesc(s1.peek())) {
s2.add(s1.pop());
}
//還需要將運算子壓入棧
s1.push(item);
}
}
//將s1中剩餘的運算子依次彈出並壓入s2
while (s1.size() > 0) {
s2.add(s1.pop());
}
return s2;
}
}
//列舉
public enum OperatorPriorityEnum {
//列舉定義要放最前面,否則報錯
ADD(0, "+"),
SUBSTRACT(0, "-"),
MULTIPLY(1, "*"),
DEVIDE(1, "/");
private int code;
private String desc;
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
OperatorPriorityEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static Integer getCodeFromDesc(String desc) {
for (OperatorPriorityEnum enums : OperatorPriorityEnum.values()) {
if (enums.getDesc().equals(desc)) {
return enums.getCode();
}
}
return null;
}
}
逆波蘭計算器完整版
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;
public class ReversePolishMultiCalc {
/**
* 匹配 + - * / ( ) 運算子
*/
static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
static final String LEFT = "(";
static final String RIGHT = ")";
static final String ADD = "+";
static final String MINUS= "-";
static final String TIMES = "*";
static final String DIVISION = "/";
/**
* 加減 + -
*/
static final int LEVEL_01 = 1;
/**
* 乘除 * /
*/
static final int LEVEL_02 = 2;
/**
* 括號
*/
static final int LEVEL_HIGH = Integer.MAX_VALUE;
static Stack<String> stack = new Stack<>();
static List<String> data = Collections.synchronizedList(new ArrayList<String>());
/**
* 去除所有空白符
* @param s
* @return
*/
public static String replaceAllBlank(String s ){
// \\s+ 匹配任何空白字元,包括空格、製表符、換頁符等等, 等價於[ \f\n\r\t\v]
return s.replaceAll("\\s+","");
}
/**
* 判斷是不是數字 int double long float
* @param s
* @return
*/
public static boolean isNumber(String s){
Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
return pattern.matcher(s).matches();
}
/**
* 判斷是不是運算子
* @param s
* @return
*/
public static boolean isSymbol(String s){
return s.matches(SYMBOL);
}
/**
* 匹配運算等級
* @param s
* @return
*/
public static int calcLevel(String s){
if("+".equals(s) || "-".equals(s)){
return LEVEL_01;
} else if("*".equals(s) || "/".equals(s)){
return LEVEL_02;
}
return LEVEL_HIGH;
}
/**
* 匹配
* @param s
* @throws Exception
*/
public static List<String> doMatch (String s) throws Exception{
if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");
s = replaceAllBlank(s);
String each;
int start = 0;
for (int i = 0; i < s.length(); i++) {
if(isSymbol(s.charAt(i)+"")){
each = s.charAt(i)+"";
//棧為空,(操作符,或者 操作符優先順序大於棧頂優先順序 && 操作符優先順序不是( )的優先順序 及是 ) 不能直接入棧
if(stack.isEmpty() || LEFT.equals(each)
|| ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
stack.push(each);
}else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
//棧非空,操作符優先順序小於等於棧頂優先順序時出棧入列,直到棧為空,或者遇到了(,最後操作符入棧
while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
if(calcLevel(stack.peek()) == LEVEL_HIGH){
break;
}
data.add(stack.pop());
}
stack.push(each);
}else if(RIGHT.equals(each)){
// ) 操作符,依次出棧入列直到空棧或者遇到了第一個)操作符,此時)出棧
while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
if(LEVEL_HIGH == calcLevel(stack.peek())){
stack.pop();
break;
}
data.add(stack.pop());
}
}
start = i ; //前一個運算子的位置
}else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
if(isNumber(each)) {
data.add(each);
continue;
}
throw new RuntimeException("data not match number");
}
}
//如果棧裡還有元素,此時元素需要依次出棧入列,可以想象棧裡剩下棧頂為/,棧底為+,應該依次出棧入列,可以直接翻轉整個stack 新增到佇列
Collections.reverse(stack);
data.addAll(new ArrayList<>(stack));
System.out.println(data);
return data;
}
/**
* 算出結果
* @param list
* @return
*/
public static Double doCalc(List<String> list){
Double d = 0d;
if(list == null || list.isEmpty()){
return null;
}
if (list.size() == 1){
System.out.println(list);
d = Double.valueOf(list.get(0));
return d;
}
ArrayList<String> list1 = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
list1.add(list.get(i));
if(isSymbol(list.get(i))){
Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
list1.remove(i);
list1.remove(i-1);
list1.set(i-2,d1+"");
list1.addAll(list.subList(i+1,list.size()));
break;
}
}
doCalc(list1);
return d;
}
/**
* 運算
* @param s1
* @param s2
* @param symbol
* @return
*/
public static Double doTheMath(String s1,String s2,String symbol){
Double result ;
switch (symbol){
case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
default : result = null;
}
return result;
}
public static void main(String[] args) {
//String math = "9+(3-1)*3+10/2";
String math = "12.8 + (2 - 3.55)*4+10/5.0";
try {
doCalc(doMatch(math));
} catch (Exception e) {
e.printStackTrace();
}
}
}
演算法——algorithm
遞迴——recursion——俄羅斯套娃
遞迴就是方法自己呼叫自己,每次呼叫時傳入不同的變數.遞迴有助於程式設計者解決複雜的問題,同時可以讓程式碼變得簡潔。
遞迴需要遵守的重要規則
- 執行一個方法時,就建立一個新的受保護的獨立空間(棧幀空間)
- 方法的區域性變數是獨立的,不會相互影響, 比如n變數;如果方法中使用的是引用型別變數(比如陣列),就會共享該引用型別的資料.
- 遞迴必須向退出遞迴的條件逼近,否則就是無限遞迴,出現StackOverflowError,死龜了。
- 當一個方法執行完畢,或者遇到return,就會返回,遵守誰呼叫,就將結果返回給誰,同時當方法執行完畢或者返回時,該方法也就執行完畢。
場景
- 列印問題
- 階乘問題
- 走迷宮回溯
- 8皇后問題
- 球和籃子的問題
- 各種演算法中也會使用到遞迴,比如快排,歸併排序,二分查詢,分治演算法等
遞迴示例
遞迴呼叫方法圖解分析
程式碼示例
public class Recursion {
//遞迴列印
public static void recursionPrint(int n) {
if (n > 1) {
recursionPrint(n - 1);
}
System.out.println("n=" + n);
}
/**
* n=1
* n=2
* n=3
* n=4
* n=5
*/
//遞迴實現階乘
public static int factorial(int n) {
if (n > 1) {
return n * factorial(n - 1);//5*4*3*2*1
} else {
return 1;
}
}
//階乘結果為:120
public static void main(String[] args) {
//recursionPrint(5);
//階乘結果為:120
System.out.println("階乘結果為:"+factorial(5));
}
}
遞迴回溯走迷宮程式碼示例
注:在沒有使用演算法求最短路徑時,最短路徑的選擇與我們程式碼中行走的策略有關。
程式碼示例
/**
* 迷宮原始圖:
* 1 1 1 1 1 1 1 1 1 1
* 1 0 0 0 0 0 0 0 0 1
* 1 0 0 0 0 0 0 0 0 1
* 1 0 0 0 0 0 0 0 0 1
* 1 0 1 0 1 1 1 1 0 1
* 1 0 1 0 0 0 0 0 0 1
* 1 0 1 0 0 0 0 0 0 1
* 1 0 1 0 0 0 0 0 0 1
* 1 0 1 0 0 0 0 0 0 1
* 1 1 1 1 1 1 1 1 1 1
* --------------------------------
* 走出迷宮路徑:
* 1 1 1 1 1 1 1 1 1 1
* 1 2 0 0 0 0 0 0 0 1
* 1 2 0 0 0 0 0 0 0 1
* 1 2 2 2 0 0 0 0 0 1
* 1 3 1 2 1 1 1 1 0 1
* 1 3 1 2 0 0 0 0 0 1
* 1 3 1 2 0 0 0 0 0 1
* 1 3 1 2 0 0 0 0 0 1
* 1 3 1 2 2 2 2 2 2 1
* 1 1 1 1 1 1 1 1 1 1
* --------------------------------
* 走出迷宮最短路徑:
* 1 1 1 1 1 1 1 1 1 1
* 1 2 2 2 2 2 2 2 2 1
* 1 0 0 0 0 0 0 0 2 1
* 1 0 0 0 0 0 0 0 2 1
* 1 0 1 0 1 1 1 1 2 1
* 1 0 1 0 0 0 0 0 2 1
* 1 0 1 0 0 0 0 0 2 1
* 1 0 1 0 0 0 0 0 2 1
* 1 0 1 0 0 0 0 0 2 1
* 1 1 1 1 1 1 1 1 1 1
*/
public class Maze {
public static void main(String[] args) {
int[][] maze = buildMaze();
System.out.println("走出迷宮路徑:");
walkMaze(maze, 1, 1);
// System.out.println("走出迷宮最短路徑:");
// walkMazeShortcut(maze, 1, 1);
printMaze(maze);
}
/**
* 約定大於配置
* 建立二維陣列模擬迷宮
* 使用1表示牆,四周置牆
*/
public static int[][] buildMaze() {
int[][] maze = new int[10][10];
for (int i = 0; i < maze.length; i++) {
//對第0和第9行設定1表示牆
maze[0][i] = 1;
maze[9][i] = 1;
//對第0和第9列設定1表示牆
maze[i][0] = 1;
maze[i][9] = 1;
//設定其他障礙
if (3 < i) {
maze[i][2] = 1;
}
if (i > 3) {
maze[4][i] = 1;
if (i == 8) {
maze[4][i] = 0;
}
}
}
System.out.println("迷宮原始圖:");
printMaze(maze);
System.out.println("--------------------------------");
return maze;
}
/**
* 使用遞迴回溯找路
*
* @param maze 地圖
* @param //x座標
* @param //y座標
* @return 找到路返回true,否則false
* x,y表示地圖位置座標,最開始位置置為(1,1)
* 約定,從座標1,1走到8,8表示走出迷宮
* //注意因為我們定義的陣列是10,10,最外層的座標9,9已經被標記為牆,則應該設定終點為8,8才能達到,否則永遠都走不通
* //值1表示牆,2表示走完迷宮的路,3表示走不通 0表示沒走過
* 尋路策略:先向下->右->上->左
*/
public static boolean walkMaze(int[][] maze, int x, int y) {
if (maze[8][8] == 2) {
return true;
} else {
if (maze[x][y] == 0) {
maze[x][y] = 2;
//向下找,行增加,x+1
if (walkMaze(maze, x + 1, y)) {
return true;
//右
} else if (walkMaze(maze, x, y + 1)) {
return true;
//上
} else if (walkMaze(maze, x - 1, y)) {
return true;
//左
} else if (walkMaze(maze, x, y - 1)) {
return true;
} else {
//走不通
maze[x][y] = 3;
return false;
}
} else {
//不為0的情況
return false;
}
}
}
//修改策略達到最短路徑
public static boolean walkMazeShortcut(int[][] maze, int x, int y) {
if (maze[8][8] == 2) {
return true;
} else {
if (maze[x][y] == 0) {
maze[x][y] = 2;
//右
if (walkMazeShortcut(maze, x, y + 1)) {
return true;
//向下找,行增加,x+1
} else if (walkMazeShortcut(maze, x + 1, y)) {
return true;
//上
} else if (walkMazeShortcut(maze, x - 1, y)) {
return true;
//左
} else if (walkMazeShortcut(maze, x, y - 1)) {
return true;
} else {
//走不通
maze[x][y] = 3;
return false;
}
} else {
//不為0的情況
return false;
}
}
}
public static void printMaze(int[][] maze) {
for (int[] intRow : maze) {
for (int column : intRow) {
System.out.printf("%d\t", column);
}
System.out.println();
}
}
}
八皇后問題——回溯演算法
八皇后問題,是一個古老而著名的問題,是回溯演算法的典型案例。該問題是國際西洋棋棋手馬克斯·貝瑟爾於1848年提出:在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即:任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法(一共有92種)。
思路分析
說明:理論上應該建立一個二維陣列來表示棋盤,但是實際上可以通過演算法,用一個一維陣列即可解決問題. arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3} //對應arr 下標 表示第幾行,即第幾個皇后,arr[i] = val , val 表示第i+1個皇后,放在第i+1行的第val+1列
程式碼實現——本質上就是暴力匹配
/**
* 0 4 7 5 2 6 1 3
* 0 5 7 2 6 3 1 4
* 0 6 3 5 7 1 4 2
* 0 6 4 7 1 3 5 2
* 1 3 5 7 2 0 6 4
* 1 4 6 0 2 7 5 3
* 1 4 6 3 0 7 5 2
* 1 5 0 6 3 7 2 4
* 1 5 7 2 0 3 6 4
* 1 6 2 5 7 4 0 3
* 1 6 4 7 0 3 5 2
* 1 7 5 0 2 4 6 3
* 2 0 6 4 7 1 3 5
* 2 4 1 7 0 6 3 5
* 2 4 1 7 5 3 6 0
* 2 4 6 0 3 1 7 5
* 2 4 7 3 0 6 1 5
* 2 5 1 4 7 0 6 3
* 2 5 1 6 0 3 7 4
* 2 5 1 6 4 0 7 3
* 2 5 3 0 7 4 6 1
* 2 5 3 1 7 4 6 0
* 2 5 7 0 3 6 4 1
* 2 5 7 0 4 6 1 3
* 2 5 7 1 3 0 6 4
* 2 6 1 7 4 0 3 5
* 2 6 1 7 5 3 0 4
* 2 7 3 6 0 5 1 4
* 3 0 4 7 1 6 2 5
* 3 0 4 7 5 2 6 1
* 3 1 4 7 5 0 2 6
* 3 1 6 2 5 7 0 4
* 3 1 6 2 5 7 4 0
* 3 1 6 4 0 7 5 2
* 3 1 7 4 6 0 2 5
* 3 1 7 5 0 2 4 6
* 3 5 0 4 1 7 2 6
* 3 5 7 1 6 0 2 4
* 3 5 7 2 0 6 4 1
* 3 6 0 7 4 1 5 2
* 3 6 2 7 1 4 0 5
* 3 6 4 1 5 0 2 7
* 3 6 4 2 0 5 7 1
* 3 7 0 2 5 1 6 4
* 3 7 0 4 6 1 5 2
* 3 7 4 2 0 6 1 5
* 4 0 3 5 7 1 6 2
* 4 0 7 3 1 6 2 5
* 4 0 7 5 2 6 1 3
* 4 1 3 5 7 2 0 6
* 4 1 3 6 2 7 5 0
* 4 1 5 0 6 3 7 2
* 4 1 7 0 3 6 2 5
* 4 2 0 5 7 1 3 6
* 4 2 0 6 1 7 5 3
* 4 2 7 3 6 0 5 1
* 4 6 0 2 7 5 3 1
* 4 6 0 3 1 7 5 2
* 4 6 1 3 7 0 2 5
* 4 6 1 5 2 0 3 7
* 4 6 1 5 2 0 7 3
* 4 6 3 0 2 7 5 1
* 4 7 3 0 2 5 1 6
* 4 7 3 0 6 1 5 2
* 5 0 4 1 7 2 6 3
* 5 1 6 0 2 4 7 3
* 5 1 6 0 3 7 4 2
* 5 2 0 6 4 7 1 3
* 5 2 0 7 3 1 6 4
* 5 2 0 7 4 1 3 6
* 5 2 4 6 0 3 1 7
* 5 2 4 7 0 3 1 6
* 5 2 6 1 3 7 0 4
* 5 2 6 1 7 4 0 3
* 5 2 6 3 0 7 1 4
* 5 3 0 4 7 1 6 2
* 5 3 1 7 4 6 0 2
* 5 3 6 0 2 4 1 7
* 5 3 6 0 7 1 4 2
* 5 7 1 3 0 6 4 2
* 6 0 2 7 5 3 1 4
* 6 1 3 0 7 4 2 5
* 6 1 5 2 0 3 7 4
* 6 2 0 5 7 4 1 3
* 6 2 7 1 4 0 5 3
* 6 3 1 4 7 0 2 5
* 6 3 1 7 5 0 2 4
* 6 4 2 0 5 7 1 3
* 7 1 3 0 6 4 2 5
* 7 1 4 2 0 6 3 5
* 7 2 0 5 1 4 6 3
* 7 3 0 2 5 1 6 4
* 一共有92種擺放方法
* 一共有92種擺放方法一共判斷了15720次
*/
public class Queen {
public static void main(String[] args) {
Queen queen = new Queen(8);
queen.showQueenMagic();
System.out.printf("一共有%d種擺放方法", queen.getCount());//92
System.out.printf("一共判斷了%d次",queen.getJudgeCount());//15720
}
//表示玩的是幾個皇后
private int max;
////對應arr 下標 表示第幾行,即第幾個皇后,arr[i] = val , val 表示第i+1個皇后,放在第i+1行的第val+1列
private int[] arr;
//累計擺放方法數
private int count;
//一共判斷了多少次
private int judgeCount;
public int getJudgeCount() {
return judgeCount;
}
public int getCount() {
return count;
}
public Queen(int max) {
this.max = max;
arr = new int[max];
}
public void showQueenMagic() {
//從第1個皇后開始放置
check(0);
}
/**
* n表示放置第n個皇后
* check是每一次遞迴時,進入check方法都會有 for (int i = 0; i < max; i++)迴圈,因此會有回溯
* 回溯就在於我們每次都把符合排列的八皇后擺法列印出來,然後程式繼續往下走,當第8個皇后擺放完畢到max時已經沒有其他的成功結果後,就會回溯到第7個皇后的下一個擺放位置,
* 繼續往下深入,當第7個皇后也擺放測試到max位置結束後,就會回溯到第6個皇后擺放位置的下一個,一直到第一個皇后從第1位擺放到第max位
* @param n
*/
public void check(int n) {
//表示進來判斷是n如果等於max比如8,則表示目前進來判斷的是第九個皇后
//也就是說第八個皇后已經放置好了
if (n == max) {
print();
count++;
return;
}
//依次在0到max中擺放皇后,判斷是否衝突
for (int i = 0; i < max; i++) {
//表示對第n個皇后的放置位置從0開始放置,並進行校驗位置放置是否滿足條件
arr[n] = i;
if (judgePlaceIsOk(n)) {
//滿足則繼續擺放第n+1個皇后
check(n + 1);
}
//不滿足則進入i++繼續判斷,繼續往下襬放
}
}
//n表示放置第n個皇后,他放置的位置與之前的0到n-1位皇后的位置進行比對是否衝突
public boolean judgePlaceIsOk(int n) {
judgeCount++;
for (int i = 0; i < n; i++) {
//斜率k=(y1-y2)/(x1-x2) ,而當兩個點在同一個斜線上時k=1,這時候得出(y1-y2) = (x1-x2)
//arr[i] == arr[n]表示在同一列上
//Math.abs(i - n) == Math.abs(arr[i] - arr[n]) 表示在同一斜線上
//同一行不需要對比,因為我們上面的for迴圈只在不同行上,因此一定不會在同一行上
if (arr[i] == arr[n] || Math.abs(i - n) == Math.abs(arr[i] - arr[n])) {
return false;
}
}
return true;
}
private void print() {
for (int q : arr) {
System.out.printf("%d\t", q);
}
System.out.println();
}
}
擴充套件:
行差等於列差,表示45°,說明在同一個斜向上。
斜率k=(y1-y2)/(x1-x2) ,而當兩個點在同一個斜線上時k=1,這時候得出(y1-y2) = (x1-x2) 。
斜率k=1時的直線: y=x+b,線與x軸夾角為+45度。
排序——Sort Algorithm
排序也稱排序演算法(Sort Algorithm),排序是將一組資料,依指定的順序進行排列的過程。
排序的分類
- 內部排序——記憶體
指將需要處理的所有資料都載入到內部儲存器中進行排序。
- 外部排序——藉助外存
資料量過大,無法全部載入到記憶體中,需要藉助外部儲存進行排序。
常見的排序演算法分類
衡量一個程式(演算法)執行時間的兩種方法
事後統計的方法
這種方法可行, 但是有兩個問題:一是要想對設計的演算法的執行效能進行評測,需要實際執行該程式;二是所得時間的統計量依賴於計算機的硬體、軟體等環境因素, 這種方式,要在同一臺計算機的相同狀態下執行,才能比較那個演算法速度更快。
事前估算的方法
通過分析某個演算法的時間複雜度來判斷哪個演算法更優.
時間複雜度——O( f(n) ) —— Time Complexity
時間頻度——T(N)
時間頻度:一個演算法花費的時間與演算法中語句的執行次數成正比例,哪個演算法中語句執行次數多,它花費時間就多。一個演算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)。
時間複雜度的計算規則
- 忽略常數項
- 忽略低次項
- 忽略係數
時間複雜度簡介
一般情況下,演算法中的基本操作語句的重複執行次數是問題規模n的某個函式,用T(n)表示,若有某個輔助函式f(n),使得當n趨近於無窮大時,T(n) / f(n) 的極限值為不等於零的常數,則稱f(n)是T(n)的同數量級函式。記作 T(n)=O( f(n) ),稱O( f(n) ) 為演算法的漸進時間複雜度,簡稱時間複雜度。
T(n) 不同,但時間複雜度可能相同。 如:T(n)=n²+7n+6 與 T(n)=3n²+2n+2 它們的T(n) 不同,但時間複雜度相同,都為O(n²)。
計算時間複雜度的方法
用常數1代替執行時間中的所有加法常數( 忽略常數項) T(n)=3n²+7n+6 => T(n)=3n²+7n+1
修改後的執行次數函式中,只保留最高階項 (忽略低次項) T(n)=3n²+7n+1 => T(n) = 3n²
去除最高階項的係數(忽略係數) T(n) = 3n² => T(n) = n² => O(n²)
常見的時間複雜度(10種)
- 常數階O(1)
- 對數階O(log2^n)
- 線性階O(n)
- 線性對數階O(nlog2^n)
- 平方階O(n^2)
- 立方階O(n^3)
- k次方階O(n^k)
- 指數階O(2^n)
- n的階乘Ο(n!)
- n的指數階O(n^n)
常見的演算法時間複雜度由小到大排列
Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)< Ο(n^k) <Ο(2^n) <Ο(n!)
隨著問題規模n的不斷增大,上述時間複雜度不斷增大,演算法的執行效率越低.
對數階O(log2^n)
對數就是指數的相反操作。
x= loga^n (a>0,a≠1),x叫做以a為底的n的對數,a為底數,n為真數。
我們要求 log2^1024,其實就是求2的幾次方=1024,也就是10。
或者,log22=1,我們可以這樣計算:log21024=log22+log22+log22+log22+log22+log22+log22+log22+log22+log22=
1+1+1+1+1+1+1+1+1+1=10
log327=3,因為33=27。注意下面i=i*3,則O(log3^n)
擴充套件
線性階O(n)
單層的for迴圈就是線性階。
線性對數階O(nlogn)
時間複雜度為O(logn)的程式碼迴圈N遍的話,那麼它的時間複雜度就是 n * O(logN),也就是了O(nlogN)。
平方階O(n²)
兩層for迴圈巢狀就是平方階
平均時間複雜度和最壞時間複雜度
- 平均時間複雜度是指所有可能的輸入例項均以等概率出現的情況下,該演算法的執行時間。
- 最壞情況下的時間複雜度稱最壞時間複雜度。一般討論的時間複雜度均是最壞情況下的時間複雜度。 這樣做的原因是:最壞情況下的時間複雜度是演算法在任何輸入例項上執行時間的界限,這就保證了演算法的執行時間不會比最壞情況更長。
- 平均時間複雜度和最壞時間複雜度是否一致,和演算法有關.
空間複雜度——Space Complexity
類似於時間複雜度的討論,一個演算法的空間複雜度(Space Complexity)定義為該演算法所耗費的儲存空間,它也是問題規模n的函式。
空間複雜度(Space Complexity)是對一個演算法在執行過程中臨時佔用儲存空間大小的量度。有的演算法需要佔用的臨時工作單元數與解決問題的規模n有關,它隨著n的增大而增大,當n較大時,將佔用較多的儲存單元,例如快速排序和歸併排序演算法、基數排序就屬於這種情況。
在做演算法分析時,主要討論的是時間複雜度。從使用者使用體驗上看,更看重的程式執行的速度。一些快取產品(redis, memcache)和演算法(基數排序)本質就是用空間換時間.
氣泡排序
其他
解決從其他地方拿到的檔案編碼有問題導致的亂碼問題處理方法
解決方法就是用記事本開啟該檔案,再另存為一份新的檔案,修改儲存的檔案格式,比如修改為UTF-8編碼格式,再把該檔案拷貝到想要的專案中或者複製過來即可。