聲明:本篇博客是博主閱讀《漫畫算法 小灰的算法之旅》所做的筆記,僅供學習,侵刪,嚴禁轉載!
(一)、數據結構
一、數組
基本操作
-
讀取元素 O(1)
根據數組下標讀取元素 --隨機讀取
-
更新元素 時間複雜度 O(1)
-
插入元素
-
尾部插入
-
中間插入 O(n)
從數組的中間插入數據,後面的數需要依次後移
//lenth 數組實際內容長度 public void insertArray(int []array, int lenth,int insertIndex,int element){ if(lenth > insertIndex || insertIndex < 0){ throw new IndexOutOfBoundsException("超出數組實際元素範圍") } for(int i = lenth;i>insertIndex;i--){ array[i] = array[i-1] } array[insertIndex]=element; lenth++; }
-
超範圍插入 O(n)+O(n)=O(n)
數組已滿,插入數據的時候,可以把數組轉移到另外一個2倍大的數組裏,再進行中間插入
-
-
刪除元素 O(n)
如果刪除的元素位於數組中間,則需要把後面的元素向前挪動一位
/* array 數組 size 數組實際最後一個元素所在的下標 index 刪除的元素所在的下標 */ public int delete(int index){ if(index<0||index > size){ return error; } int deleteElement = array[index]; for(int i = index;i<size-1;i++){ array[i]=array[i+1]; } size--; return deleteElement; }
另一種操作方法:將數組的最後一個元素複製到將要刪除的元素所在的位置,然後刪除最後一個元素,當然這不是刪除元素的主流操作方式。
優缺點
- 非常高效的隨機訪問能力(二分查找)
- 插入刪除操作不便
- 數組適合讀操作多,寫操作少的場景
二、鏈表
-
單向鏈表
private static class Node{ int data; Node next; }
-
雙向鏈表
private static class Node{ int data; Node prev; Node next; }
-
鏈表是隨機存儲的
基本操作
-
查找節點
從頭節點開始遍歷,直到查找到目標節點,最壞的時間複雜度是O(n)
-
更新節點
查找到節點後,對數據進行更新
-
插入節點
-
尾部插入
把最後一個節點的next指針指向新的節點
-
頭部插入
把新節點的next指針指向頭節點,把新節點變爲鏈表的頭節點
-
中間插入
新節點的next指針指向插入位置的節點
插入位置的前置節點的next指針指向新的節點
-
-
刪除元素
-
尾部刪除
把倒數第二節點的next指針指向空
-
頭部刪除
把頭節點的next指針指向的節點設置爲頭節點
-
中間刪除
把刪除位置的前置節點的next指針指向刪除位置的下一個節點
-
java有垃圾回收機制,對於沒有外部引用指向的節點,會被自動回收。
如果不考慮插入、刪除之前查找元素的過程,只考慮純粹的插入和刪除操作,時間複雜度都是O(1)
public class NodeList {
private static class Node{
int data;
Node next;
Node(int data){
this.data = data;
}
}
//頭節點
private Node head;
//尾部節點
private Node last;
//鏈表實際長度
private int size;
//根據data的值尋找節點
public Node find(int data){
Node findNode = head;
while (findNode!=null){
if(findNode.data == data){
return findNode;
}else{
findNode = findNode.next;
}
}
return null;
}
//根據index查找第幾個節點
public Node findIndex(int index) throws Exception{
if(index<0||index>=size){
throw new IndexOutOfBoundsException("超出鏈表節點範圍");
}
Node temp = head;
for(int i=0;i<index;i++){
temp = temp.next;
}
return temp;
}
//插入節點
public boolean insert(int data,int index)throws Exception{
if(index<0||index>size){
throw new IndexOutOfBoundsException("超出鏈表節點範圍");
}
Node nodeInsert = new Node(data);
if(size==0){
//鏈表爲空
this.head = nodeInsert;
this.last = nodeInsert;
}else if(index==0){
//插入的位置爲頭結點
nodeInsert.next = this.head;
this.head = nodeInsert;
}else if(index==size){
//插入的位置爲尾節點
this.last.next = nodeInsert;
this.last = nodeInsert;
}else{
//插入的位置爲中間位置
Node preNode = findIndex(index-1);
nodeInsert.next = preNode.next;
preNode.next=nodeInsert;
}
size++;
return true;
}
//刪除節點
public Node delete(int index)throws Exception{
if(index<0||index>=size){
throw new IndexOutOfBoundsException("超出鏈表節點範圍");
}
Node removeNode = null;
if(index==0){
//刪除頭結點
removeNode = this.head;
this.head = this.head.next;
}else if(index == size-1){
//刪除尾節點
Node prevNode = findIndex(size-1);
removeNode = this.last;
prevNode.next = null;
this.last = prevNode;
}else{
//刪除中間節點
Node prevNode = findIndex(index-1);
removeNode = prevNode.next;
prevNode.next = prevNode.next.next;
}
size--;
return removeNode;
}
}
優缺點
數組與鏈表的對比
查找 | 更新 | 插入 | 刪除 | |
---|---|---|---|---|
數組 | O(1) | O(1) | O(n) | O(n) |
鏈表 | O(n) | O(1) | O(1) | O(1) |
- 更加靈活地進行插入和刪除操作
- 適合在尾部頻繁插入、刪除元素
三、棧
-
先入後出
最早進入的元素存放的位置是棧底
最後進入的元素存放的位置是棧頂
-
基本操作 O(1)
- 入棧
- 出棧
四、隊列
-
先入先出 FIFO
出口端叫做隊頭
入口端叫做隊尾
用數組實現時,爲了入隊操作的方便,把隊尾的位置規定爲最後入隊元素的下一個位置。
-
基本操作 O(1)
- 入隊
- 出隊
用數組實現的隊列可以使用循環隊列的方式來維持隊列容量的恆定。
(隊尾下標+1)%數組長度=隊頭下標時,代表隊列已滿,隊尾指針指向的位置永遠空出一位
所以隊列最大容量=數組長度-1
public class MyQueue {
private int []array;
private int front;
private int rear;
public MyQueue(int size){
this.array = new int[size];
}
//入隊
public void inQueue(int value)throws Exception{
if((rear+1)%array.length == front){
throw new Exception("隊列已滿");
}
array[rear]=value;
rear = (rear+1)%array.length;
}
//出隊
public int outQueue()throws Exception{
if(rear == front){
throw new Exception("隊列已空");
}
int outValue = array[front];
front=(front+1)%array.length;
return outValue;
}
//輸出隊列
public void outPutQueue(){
for (int i=front;i!=rear;i=(i+1)%array.length){
System.out.println(array[i]);
}
}
}
棧和隊列的應用
- 實現遞歸的邏輯,可以使用棧來代替,因爲棧可以回溯方法,還有一個著名的應用場景是 麪包屑導航 ,使用戶在瀏覽頁面時可以輕鬆地回溯到上一級或者更上一級頁面。
- 隊列應用到多線程中,爭奪公平鎖的等待隊列。網絡爬蟲把待爬取的網站url存入隊列中。
- 雙端隊列 deque 隊頭隊尾均可入隊或出隊
- 優先隊列 基於二叉堆來實現,誰的優先級高誰優先出隊
五、散列表/哈希表 hash table
寫操作
在散列表中插入鍵值對
- 通過哈希函數,把key轉換成數組下標
- 保存到數組裏
哈希衝突
-
開放尋址法
尋址當前元素的下一個位置是否爲空,爲空則可以佔用
-
鏈表法
哈希數組的每一個元素不僅是一個Entry對象,還是一個鏈表的頭節點,當有衝突試,插入到對應的鏈表中即可。
讀操作
- 通過哈希函數,把key轉換成數組下標x
- 在數組中的下標x處找對應的元素
擴容
散列表會在多次插入後達到一定飽和度,key映射位置發生衝突的概率會提高
步驟
- 擴容,創建一個新的entry空數組,長度是原來的2倍
- 重新hash,擴容後hash規則會隨之改變,把所有的entry重新hash到新的數組中。
六、二叉樹
- 滿二叉樹
- 完全二叉樹
二叉樹的數組存儲
父節點的下標是parent
,則其左孩子節點的下標是2*parent+1
,右孩子節點下標是2*parent+2
對於稀疏的二叉樹來說,用數組表示法是非常浪費空間的。
二叉樹的應用
二叉查找樹
- 如果左子樹不爲空,則左子樹上所有節點的值均小於根節點的值
- 如果右子樹不爲空,則右子樹上所有節點的值均大於根節點的值
- 左右子樹也都是二叉查找樹
二叉排序樹
二叉排序樹也就是二叉查找樹,新插入節點時根據上述規則來插入數據,但是會造成二叉樹不平衡。解決不平衡的方式:紅黑樹,AVL樹,樹堆。
二叉樹的遍歷
1.深度優先遍歷
-
前序遍歷
根節點->左子樹->右子樹
-
中序遍歷
左子樹->根節點->右子樹
-
後序遍歷
左子樹->右子樹->根節點
public class LinkTree {
private static class TreeNode{
int data;
TreeNode leftChild;
TreeNode rightChild;
TreeNode(int data){
this.data = data;
}
}
//構建二叉樹
public static TreeNode createBinaryTree(LinkedList<Integer> inputList){
TreeNode node = null;
if(inputList==null||inputList.isEmpty()){
return null;
}
Integer data = inputList.removeFirst();
if(data != null){
node = new TreeNode(data);
node.leftChild = createBinaryTree(inputList);
node.rightChild = createBinaryTree(inputList);
}
return node;
}
//遞歸深度優先遍歷
//前序遍歷
public static void preOrderTraveral(TreeNode node){
if(node==null) return;
System.out.println(node.data);
preOrderTraveral(node.leftChild);
preOrderTraveral(node.rightChild);
}
//中序遍歷
public static void inOrderTraveral(TreeNode node){
if(node==null) return;
inOrderTraveral(node.leftChild);
System.out.println(node.data);
inOrderTraveral(node.rightChild);
}
//後序遍歷
public static void postOrderTraveral(TreeNode node){
if(node==null) return;
postOrderTraveral(node.leftChild);
postOrderTraveral(node.rightChild);
System.out.println(node.data);
}
//非遞歸深度優先遍歷
//二叉樹非遞歸前序遍歷 V1
public void preOrderTravelStackV1(TreeNode root){
if(root==null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode treeNode = root;
while(treeNode!=null || !stack.isEmpty()){
while (treeNode!=null){
System.out.println(treeNode.data);
stack.push(treeNode);
treeNode=treeNode.leftChild;
}
if(!stack.isEmpty()){
treeNode = stack.pop();
treeNode = treeNode.rightChild;
}
}
}
//二叉樹非遞歸前序遍歷 V2
public void preOrderTravelStackV2(TreeNode root){
if(root==null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode treeNode = root;
while(treeNode!=null || !stack.isEmpty()){
if(treeNode!=null){
System.out.println(treeNode.data);
stack.push(treeNode);
treeNode = treeNode.leftChild;
}else{
treeNode = stack.pop();
treeNode = treeNode.rightChild;
}
}
}
//二叉樹非遞歸前序遍歷 V3
public void preOrderTravelStackV3(TreeNode root){
if(root==null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode treeNode = root;
stack.push(treeNode);
while(!stack.isEmpty()){
System.out.println(treeNode.data);
if(treeNode.rightChild!=null){
stack.push(treeNode.rightChild);
}
if(treeNode.leftChild!=null){
treeNode = treeNode.leftChild;
}else{
treeNode = stack.pop();
}
}
}
//二叉樹非遞歸中序遍歷 v1
public void inOrderTravelStackV1(TreeNode root){
if(root==null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode treeNode = root;
while(treeNode!=null || !stack.isEmpty()){
while (treeNode!=null){
stack.push(treeNode);
treeNode=treeNode.leftChild;
}
if(!stack.isEmpty()){
treeNode = stack.pop();
System.out.println(treeNode.data);
treeNode=treeNode.rightChild;
}
}
}
//二叉樹非遞歸中序遍歷 v2
public void inOrderTravelStackV2(TreeNode root){
if(root==null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
TreeNode treeNode = root;
while (treeNode!=null || !stack.isEmpty()){
if(treeNode==null){
treeNode = stack.pop();
System.out.println(treeNode.data);
treeNode = treeNode.rightChild;
}else{
stack.push(treeNode);
treeNode = treeNode.leftChild;
}
}
}
//二叉樹非遞歸後序遍歷 v1
public void postOrderTravelStackV1(TreeNode root){
Stack<TreeNode> stack1 = new Stack<TreeNode>();
Stack<TreeNode> stack2 = new Stack<TreeNode>();
TreeNode treeNode = root;
stack1.push(treeNode);
while(!stack1.isEmpty()){
TreeNode cur = stack1.pop();
stack2.push(cur);
if(cur.leftChild!=null) stack1.push(cur.leftChild);
if(cur.rightChild!=null) stack1.push(cur.rightChild);
}
while (!stack2.isEmpty()){
System.out.println(stack2.pop().data);
}
}
//二叉樹非遞歸後序遍歷 v2
public void postOrderTravelStackV2(TreeNode root){
if(root==null) return;;
Stack<TreeNode> stack= new Stack<TreeNode>();
TreeNode lastVisit=null;
TreeNode treeNode = root;
while (treeNode!=null){
stack.push(treeNode);
treeNode = treeNode.leftChild;
}
while (!stack.isEmpty()){
treeNode = stack.pop();
//走到這裏,treeNode 都是空的,並且已經遍歷到左子樹底端
if(treeNode.rightChild == null || treeNode.rightChild == lastVisit){
System.out.println(treeNode.data);
lastVisit = treeNode;
}else {
//左子樹剛被訪問過,則需進入右子樹(根節點需再次入棧)
stack.push(treeNode);
treeNode = treeNode.rightChild;
while (treeNode!=null){
stack.push(treeNode);
treeNode = treeNode.leftChild;
}
}
}
}
public static void main(String[] args) {
LinkedList<Integer> inputList = new LinkedList<Integer>(Arrays.asList(
new Integer[]{
3,2,9,null,null,10,null,null,8,null,4
}
));
TreeNode treeNode = createBinaryTree(inputList);
System.out.println("前序");
preOrderTraveral(treeNode);
System.out.println("中序");
inOrderTraveral(treeNode);
System.out.println("後序");
postOrderTraveral(treeNode);
}
}
2.廣度優先遍歷
層序遍歷
//層序遍歷
public static void levelOrderTraversal(TreeNode root){
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()){
TreeNode node = queue.poll();
System.out.println(node.data);
if(node.leftChild!=null){
queue.offer(node.leftChild);
}
if(node.rightChild!=null){
queue.offer(node.rightChild);
}
}
}
七、二叉堆
-
最大堆
任何一個父節點的值,都大於或等於它左右孩子節點的值
-
最小堆
任何一個父節點的值,都小於或等於它左右孩子節點的值
-
堆頂
二叉堆的根節點
-
二叉堆的自我調整
-
插入節點 O(logn)
插入位置是完全二叉樹的最後一個位置,如果新節點比父節點的值小(對於最小堆來說)則讓新節點‘上浮’,和父節點交換位置。
-
刪除節點 O(logn)
刪除位置爲堆頂,把最後一個節點臨時補到原本堆頂的位置,當其比左右孩子節點中最小的一個還要大時(對於最小堆來說),則讓其‘下沉’,和最小的節點交換位置。
-
構建二叉堆 O(n)
把無序的完全二叉樹調整爲二叉堆,本質就是讓所有非葉子節點依次’下沉‘。
-
public class MyHeap {
//二叉堆雖然是一個完全二叉樹,但其使用順序存儲,均存儲在數組中
//‘上浮’調整
public static void upAdjust(int[] array){
int childIndex = array.length-1;
int parentIndex = (childIndex-1)/2;
//temp保存插入的葉子節點值,用於最後的賦值
int temp = array[childIndex];
while (childIndex>0 && temp<array[parentIndex]){
//無須真正交換,單向賦值即可
array[childIndex] = array[parentIndex];
childIndex = parentIndex;
parentIndex = (childIndex-1)/2;
}
array[childIndex]=temp;
}
//‘下沉’調整
public static void downAdjust(int []array,int parentIndex,int length){
//temp保存父節點值,用於最後的賦值
int temp = array[parentIndex];
int childIndex = 2*parentIndex+1;
while (childIndex<length){
//如果有右孩子,且右孩子的小於左孩子的值,則定位到右孩子
if(childIndex+1<length && array[childIndex+1]<array[childIndex]){
childIndex++;
}
//如果父節點小於任何一個孩子的值,則直接跳出
if(temp<=array[childIndex])
break;
//無須真正交換,單向賦值即可
array[parentIndex]=array[childIndex];
parentIndex = childIndex;
childIndex = 2*childIndex+1;
}
array[parentIndex]=temp;
}
//構建堆
public static void buildHeap(int []array){
for(int i = (array.length-2)/2;i>=0;i--){
downAdjust(array,i,array.length);
}
}
public static void main(String[] args) {
int[]array = new int[]{
1,3,2,6,5,7,8,9,10,0
};
upAdjust(array);
System.out.println(Arrays.toString(array));
array = new int[]{
7,1,3,10,5,2,8,9,6
};
buildHeap(array);
System.out.println(Arrays.toString(array));
}
}
八、優先隊列
-
最大優先隊列
無論入隊順序如何,都是當前最大的元素優先出隊
-
最小優先隊列
無論入隊順序如何,都是當前最小的元素優先出隊
-
實現
使用最大堆來實現最大優先隊列,那麼,每一次入隊就是堆的插入操作,每一次出隊,就是堆的刪除堆頂操作。
public class PriorityQueue {
private int[] array;
private int size;
public PriorityQueue(){
array = new int[32];
}
//入隊
public void enQueue(int key){
//隊列長度超出範圍,擴容
if(size>=array.length){
resize();
}
array[size++]=key;
upAdjust();
}
//出隊
public int deQueue()throws Exception{
if(size<=0){
throw new Exception("the queue is empty");
}
//獲取堆頂元素
int head = array[0];
//讓最後一個元素移動到堆頂
array[0]=array[--size];
downAdjust();
return head;
}
//上浮調整
private void upAdjust(){
int childIndex = size-1;
int parentIndex = (childIndex-1)/2;
//temp 保存插入的葉子節點值,用於最後的賦值
int temp = array[childIndex];
while (childIndex>0 && temp>array[parentIndex]){
//無須真正交換,單向賦值即可
array[childIndex]=array[parentIndex];
childIndex=parentIndex;
parentIndex=parentIndex/2;
}
array[childIndex]=temp;
}
//下沉調整
private void downAdjust(){
//temp 保存父節點的值,用於最後的賦值
int parentIndex = 0;
int temp = array[parentIndex];
int childIndex = 1;
while (childIndex<size){
//如果有右孩子,且右孩子大於左孩子的值,則定位到右孩子
if(childIndex+1<size && array[childIndex+1]>array[childIndex]){
childIndex++;
}
//如果父節點大於任何一個孩子的值,直接跳出
if(temp>=array[childIndex]){
break;
}
//無須真正交換,單向賦值即可
array[parentIndex]=array[childIndex];
parentIndex = childIndex;
childIndex = 2* childIndex +1;
}
array[parentIndex]=temp;
}
//隊列擴容
private void resize(){
int newSize = this.size*2;
this.array = Arrays.copyOf(this.array,newSize);
}
public static void main(String[] args) throws Exception {
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.enQueue(3);
priorityQueue.enQueue(5);
priorityQueue.enQueue(10);
priorityQueue.enQueue(2);
priorityQueue.enQueue(7);
System.out.println("出隊元素:"+priorityQueue.deQueue());
System.out.println("出隊元素:"+priorityQueue.deQueue());
}
}
(二)、排序算法
- O(n^2)
- 冒泡
- 選擇排序
- 插入排序
- 希爾排序
- O(nlogn)
- 快速排序
- 歸併排序
- 堆排序
- O(n)
- 計數排序
- 桶排序
- 基數排序
一、冒泡排序
public class MySort {
public static void bubbleSortV1(int array[]){
for(int i=0;i<array.length-1;i++){
for(int j=0;j<array.length-i-1;j++){
int temp =0;
if(array[j]>array[j+1]){
temp = array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
}
}
//添加flag提前停止排序
public static void bubbleSortV2(int array[]){
for(int i=0;i<array.length-1;i++){
boolean isSorted = true;
for(int j=0;j<array.length-i-1;j++){
int temp =0;
if(array[j]>array[j+1]){
temp = array[j];
array[j]=array[j+1];
array[j+1]=temp;
isSorted = false;
}
}
if(isSorted) break;
}
}
//添加無序數列的邊界,減少比對範圍
public static void bubbleSortV3(int array[]){
//記錄最後一次交換的位置
int lastChange = 0;
//無序數列的邊界,每次比較只需要比到這裏爲止
int sortBorder = array.length-1;
for(int i=0;i<array.length-1;i++){
boolean isSorted = true;
for(int j=0;j<sortBorder;j++){
int temp =0;
if(array[j]>array[j+1]){
temp = array[j];
array[j]=array[j+1];
array[j+1]=temp;
isSorted = false;
lastChange = j;
}
}
sortBorder = lastChange;
if(isSorted) break;
}
}
public static void main(String[] args) {
int []array = new int[]{
5,8,6,3,9,2,1,7
};
bubbleSortV3(array);
System.out.println(Arrays.toString(array));
}
}
二、雞尾酒排序
- 雞尾酒排序是冒泡排序的優化,元素比較和交換是雙向的
- 適合數組中大多數元素有序的情況
- 雞尾酒排序能在特定條件下減少排序的回合數,但是代碼量增多了
//雞尾酒排序
public static void cocktailSort(int array[]){
int temp=0;
for(int i =0; i<array.length/2;i++){
boolean isSort = true;
for(int j = 0;j<array.length-i-1;j++){
if(array[j]>array[j+1]){
temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
isSort = false;
}
}
if(isSort) break;
isSort=true;
for(int j=array.length-i-1;j>i;j--){
if(array[j]<array[j-1]){
temp=array[j];
array[j]=array[j-1];
array[j-1]=temp;
isSort = false;
}
}
if(isSort) break;
}
}
三、快速排序
在每一輪挑選一個基準元素,把其他比他大的元素移動到數列一邊,把比他小的元素移動到數列的另一邊,從而把數列拆成兩個部分,再進行分治排序。
-
雙邊循環法
從數組的兩邊交替遍歷元素
//快速排序-雙邊循環法
public static void quickSort(int[]array,int startIndex,int endIndex){
if(startIndex>=endIndex){
return;
}
//得到基準元素
int privotIndex = partitionDouble(array,startIndex,endIndex);
//根據基準元素,分成兩部分進行遞歸排序
quickSort(array,startIndex,privotIndex-1);
quickSort(array,privotIndex+1,endIndex);
}
public static int partitionDouble(int[] array,int startIndex,int endIndex){
//取第一個位置的元素作爲基準元素,也可以選擇隨機位置
int pivot = array[startIndex];
int left = startIndex;
int right = endIndex;
while (left!=right){
//控制right指針比較並左移
while (left<right&&array[right]>pivot){
right--;
}
while (left<right&&array[left]<=pivot){
left++;
}
if(left<right){
int p = array[left];
array[left]=array[right];
array[right]=p;
}
}
//pivot 和指針重合點交換
array[startIndex]=array[left];
array[left]=pivot;
return left;
}
-
單邊循環法
只從數組的一邊對元素進行遍歷和交換
//快速排序-單邊循環法
public static int partitionSingle(int []array,int startIndex,int endIndex){
//取第一個位置的元素作爲基準元素,也可以選擇隨機位置
int pivot = array[startIndex];
int mark = startIndex;
for(int i = startIndex+1;i<=endIndex;i++){
if(array[i]<pivot){
mark++;
int p = array[mark];
array[mark]=array[i];
array[i]=p;
}
}
array[startIndex]=array[mark];
array[mark]=pivot;
return mark;
}
非遞歸快速排序,只需要重寫quikcSort方法爲非遞歸:
//非遞歸
public static void quickSortV2(int[]arr,int startIndex,int endIndex){
//用一個集合棧來代替遞歸的函數棧
Stack<Map<String,Integer>> quickSortStack = new Stack<Map<String, Integer>>();
//整個數列的起止下標,以哈希的形式入棧
Map rootParam = new HashMap();
rootParam.put(START_INDEX_KEY,startIndex);
rootParam.put(END_INDEX_KEY,endIndex);
quickSortStack.push(rootParam);
//循環結束條件:棧爲空時
while (!quickSortStack.isEmpty()){
//棧頂元素出棧,得到起止下標
Map<String,Integer> param = quickSortStack.pop();
//得到基準元素位置
int pivotIndex = partitionSingle(arr,param.get(START_INDEX_KEY),param.get(END_INDEX_KEY));
//根據基準元素分爲兩部分,把每一部分的起止下標入棧
if(param.get(START_INDEX_KEY)<pivotIndex-1){
Map<String,Integer> leftParam = new HashMap<String, Integer>();
leftParam.put(START_INDEX_KEY,param.get(START_INDEX_KEY));
leftParam.put(END_INDEX_KEY,pivotIndex-1);
quickSortStack.push(leftParam);
}
if(pivotIndex+1<param.get(END_INDEX_KEY)){
Map<String,Integer> rightParam = new HashMap<String, Integer>();
rightParam.put(START_INDEX_KEY,pivotIndex+1);
rightParam.put(END_INDEX_KEY,param.get(END_INDEX_KEY));
quickSortStack.push(rightParam);
}
}
}
四、堆排序
算法步驟:
- 把無序數組構建成二叉堆。需要從小到達排序,則構建成最大堆;需要從大到小排序,則構建成最小堆。
- 循環刪除堆頂元素,替換到二叉堆的末尾,調整堆產生新的堆頂。
//‘下沉’調整
public static void downAdjustV2(int []array,int parentIndex,int length){
//temp保存父節點值,用於最後的賦值
int temp = array[parentIndex];
int childIndex = 2*parentIndex+1;
while (childIndex<length){
//如果有右孩子,且右孩子的大於左孩子的值,則定位到右孩子
if(childIndex+1<length && array[childIndex+1]>array[childIndex]){
childIndex++;
}
//如果父節點大於任何一個孩子的值,則直接跳出
if(temp>=array[childIndex])
break;
//無須真正交換,單向賦值即可
array[parentIndex]=array[childIndex];
parentIndex = childIndex;
childIndex = 2*childIndex+1;
}
array[parentIndex]=temp;
}
//堆排序 (升序)
public static void heapSort(int [] array){
//1.把無序數組構建成最大堆
for(int i= (array.length-2)/2;i>=0;i--){
downAdjustV2(array,i,array.length);
}
System.out.println(Arrays.toString(array));
//2.循環刪除堆頂元素,移動到集合尾部,調整堆產生新的堆頂
for(int i= array.length-1;i>0;i--){
//最後一個元素和第一個元素進行交換
int temp = array[i];
array[i]=array[0];
array[0]=temp;
downAdjustV2(array,0,i);
}
}
public static void main(String[] args) {
int[]array = new int[]{
1,3,2,6,5,7,8,9,10,0
};
heapSort(array);
System.out.println(Arrays.toString(array));
}
五、計數排序
使用數組保存數字出現過的次數,遍歷完待排序的數組後,根據統計數組再生成已排序的數組。
//計數排序
public static int[] countSort(int[]array){
//得到數列的最大值
int max = array[0];
for(int i = 1;i<array.length;i++){
if(array[i]>max) max=array[i];
}
//根據數列最大值確定統計數組的長度
int[] countArray = new int[max+1];
//遍歷數列,填充統計數組
for(int i=0;i<array.length;i++){
countArray[array[i]]++;
}
//遍歷數組,輸出結果
int index = 0;
int []sortedArray = new int[array.length];
for(int i=0;i<countArray.length;i++){
for(int j = 0;j<countArray[i];j++){
sortedArray[index++]=i;
}
}
return sortedArray;
}
public static void main(String[] args) {
int []array = new int[]{
4,4,6,5,3,2,8,1,7,5,6,0,10
};
int[]sortedArray = countSort(array);
System.out.println(Arrays.toString(sortedArray));
}
優化: 當最大值很大而需要排序的數組長度很小時,可以採用最大值-最小值
這一偏移量來初始化統計數組,減小統計數組的長度。
侷限性:
- 數列最大和最小值差距過大時不適合做計數排序
- 當數列元素不是整數時也不適合
六、桶排序
每一個桶(bucket)代表一個區間範圍,裏面可以承載一個或這多個元素。
- 創建桶
區間跨度 = (最大值 - 最小值)/(桶的數量 - 1)
-
遍歷原始數列,把元素對號入座放入各個桶中。
-
對每個桶內部的元素分別進行排序
-
遍歷所有的桶,輸出結果
0.5 0.84 2.18 3.25 4.5
//桶排序
public static double[] bucketSort(double[] array){
//1.得到數列的最大值和最小值,計算出差值d
double max = array[0];
double min = array[0];
for(int i=1;i<array.length;i++){
if(array[i]>max) max = array[i];
if(array[i]<min) min = array[i];
}
double d = max-min;
//2.初始化桶
int bucketNum = array.length;
ArrayList<LinkedList<Double>> bucketList = new ArrayList<LinkedList<Double>>(bucketNum);
for(int i=0;i<bucketNum;i++){
bucketList.add(new LinkedList<Double>());
}
//3.遍歷原始數組,將每個元素放入桶中
for(int i = 0;i<array.length;i++){
int num=(int)((array[i]-min)*(bucketNum-1)/d);
bucketList.get(num).add(array[i]);
}
//4.對每個桶內部進行排序
for(int i =0;i<bucketList.size();i++){
Collections.sort(bucketList.get(i));
}
//5.輸出全部元素
double[] sortedArray = new double[array.length];
int index =0;
for(LinkedList<Double>list:bucketList){
for(double element:list){
sortedArray[index]=element;
index++;
}
}
return sortedArray;
}
public static void main(String[] args) {
double[]array=new double[]{
4.12,6.421,0.023,3.0,2.123,8.122,4.12,10.09
};
double[] sortedArray = bucketSort(array);
System.out.println(Arrays.toString(sortedArray));
}
- 優點:時間複雜度爲O(n),空間複雜度O(n)
- 缺點:桶排序的性能並非絕對穩定,如果元素的分佈極不均衡,極端情況下,第一個桶中有n-1個元素,最後一個桶有1個元素。此時的時間複雜度將退化爲O(nlogn):
(三)、面試中的算法
一、如何判斷鏈表中有環
類似數學上的追及問題,用兩個指針,一個指針的移動速度是兩個節點,一個的速度是一個節點,從頭結點出發,當兩個節點相遇時,則證明有環。
/**
* 判斷是否有環
* @param head 鏈表頭節點
* @return
*/
public static boolean isCycle(Node head){
Node p1 = head;
Node p2 = head;
while (p2!=null && p2.next!=null){
p1=p1.next;
p2 = p2.next.next;
if(p1==p2){
return true;
}
}
return false;
}
-
如果鏈表有環,如何求出環的長度
當兩個指針首次相遇,則證明有環,讓兩個指針從相遇點繼續循環前進,並統計前進的循環次數,直到兩個指針第二次相遇,此時,統計出來的前進次數就是環長。
**環長=每一次速度差×前進次數=前進次數 **
-
如果鏈表有環,如何求出入環點
當兩個指針首次相遇時,把一個指針放回頭結點位置,另一個指針保持在首次相遇點,兩個指針都是前進一步,他們最終相遇的節點就是入環點
二、最小棧的實現
實現一個棧,該棧帶有出棧(pop)、入棧(push)、取最小元素(getMin)3個方法。要保證這3個方法的時間複雜度都是O(1)。
步驟
- 設原有的棧叫作棧A,此時創建一個額外的“備胎”棧B,用於輔助棧A
- 當第1個元素進入棧A時,讓新元素也進入棧B。這個唯一的元素是棧A的當前最小值
- 之後,每當新元素進入棧A時,比較新元素和棧A當前最小值的大小,如果小於棧A當前最小值,則讓新元素進入棧B,此時棧B的棧頂元素就是棧A當前最小值
- 每當棧A有元素出棧時,如果出棧元素是棧A當前最小值,則讓棧B的棧頂元素也出棧。此時棧B餘下的棧頂元素所指向的,是棧A當中原本第2小的元素,代替剛纔的出棧元素成爲棧A的當前最小值。(備胎轉正。)
- 當調用getMin方法時,返回棧B的棧頂所存儲的值,這也是棧A的最小值。
顯然,這個解法中進棧、出棧、取最小值的時間複雜度都是O(1),最壞情況空間複雜度是O(n)。
private Stack<Integer> mainStack = new Stack<Integer>();
private Stack<Integer> minStack = new Stack<Integer>();
/**
* 入棧操作
* @param element 入棧的元素
*/
public void push(int element){
mainStack.push(element);
//如果輔助棧爲空,或者新元素小於或等於輔助棧棧頂,則將新元素壓入輔助棧
if(minStack.empty()||element<=minStack.peek()){
minStack.push(element);
}
}
//出棧操作
public Integer pop(){
//如果出棧元素和輔助棧棧頂元素值相等,輔助棧出棧
if(mainStack.peek().equals(minStack.peek())){
minStack.pop();
}
return mainStack.pop();
}
//獲取棧的最小元素
public int getMin()throws Exception{
if(mainStack.empty())
throw new Exception("stack is empty");
return minStack.peek();
}
三、最大公約數
1. 輾轉相除法
兩個正整數a和b(a>b),它們的最大公約數等於a除以b的餘數c和b之間的最大公約數
O(log(max(a,b)))
//輾轉相除法求最大公約數
public static int getGreatestCommonDivisor(int a,int b){
int big = a>b ? a : b;
int small = a<b ? a : b;
if(big%small==0){
return small;
}
return getGreatestCommonDivisor(big%small,small);
}
2. 更相減損術
兩個正整數a和b(a>b),它們的最大公約數等於a-b的差值c和較小數b的最大公約數
O(max(a,b))
//更相減損術求最大公約數
public static int getGreatestCommonDivisorV2(int a,int b){
if(a==b) return a;
int big = a>b ? a : b;
int small = a<b ? a : b;
return getGreatestCommonDivisor(big-small,small);
}
3.更相減損術與移位相結合
O(log(max(a,b)))
//更相減損術與移位相結合
public static int gcd(int a,int b){
if(a==b) return a;
if((a&1)==0&&(b&1)==0){
return gcd(a>>1,b>>1)<<1;
}else if((a&1)==0 && (b&1)!=0){
return gcd(a>>1,b);
}else if((a&1)!=0 && (b&1)==0){
return gcd(a,b>>1);
}else{
int big = a>b?a:b;
int small = a<b?a:b;
return gcd(big-small,small);
}
}
四、判斷一個數爲2的整數次冪
對於一個整數n,只需要計算n&(n-1)的結果是不是0,是0則爲2的整數次冪
//判斷一個數爲2的整數次冪
public static boolean isPowerOf2(int num){
return (num&num-1)==0;
}
五、最大相鄰差
有一個無序整型數組,如何求出該數組排序後的任意兩個相鄰元素的最大差值?要求時間和空間複雜度儘可能低
- 利用桶排序的思想,根據原數組的長度n,創建出n個桶,每一個桶代表一個區間範圍。其中第1個桶從原數組的最小值min開始,區間跨度是(max-min)/(n-1)。
- 遍歷原數組,把原數組每一個元素插入到對應的桶中,記錄每一個桶的最大和最小值。
- 遍歷所有的桶,統計出每一個桶的最大值,和這個桶右側非空桶的最小值的差,數值最大的差即爲原數組排序後的相鄰最大差值。
//最大相鄰差
public static int getMaxSortedDistance(int []array){
//1.得到最小值和最大值
int max = array[0];
int min = array[0];
for(int i = 1;i<array.length;i++){
if(array[i]>max) max = array[i];
if(array[i]<min) min = array[i];
}
int d = max-min;
//如果max 和min相等,則說明所有元素都相等,返回0
if(d==0){
return 0;
}
//2.初始化桶
int bucketNum = array.length;
Bucket[] buckets = new Bucket[bucketNum];
for(int i = 0;i<bucketNum;i++){
buckets[i] = new Bucket();
}
//3.遍歷原始數組,確定每個桶的最大最小值
for(int i =0;i<array.length;i++){
//確定數組元素所屬的下標
int index = ((array[i]-min)*(bucketNum-1)/d);
if(buckets[index].min==null||buckets[index].min>array[i]){
buckets[index].min = array[i];
}
if(buckets[index].max==null||buckets[index].max<array[i]){
buckets[index].max = array[i];
}
}
//4.遍歷桶,找到最大差值
int leftMax = buckets[0].max;
int maxdistance = 0;
for(int i = 1;i<buckets.length;i++){
if(buckets[i].min==null){
continue;
}
if(buckets[i].min-leftMax>maxdistance){
maxdistance = buckets[i].min - leftMax;
}
leftMax = buckets[i].max;
}
return maxdistance;
}
private static class Bucket{
Integer min;
Integer max;
}
public static void main(String[] args) {
int [] array = new int[]{2,6,3,4,5,10,9};
System.out.println(getMaxSortedDistance(array));
}
六、用棧實現隊列
用兩個堆棧A和B來實現
把新元素都插入A,當第一次出隊時,把A中所有的元素依次彈出,插入到B,這樣順序就反過來了,然後從B出棧,只要B不空,所有的出隊操作從B出,當B爲空時,則再一次把A中所有的元素依次彈出,插入到B。
//用棧實現隊列
private Stack<Integer> stackA = new Stack<Integer>();
private Stack<Integer> stackB = new Stack<Integer>();
//入棧操作
public void enQueue(int element){
stackA.push(element);
}
//出棧操作
public Integer deQueue(){
if(stackB.isEmpty()){
if(stackA.isEmpty()){
return null;
}
transfer();
}
return stackB.pop();
}
//棧A轉移到棧B
public void transfer(){
while (!stackA.isEmpty()){
stackB.push(stackA.pop());
}
}
七、尋找全排列的下一個數
給出一個正整數,找出這個正整數所有數字全排列的下一個數。
如果輸入12345,則返回12354。
如果輸入12354,則返回12435。
如果輸入12435,則返回12453。
步驟:
- 從後向前查看逆序區域,找到逆序區域的前一位,也就是數字置換的邊界。
- 讓逆序區域的前一位和逆序區域中大於它的最小的數字交換位置
- 把原來的逆序區域轉爲順序狀態
//尋找全排列的下一個數
public static int[] findNearsetNumber(int[] numbers){
//1.從後向前查看逆序區域,找到逆序區域的前一位,也就是數字置換的邊界
int index = findTransferPoint(numbers);
//如果爲0,說明數組已經逆序,沒有更大的數字
if(index==0){
return null;
}
//2.把逆序區域的前一位和逆序區域中剛剛大於它的數字交換位置
int [] numbersCopy = Arrays.copyOf(numbers,numbers.length);
exchangeHead(numbersCopy,index);
//3.把原來的逆序區轉爲順序
reverse(numbersCopy,index);
return numbersCopy;
}
public static int findTransferPoint(int[] numbers){
for(int i= numbers.length-1;i>0;i--){
if(numbers[i]>numbers[i-1]){
return i;
}
}
return 0;
}
public static void exchangeHead(int[]numbers,int index){
int head = numbers[index-1];
for(int i=numbers.length-1;i>0;i--){
if(numbers[i]>head){
numbers[index-1]=numbers[i];
numbers[i]=head;
break;
}
}
}
public static int[] reverse(int []num,int index){
for(int i=index,j=num.length-1;i<j;i++,j--){
int temp = num[i];
num[i]=num[j];
num[j]=temp;
}
return num;
}
private static void outputNumbers(int[]numbers){
for(int i:numbers){
System.out.print(i);
}
System.out.println();
}
public static void main(String[] args) {
int[]numbers={1,2,3,4,5};
for(int i=0;i<10;i++){
numbers = findNearsetNumber(numbers);
outputNumbers(numbers);
}
}
八、刪去k個數字後的最小值
給出一個整數,從該整數中去掉k個數字,要求剩下的數字形成的新整數儘可能小。應該如何選取被去掉的數字?
/**
* 刪除整數的k個數字,獲得刪除後的最小值
* @param num 原整數
* @param k 刪除數量
* @return
*/
public static String removeKDigits(String num,int k){
//新整數的長度
int newLength = num.length()-k;
//創建一個棧,用於接收所有的數字
char [] stack = new char[num.length()];
int top = 0;
for(int i=0;i<num.length();i++){
//遍歷當前數字
char c = num.charAt(i);
//當棧頂數字大於遍歷到的當前數字時,棧頂數字出棧
while (top>0&&stack[top-1]>c&&k>0){
top -=1;
k-=1;
}
//遍歷到的當前數字入棧
stack[top++]=c;
}
//找到棧中第一個非零數字的位置,以此構建新的整數字符串
int offset = 0;
while (offset<newLength&&stack[offset]=='0'){
offset++;
}
return offset == newLength? "0":new String(stack,offset,newLength-offset);
}
九、大整數相加
給出兩個很大的整數,要求實現程序求出兩個整數之和。
/**
* 大整數求和
* @param bigNumberA
* @param BigNumberB
* @return
*/
public static String bigNumberSum(String bigNumberA,String BigNumberB){
//1.把兩個大整數用數組逆序存儲,數組長度等於較大整數位數+1
int maxLength = bigNumberA.length()>BigNumberB.length() ? bigNumberA.length():BigNumberB.length();
int[] arrayA = new int[maxLength+1];
for(int i = 0;i<bigNumberA.length();i++){
arrayA[i] = bigNumberA.charAt((bigNumberA.length()-1-i))-'0';
}
int [] arrayB = new int[maxLength+1];
for(int i = 0;i<BigNumberB.length();i++){
arrayB[i] = BigNumberB.charAt(BigNumberB.length()-i-1)-'0';
}
//2.構建result數組,數組長度等於較大整數位數+1
int []result = new int [maxLength+1];
//3.遍歷數組,按位相加
for(int i =0;i<result.length;i++){
int temp = result[i];
temp+=arrayA[i];
temp+=arrayB[i];
if(temp>=10){
temp=temp-10;
result[i+1]=1;
}
result[i]=temp;
}
//4.把result逆序轉string
StringBuffer sb = new StringBuffer();
//是否找到最大整數的最高有效位
boolean findFirst = false;
for(int i = result.length-1;i>=0;i--){
if(!findFirst){
if(result[i]==0){
continue;
}
findFirst=true;
}
sb.append(result[i]);
}
return sb.toString();
}
十、金礦問題
/**
* 獲得金礦最優收益
* @param w 工人數量
* @param p 金礦開採所需要的工人數量
* @param g 金礦儲量
* @return
*/
public static int getBestGoldMining(int w,int[] p ,int[] g){
//創建當前結果
int[]results = new int[w+1];
//填充一維數組
for(int i=1;i<g.length;i++){
for(int j=w;j>-1;j--){
if(j>=p[i-1]){
results[j] = Math.max(results[j],results[j-p[i-1]]+g[i-1]);
}
}
}
return results[w];
}