目錄
什麼是數據結構?
簡單地說,數據結構是以某種特定的佈局方式存儲數據的容器。這種“佈局方式”決定了數據結構對於某些操作是高效的,而對於其他操作則是低效的。首先我們需要理解各種數據結構,才能在處理實際問題時選取最合適的數據結構。
爲什麼我們需要數據結構?
數據是計算機科學當中最關鍵的實體,而數據結構則可以將數據以某種組織形式存儲,因此,數據結構的價值不言而喻。無論你以何種方式解決何種問題,你都需要處理數據——無論是涉及員工薪水、股票價格、購物清單,還是隻是簡單的電話簿問題。數據需要根據不同的場景,按照特定的格式進行存儲。有很多數據結構能夠滿足以不同格式存儲數據的需求。
1.數組
數組是最簡單、也是使用最廣泛的數據結構。棧、隊列等其他數據結構均由數組演變而來。下圖是一個包含元素(1,2,3和4)的簡單數組,數組長度爲4。
每個數據元素都關聯一個正數值,我們稱之爲索引,它表明數組中每個元素所在的位置。大部分語言將初始索引定義爲零。
數組分爲兩種類型:一維數組,多維數組;
1.1數組基本操作
1)Insert——在指定索引位置插入一個元素
2)Get——返回指定索引位置的元素
3)Delete——刪除指定索引位置的元素
4)Size——得到數組所有元素的數量
1.2數組常見面試算法
1.2.1尋找數組中第二小的元素
public void secondMax(int[] arrays){
if(arrays.length == 0) return;
int max = arrays[0];
int secondMax = arrays[0];
for(int i=1; i<arrays.length; i++){
if(max<arrays[i]){
secondMax = max;
max = arrays[i];
}
}
System.out.println(secondMax);
}
1.2.2找到數組中第一個不重複出現的整數
public void firstNoRepeat(int[] arrays){
//雙循環
//遍歷所有元素
for(int i=0; i<arrays.length; i++){
int count = 1;
//與比較arrays[i]元素比較是否相等
for(int l=0; l<arrays.length; l++){
if(arrays[i] == arrays[l] && i!=l){
count++;
break;//重複元素退出當前循環
}
}
if(count == 1){//第一次出現不重複的元素
System.out.println(arrays[i]);
break;
}
}
//藉助HashMap存儲元素,值做爲key
HashMap<Integer, Integer> map = new HashMap<>();
for(int i=0; i<arrays.length; i++){
if(map.get(arrays[i]) == null){
map.put(arrays[i], 1);
}else {
int value = map.get(arrays[i]);
map.put(arrays[i], value+1);
}
}
//判斷第一次出現一次的整數
for(int i=0; i<arrays.length; i++){
if(map.get(arrays[i]) == 1){
map.get(arrays[i]);
break;
}
}
}
1.2.3合併兩個有序數組
int[] sortone = new int[]{1,30,30,100,101,345,346};
int[] sorttwo = new int[]{1,2,30,300,301,302,400,405,406};
mergeSort(sortone, sorttwo);
public void mergeSort(int[] sortone, int[] sorttwo){
int one = 0;
int two = 0;
int[] merge = new int[sortone.length+sorttwo.length];
int index = 0;
while (one<sortone.length && two<sorttwo.length){
if(sortone[one]<=sorttwo[two]){
merge[index++] = sortone[one++];
}else {
merge[index++] = sorttwo[two++];
}
}
while (one<sortone.length){
merge[index++] = sortone[one++];
}
while (two<sorttwo.length){
merge[index++] = sorttwo[two++];
}
System.out.println(merge);
}
1.2.4重新排列數組中的正值和負值
將數組裏的負數排在數組的前面,正數排在數組的後面。但不改變原先負數和正數的排列順序;
int v[] = {-5, 2, -3, 4, -8, -9, 1, 3, -10};
//int v[] = {-5, 2, -3, 4, -8, -9, 1, 3, 12, 15, 19, -7,-2,};
//int v[] = {-1, -3, -5, -7, 1, 3, 5, 7, 9};
public void process(int[] negative){
int[] negativecopy = new int[negative.length];
int begin = 0;
int end = negative.length-1;
for(int l=begin, r=end; l<negative.length&&r>=0;l++, r--){
if(negative[l]<0){
negativecopy[begin++] = negative[l];
}
if(negative[r]>=0){
negativecopy[end--] = negative[r];
}
}
System.out.println(negativecopy);
}
2.棧(後進先出)
著名的撤銷操作幾乎遍佈任意一個應用。但你有沒有思考過它是如何工作的呢?這個問題的解決思路是按照將最後的狀態排列在先的順序,在內存中存儲歷史工作狀態(當然,它會受限於一定的數量)。這沒辦法用數組實現。但有了棧,這就變得非常方便了。
可以把棧想象成一列垂直堆放的書。爲了拿到中間的書,你需要移除放置在這上面的所有書。這就是LIFO(後進先出)的工作原理。
下圖是包含三個數據元素(1,2和3)的棧,其中頂部的3將被最先移除:
2.1棧的基本操作
1)Push——在頂部插入一個元素
2)Pop——返回並移除棧頂元素
3)isEmpty——如果棧爲空,則返回true
4)Top——返回頂部元素,但並不移除它
2.2數組常見面試算法
2.2.1用兩個棧來實現一個隊列
用兩個棧來實現一個隊列,完成隊列的Push和Pop操作。 隊列中的元素爲int類型。
import java.util.Stack;
public class Solution {
Stack<Integer> stack1 = new Stack<Integer>();
Stack<Integer> stack2 = new Stack<Integer>();
public void push(int node) { //stack1入棧操作
stack1.push(node);
}
public int pop() {//stack1模擬隊列先入先出
while(!stack1.isEmpty()){
int value = stack1.pop();
stack2.push(value);
}
if(!stack2.isEmpty()){
int result = stack2.pop();
while(!stack2.isEmpty()){
int value = stack2.pop();
stack1.push(value);
}
return result;
}
return 0;
}
}
2.2.2對棧的元素進行排序
將棧中的元素排序;
public void stackSort(Stack<Integer> stack){
//臨時堆棧
Stack<Integer> stacktwo = new Stack<>();
//排序處理
while (!stack.isEmpty()){
int topone = stack.pop(); //獲取stack頭部元素
int toptwo = -1;
//判斷臨時堆棧元素元素是否小於stack頭部元素
while (!stacktwo.isEmpty()&&(toptwo=stacktwo.peek())<topone){
//將小於stack頭部元素重新壓入stack
stack.push(stacktwo.pop());
}
//頭部元素壓入臨時棧
stacktwo.push(topone);
}
}
2.2.3使用棧計算後綴表達式
package com.github.baby.owspace.view.activity.fragments;
import java.util.Scanner;
import java.util.Stack;
import java.util.StringTokenizer;
/**
* 使用棧計算後綴表達式
*/
public class PostfixEvaluator {
//定義操作符
private final static char ADD = '+';
private final static char SUBTRACT = '-';
private final static char MULTIPLY = '*';
private final static char DIVIDE = '/';
//存放數據堆棧
private Stack<Integer> stack;
public PostfixEvaluator(){
stack = new Stack<>();
}
//判斷是否是操作符
public boolean isOperator(String str){
return str.equals("+") || str.equals("-") ||
str.equals("*") ||str.equals("/");
}
//根據操作符進行計算
private int caculateSingleOp(char operator, int num1, int num2){
int result = 0;
switch (operator){
case ADD:
result = num1+num2;
break;
case SUBTRACT:
result = num1-num2;
break;
case MULTIPLY:
result = num1*num2;
break;
case DIVIDE:
result = num1/num2;
break;
}
return result;
}
//解析後綴表達式
public int evaluate(String str){
int num1, num2, result=0; //result是臨時結果
String token = ""; //存放解析到的字符
StringTokenizer tokenizer = new StringTokenizer(str);
while (tokenizer.hasMoreTokens()){
token = tokenizer.nextToken(); //字符
if(isOperator(token)){ //判斷是否是計算字符
num2 = (stack.pop()).intValue();//第一次出棧放在右側計算
num1 = (stack.pop()).intValue();//第二次出棧放在右側計算
//計算結果
result = caculateSingleOp(token.charAt(0), num1, num2);
stack.push(result);//重新入棧
}else {
stack.push(new Integer(Integer.parseInt(token)));//非計算字符重新入棧
}
}
return result;
}
}
PostfixEvaluator evaluator = new PostfixEvaluator();
evaluator.evaluate("35 9 *");
測試數據:7 4 -3 * 1 5 + / *
2.2.4判斷表達式是否括號平衡
/*
* 寫一段代碼,判斷包含括號 { [ ( ) ] } 的表達式是否合法
* 考慮對應關係的符號總是挨着這樣的特殊性,通過棧數據結構判斷入棧的元素和棧頂元素關係完成有效性的判斷
*/
public boolean valid(String expression){
//定義堆棧結構
Stack<Character> stack = new Stack<>();
Map<String, String> map = new HashMap<>();
map.put("{","}");
map.put("[","]");
map.put("(",")");
char[] exp = expression.toCharArray();
for(int i=0; i<exp.length; i++){
//開始字符壓棧
if(exp[i] == '{' || exp[i] == '[' || exp[i] == '('){
stack.push(exp[i]);
}
//如果結束字符則出棧,則判斷當前棧頂元素和該結束符號是否是對應關係
if(exp[i] == '{' || exp[i] == '[' || exp[i] == '('){
String c = String.valueOf(stack.pop());
if(exp[i] != map.get(c).toCharArray()[0]){
return false;
}
}
}
return true;
}
String s = "{}(([{}])){}{}";
System.out.println(valid(s));
3.隊列(先進先出)
3.1隊列是什麼 ?
與Stack的區別在於, Stack的刪除與添加都在隊尾進行, 而Queue刪除在隊頭, 添加在隊尾;
一個完美的隊列現實例子:售票亭排隊隊伍。如果有新人加入,他需要到隊尾去排隊,而非隊首——排在前面的人會先拿到票,然後離開隊伍。
3.2隊列的用途是什麼?
一般情況下,如果是一些及時消息的處理,並且處理時間很短的情況下是不需要使用隊列的,直接阻塞式的方法調用就可以了。但是,如果在消息處理的時候特別費時間,這個時候如果有新的消息來了,就只能處於阻塞狀態,造成用戶等待。這個時候在項目中引入隊列是十分有必要的。當我們接受到消息後,先把消息放到隊列中,然後再用新的線程進行處理,這個時候就不會有消息的阻塞了;
3.3隊列接口定義
Queue接口與List、Set同一級別,都是繼承了Collection接口,LinkedList實現了Deque接口;
package java.util;
public interface Queue<E> extends Collection<E> {
boolean add(E var1);
boolean offer(E var1);
E remove();
E poll();
E element();
E peek();
}
a.add 增加一個元索,如果隊列已滿,則拋出一個IIIegaISlabEepeplian異常
b.offer 添加一個元素並返回true,如果隊列已滿,則返回false
c.remove 移除並返回隊列頭部的元素,如果隊列爲空,則拋出一個NoSuchElementException異常
d.poll 移除並返問隊列頭部的元素 如果隊列爲空,則返回null
e.element 返回隊列頭部的元素,如果隊列爲空,則拋出一個NoSuchElementException異常
f.peek 返回隊列頭部的元素,如果隊列爲空,則返回null
public interface BlockingQueue<E> extends Queue<E> {}
g.put 添加一個元素,如果隊列滿,則阻塞
h.take 移除並返回隊列頭部的元素,如果隊列爲空,則阻塞
3.4沒有實現阻塞隊列的接口
a.LinkedList: 實現了java.util.Deque
內置的不阻塞隊列: PriorityQueue 和 ConcurrentLinkedQueue實現java.util.AbstractQueue接口;
PriorityQueue 和 ConcurrentLinkedQueue 類在 Collection Framework 中加入兩個具體集合實現。
b.PriorityQueue 類實質上維護了一個有序列表。加入到 Queue 中的元素根據它們的天然排序(通過其 java.util.Comparable 實現)或者根據傳遞給構造函數的 java.util.Comparator 實現來定位。
c.ConcurrentLinkedQueue 是基於鏈接節點的、線程安全的隊列。併發訪問不需要同步。因爲它在隊列的尾部添加元素並從頭部刪除它們,所以只要不需要知道隊列的大小,ConcurrentLinkedQueue 對公共集合的共享訪問就可以工作得很好。收集關於隊列大小的信息會很慢,需要遍歷隊列;
public static void main(String[] args) {
//BlockingQueueTest.testBasket();
//add()和remove方法在失敗的時候會拋出異常(不推薦)
Queue<String> queue = new LinkedList<String>();
queue.add("a");
queue.add("b");
queue.add("c");
queue.add("d");
//輸出結果 a b c d
for(String q: queue){
System.out.println(q);
}
//返回對頭元素,並在隊列中刪除 a
System.out.println("poll="+queue.poll());
//輸出結果 b c d
for(String q: queue){
System.out.println(q);
}
//返回對頭元素 b
System.out.println("element="+queue.element());
//輸出結果 b c d
for(String q: queue){
System.out.println(q);
}
//返回對頭元素 b
System.out.println("peek="+queue.peek());
//輸出結果 b c d
for(String q: queue){
System.out.println(q);
}
}
3.5實現阻塞隊列的接口
java.util.concurrent 中加入了 BlockingQueue 接口和五個阻塞隊列類。它實質上就是一種帶有一點扭曲的 FIFO 數據結構。不是立即從隊列中添加或者刪除元素,線程執行操作阻塞,直到有空間或者元素可用。
五個隊列所提供的各有不同:
a.ArrayBlockingQueue :一個由數組支持的有界隊列。
b.LinkedBlockingQueue :一個由鏈接節點支持的可選有界隊列。
c.PriorityBlockingQueue :一個由優先級堆支持的無界優先級隊列。
d.DelayQueue :一個由優先級堆支持的、基於時間的調度隊列。
e.SynchronousQueue :一個利用 BlockingQueue 接口的簡單聚集(rendezvous)機制。
如何處理隊列阻塞,及防治數據不一致:
3.5.1添加元素
ArrayBlockingQueue
/** 等候提取元素條件 */
private final Condition notEmpty;
/** 等候放入元素的條件 */
private final Condition notFull;
* 在尾部插入指定元素, 隊列滿了則調用線程等待await()
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
1)加鎖,ReentrantLock
加鎖保證了不會同時有多個生產者來生產元素,達到阻塞,否則數據錯亂;
2)判斷當前隊列裏的元素數count,和隊列容量capacity比較
判斷是否可以往裏面生產元素,notFull.await();提示生產者線程隊列已滿,阻塞所有生產操作線程;
3)數據入隊
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal();
}
4)計數++
5)調用notEmpty喚醒消費者線程有數據,消費者線程進行處理;
6)釋放lock,其它線程可以繼續調用offer了
3.5.2提取元素
ArrayBlockingQueue
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
1.加鎖ReentrantLock
加鎖保證了不會同時有多個消費者來消費元素,達到阻塞,否則數據錯亂;
2)判斷當前隊列裏的元素數count==0,隊列空
判斷是否可以繼續消費元素,notEmpty.await();提示消費者隊列已空,阻塞所有消費操作線程;
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
4)計數--
5)調用notFull.signal();喚醒生產者線程可以繼續生產;
6)釋放lock,其它線程可以繼續調用take()了
阻塞隊列實現步驟:
1)生產消費同步加鎖,進行阻塞
2)生產和消費完判斷隊列元素個數,喚醒對應的生產消費線程;
/**
* 線程隊列管理
*/
public class ThreadPoolManager {
private static final String TAG = ThreadPoolManager.class.getSimpleName();
private static ThreadPoolManager instance = new ThreadPoolManager();
public static ThreadPoolManager getInstance(){
return instance;
}
/**
* 線程池
*/
private ThreadPoolExecutor threadPoolExecutor;
/**
* 請求隊列
*/
private LinkedBlockingDeque<Future<?>> service = new LinkedBlockingDeque<>();
//線程池拒絕執行以後,放入阻塞隊列
private RejectedExecutionHandler handler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
service.put(new FutureTask<Object>(r, null));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
/**
* 初始化
*/
private ThreadPoolManager(){
threadPoolExecutor = new ThreadPoolExecutor(4, 10,
10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(4), handler);
threadPoolExecutor.execute(runnable);
}
/**
* 消費者,不斷檢測線程隊列是否有線程需要執行
*/
private Runnable runnable = new Runnable() {
@Override
public void run() {
while (true){
FutureTask futureTask = null;
try {
Log.e(TAG, "隊列中等待數量:"+service.size());
futureTask = (FutureTask)service.take();
Log.e(TAG, "線程池中線程的數量:"+threadPoolExecutor.getPoolSize());
} catch (InterruptedException e) {
e.printStackTrace();
}
if(futureTask != null){
//消費需要執行的線程
threadPoolExecutor.execute(futureTask);
}
}
}
};
/**
* 執行線程任務,延時多少秒執行,相當於生產者,把需要執行的線程放入消息隊列
* @param futureTask
* @param delayed
* @param <T>
*/
public <T> void execute(final FutureTask<T> futureTask, Object delayed){
if(futureTask != null){
//延時執行
if(delayed != null){
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
service.put(futureTask);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, (long)delayed);
}else {
//不需要延遲處理
try {
service.put(futureTask);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
參考:
https://www.runoob.com/java/data-queue.html
https://baijiahao.baidu.com/s?id=1609200503642486098&wfr=spider&for=pc
https://www.cnblogs.com/xdecode/p/9321848.html