Android學習總結之設計場景題

設計圖片請求框架的緩存模塊

核心目標是通過分層緩存策略(內存緩存 + 磁盤緩存)提升圖片加載效率,同時兼顧內存占用和存儲性能。以下是針對 Android 面試官的回答思路,結合代碼注釋說明關鍵設計點:

一、緩存架構設計:分層緩存策略

采用內存緩存(LRU)+ 磁盤緩存(持久化)+ 網絡兜底的三級架構,優先從內存快速獲取,其次從磁盤讀取,最后網絡加載,減少重復請求和資源消耗。

二、內存緩存設計(LruCache)

核心作用:利用內存快速訪問特性,緩存近期使用的圖片,避免重復解碼 Bitmap。
實現要點

  1. 使用 Android 內置的LruCache(或 Kotlin 的LinkedHashMap手動實現 LRU),根據內存大小動態設置緩存上限(通常為應用可用內存的 1/8)。
  2. 以圖片 URL 的 MD5 值作為 Key,確保唯一性;Value 存儲解碼后的Bitmap
  3. 結合onTrimMemory()回調,在系統內存緊張時主動釋放內存緩存。
代碼示例(帶注釋)
public class MemoryCache {private LruCache<String, Bitmap> lruCache;public MemoryCache(Context context) {// 計算內存緩存上限:取應用可用內存的1/8(避免OOM)int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);int cacheSize = maxMemory / 8;lruCache = new LruCache<String, Bitmap>(cacheSize) {// 重寫尺寸計算(Bitmap的內存占用以像素數衡量:width * height * bytePerPixel)@Overrideprotected int sizeOf(String key, Bitmap value) {return value.getByteCount() / 1024; // 單位KB}};}// 存入內存緩存(主線程調用需注意同步,但LruCache本身線程安全)public void put(String url, Bitmap bitmap) {if (get(url) == null) { // 避免重復存儲lruCache.put(hashKeyForUrl(url), bitmap);}}// 獲取內存緩存public Bitmap get(String url) {return lruCache.get(hashKeyForUrl(url));}// 清理緩存(在Activity/Fragment銷毀時調用,避免內存泄漏)public void clear() {if (!lruCache.isEmpty()) {lruCache.evictAll(); // 清空所有緩存}}// URL轉MD5,確保Key唯一且合法(避免特殊字符導致的問題)private String hashKeyForUrl(String url) {try {MessageDigest md = MessageDigest.getInstance("MD5");byte[] hashBytes = md.digest(url.getBytes());// 轉換為16進制字符串StringBuilder hexString = new StringBuilder();for (byte b : hashBytes) {String hex = String.format("%02X", b);hexString.append(hex.toLowerCase());}return hexString.toString();} catch (Exception e) {return String.valueOf(url.hashCode()); // 異常時用hashCode兜底}}
}

三、磁盤緩存設計(DiskLruCache)

核心作用:持久化存儲圖片文件,避免重復下載,同時減輕內存壓力。
實現要點

  1. 使用 Android 推薦的DiskLruCache(需處理 Android 10 + 的分區存儲適配),按文件大小或時間實現 LRU 淘汰。
  2. 緩存路徑建議放在應用私有目錄(如Context.getCacheDir()),避免用戶刪除或權限問題。
  3. 異步處理磁盤 IO(如使用ExecutorService),避免阻塞主線程。
  4. 支持緩存有效期(如 7 天),定期清理過期文件。
代碼示例(帶注釋)
public class DiskCache {private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MBprivate static final int APP_VERSION = 1; // 版本號變更時清空緩存private static final String DISK_CACHE_SUBDIR = "image_cache"; // 子目錄名稱private DiskLruCache diskLruCache;private ExecutorService diskExecutor;public DiskCache(Context context) {diskExecutor = Executors.newSingleThreadExecutor(); // 單線程保證磁盤操作有序File cacheDir = new File(context.getCacheDir(), DISK_CACHE_SUBDIR);try {diskLruCache = DiskLruCache.open(cacheDir, APP_VERSION, 1, MAX_DISK_CACHE_SIZE);} catch (IOException e) {e.printStackTrace();}}// 異步寫入磁盤緩存(在子線程調用)public void asyncPut(String url, byte[] data) {diskExecutor.execute(() -> {String key = hashKeyForUrl(url);try (DiskLruCache.Editor editor = diskLruCache.edit(key)) {if (editor != null) {OutputStream outputStream = editor.newOutputStream(0);outputStream.write(data);editor.commit(); // 提交寫入}} catch (IOException e) {e.printStackTrace();try {if (editor != null) {editor.abort(); // 失敗時回滾}} catch (IOException ex) {ex.printStackTrace();}}});}// 同步讀取磁盤緩存(建議在子線程調用,避免ANR)public byte[] get(String url) {String key = hashKeyForUrl(url);try (DiskLruCache.Snapshot snapshot = diskLruCache.get(key)) {if (snapshot != null) {InputStream inputStream = snapshot.getInputStream(0);return inputStreamToByteArray(inputStream); // 轉換為字節數組}} catch (IOException e) {e.printStackTrace();}return null;}// 清理過期緩存(可結合定時任務或開機廣播觸發)public void cleanExpiredCache(long expirationMillis) {diskExecutor.execute(() -> {File cacheDir = diskLruCache.getDirectory();for (File file : cacheDir.listFiles()) {if (System.currentTimeMillis() - file.lastModified() > expirationMillis) {file.delete(); // 刪除超過有效期的文件}}try {diskLruCache.trimToSize(MAX_DISK_CACHE_SIZE); // 按大小LRU淘汰} catch (IOException e) {e.printStackTrace();}});}// 輸入流轉字節數組(工具方法)private byte[] inputStreamToByteArray(InputStream is) throws IOException {ByteArrayOutputStream os = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int len;while ((len = is.read(buffer)) != -1) {os.write(buffer, 0, len);}return os.toByteArray();}
}

四、緩存協同邏輯

  1. 獲取圖片流程

    • 先查內存緩存,存在則直接使用(無需解碼,最快)。
    • 內存無緩存則查磁盤緩存,存在則解碼為 Bitmap 并存入內存(下次直接讀內存)。
    • 磁盤無緩存則發起網絡請求,下載后同時寫入磁盤和內存。
  2. 內存與磁盤的一致性

    • 磁盤緩存寫入完成后,再更新內存緩存,避免內存與磁盤數據不一致。
    • 圖片尺寸適配:根據 ImageView 的目標尺寸(width/height)緩存對應尺寸的圖片,避免內存浪費(如存儲 1080p 圖片到僅需 200x200 的 View)。

五、面試官高頻問題補充

  1. 為什么選擇 LruCache 而不是 HashMap?
    LruCache 內置 LRU 淘汰算法,自動管理內存釋放,避免 OOM;HashMap 需手動實現淘汰邏輯,容易導致內存泄漏。

  2. 磁盤緩存為什么用 DiskLruCache 而不是直接寫文件?
    DiskLruCache 封裝了文件 IO 的原子性操作(如寫入失敗時回滾)、LRU 淘汰策略、版本管理(版本號變更時清空緩存),比手動管理文件更可靠。

  3. 如何處理緩存穿透和緩存擊穿?

    • 緩存穿透(請求不存在的 Key):對無效 Key 進行短期內存緩存(如緩存空結果 1 分鐘)。
    • 緩存擊穿(熱點 Key 失效):加分布式鎖(或本地鎖),確保同一 Key 的網絡請求僅發起一次。
  4. Android 10 + 分區存儲對磁盤緩存的影響?
    緩存路徑必須使用應用私有目錄(如getCacheDir()),避免使用外部存儲公共目錄(需申請權限且可能被用戶清理)。

ScrollView里面嵌套兩個高度都為兩個屏幕RecycleView

整體思路闡述

當?ScrollView?嵌套兩個高度為兩個屏幕的?RecyclerView?時,要實現特定的?ACTION_MOVE?事件處理邏輯,也就是讓?MOVE?事件先由?RecyclerView1?處理,等?RecyclerView1?滾動到底部后將事件交給?ScrollView?處理,待?RecyclerView2?完全展示在屏幕上時再把事件交給?RecyclerView2?處理,關鍵在于重寫?ScrollView?的?onInterceptTouchEvent?方法來精確控制事件的攔截與分發。同時,需要編寫方法來判斷?RecyclerView?是否滾動到底部以及是否完全顯示在屏幕上。

代碼實現:

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ScrollView;
import androidx.recyclerview.widget.RecyclerView;// 自定義 ScrollView 類,用于處理嵌套 RecyclerView 的觸摸事件
public class CustomScrollView extends ScrollView {// 聲明兩個 RecyclerView 成員變量private RecyclerView recyclerView1;private RecyclerView recyclerView2;// 構造函數,用于在代碼中創建 CustomScrollView 實例public CustomScrollView(Context context) {super(context);}// 構造函數,用于在 XML 布局中使用 CustomScrollViewpublic CustomScrollView(Context context, AttributeSet attrs) {super(context, attrs);}// 構造函數,帶有默認樣式屬性public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}// 設置關聯的兩個 RecyclerViewpublic void setRecyclerViews(RecyclerView recyclerView1, RecyclerView recyclerView2) {this.recyclerView1 = recyclerView1;this.recyclerView2 = recyclerView2;}// 重寫 onInterceptTouchEvent 方法,用于攔截觸摸事件@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {// 確保兩個 RecyclerView 已經被正確設置if (recyclerView1 != null && recyclerView2 != null) {// 根據觸摸事件的動作類型進行處理switch (ev.getAction()) {case MotionEvent.ACTION_MOVE:// 判斷 RecyclerView1 是否滾動到底部if (!isRecyclerViewAtBottom(recyclerView1)) {// 如果 RecyclerView1 未滾動到底部,不攔截事件,讓 RecyclerView1 處理return false;}// 判斷 RecyclerView2 是否完全顯示在屏幕上if (!isRecyclerViewFullyVisible(recyclerView2)) {// 如果 RecyclerView2 未完全顯示,攔截事件,由 ScrollView 處理return true;}break;}}// 其他情況,調用父類的 onInterceptTouchEvent 方法return super.onInterceptTouchEvent(ev);}// 判斷 RecyclerView 是否滾動到底部的方法private boolean isRecyclerViewAtBottom(RecyclerView recyclerView) {// 檢查 RecyclerView 的適配器是否為空if (recyclerView.getAdapter() == null) {return false;}// 獲取 RecyclerView 的 LayoutManagerRecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();// 檢查 LayoutManager 是否為空if (layoutManager == null) {return false;}// 獲取最后一個可見項的位置int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();// 判斷最后一個可見項是否是列表中的最后一項return lastVisibleItemPosition == recyclerView.getAdapter().getItemCount() - 1;}// 判斷 RecyclerView 是否完全顯示在屏幕上的方法private boolean isRecyclerViewFullyVisible(RecyclerView recyclerView) {// 檢查 RecyclerView 的適配器是否為空if (recyclerView.getAdapter() == null) {return false;}// 獲取 RecyclerView 的 LayoutManagerRecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();// 檢查 LayoutManager 是否為空if (layoutManager == null) {return false;}// 獲取第一個可見項的位置int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();// 獲取最后一個可見項的位置int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();// 判斷第一個可見項是否是列表中的第一項,且最后一個可見項是否是列表中的最后一項return firstVisibleItemPosition == 0 && lastVisibleItemPosition == recyclerView.getAdapter().getItemCount() - 1;}
}    
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;// 主 Activity 類
public class MainActivity extends AppCompatActivity {// 聲明 CustomScrollView 和兩個 RecyclerView 成員變量private CustomScrollView customScrollView;private RecyclerView recyclerView1;private RecyclerView recyclerView2;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 設置布局文件setContentView(R.layout.activity_main);// 從布局文件中獲取 CustomScrollView 和兩個 RecyclerView 的實例customScrollView = findViewById(R.id.customScrollView);recyclerView1 = findViewById(R.id.recyclerView1);recyclerView2 = findViewById(R.id.recyclerView2);// 為 RecyclerView1 設置線性布局管理器recyclerView1.setLayoutManager(new LinearLayoutManager(this));// 為 RecyclerView2 設置線性布局管理器recyclerView2.setLayoutManager(new LinearLayoutManager(this));// 為 RecyclerView1 設置適配器,并傳入模擬數據recyclerView1.setAdapter(new MyAdapter(createDummyData()));// 為 RecyclerView2 設置適配器,并傳入模擬數據recyclerView2.setAdapter(new MyAdapter(createDummyData()));// 將兩個 RecyclerView 關聯到 CustomScrollView 中customScrollView.setRecyclerViews(recyclerView1, recyclerView2);}// 創建模擬數據的方法private List<String> createDummyData() {// 創建一個字符串列表來存儲模擬數據List<String> data = new ArrayList<>();// 循環添加 50 條模擬數據for (int i = 0; i < 50; i++) {data.add("Item " + i);}return data;}
}    

?

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;// RecyclerView 的適配器類
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {// 存儲要顯示的數據列表private List<String> data;// 構造函數,傳入數據列表public MyAdapter(List<String> data) {this.data = data;}// 創建 ViewHolder 實例@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {// 從布局文件中加載單個列表項的視圖View view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);// 創建 ViewHolder 實例并傳入視圖return new ViewHolder(view);}// 綁定數據到 ViewHolder@Overridepublic void onBindViewHolder(@NonNull ViewHolder holder, int position) {// 將指定位置的數據設置到 TextView 中holder.textView.setText(data.get(position));}// 獲取數據列表的大小@Overridepublic int getItemCount() {return data.size();}// ViewHolder 類,用于緩存視圖組件public static class ViewHolder extends RecyclerView.ViewHolder {// 聲明 TextView 成員變量TextView textView;// 構造函數,傳入視圖public ViewHolder(@NonNull View itemView) {super(itemView);// 從視圖中獲取 TextView 實例textView = itemView.findViewById(android.R.id.text1);}}
}    

代碼調用邏輯說明

  1. 初始化階段:在?MainActivity?的?onCreate?方法中,首先通過?setContentView?設置布局文件,然后從布局文件中獲取?CustomScrollView?和兩個?RecyclerView?的實例。接著為兩個?RecyclerView?設置?LinearLayoutManager?和?MyAdapter,并調用?customScrollView.setRecyclerViews?方法將兩個?RecyclerView?關聯到?CustomScrollView?中。
  2. 觸摸事件處理階段:當用戶進行觸摸操作時,觸摸事件會先傳遞到?CustomScrollView?的?onInterceptTouchEvent?方法。在該方法中,會根據?RecyclerView1?是否滾動到底部以及?RecyclerView2?是否完全顯示在屏幕上的情況來決定是否攔截事件。如果?RecyclerView1?未滾動到底部,不攔截事件,讓?RecyclerView1?處理;如果?RecyclerView2?未完全顯示,攔截事件,由?CustomScrollView?處理;其他情況則調用父類的?onInterceptTouchEvent?方法。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/81100.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/81100.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/81100.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Webug3.0通關筆記14 第十四關:存儲型XSS

目錄 第十四關:存儲型XSS 1.打開靶場 2.源碼分析 3.滲透實戰 第十四關:存儲型XSS 本文通過《webug3靶場第十四關 存儲型XSS》來進行存儲型XSS關卡的滲透實戰。 存儲型 XSS&#xff08;Stored Cross - Site Scripting&#xff09;&#xff0c;也被稱為持久型 XSS&#xff…

Java父類、子類實例初始化順序詳解

1、完整的初始化順序&#xff08;含繼承&#xff09; 1、父類的靜態初始化 父類靜態變量默認值 → 父類靜態變量顯式賦值 父類靜態代碼塊&#xff08;按代碼順序執行&#xff09;。 2、子類的靜態初始化 子類靜態變量默認值 → 子類靜態變量顯式賦值 子類靜態代碼塊&…

13.組合模式:思考與解讀

原文地址:組合模式&#xff1a;思考與解讀 更多內容請關注&#xff1a;7.深入思考與解讀設計模式 引言 在軟件開發中&#xff0c;是否曾經遇到過這樣一種情況&#xff1a;你有一個對象&#xff0c;它本身很簡單&#xff0c;但是它包含了其他類似的對象。隨著系統變得越來越復…

OpenCV實戰教程 第一部分:基礎入門

第一部分&#xff1a;基礎入門 1. OpenCV簡介 什么是OpenCV及其應用領域 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是一個開源的計算機視覺和機器學習軟件庫&#xff0c;于1999年由Intel公司發起&#xff0c;現在由非營利組織OpenCV.org維護。Ope…

虛幻商城 Quixel 免費資產自動化入庫(2025年版)

文章目錄 一、背景二、問題講解1. Quixel 免費資產是否還能一鍵入庫?2. 是不是使用了一鍵入庫功能 Quixel 的所有資產就能入庫了?3. 一鍵入庫會入庫哪些資產?三、實現效果展示四、實現自動化入庫五、常見問題1. 出現401報錯2. 出現429報錯3. 入庫過于緩慢4. 入庫 0 個資產一…

uni-app - 小程序使用高德地圖完整版

文章目錄 ??功能描述??效果??開發環境??代碼部分??功能描述 頁面自動通過定位獲取用戶位置并展示周邊POI數據,同時支持關鍵詞輸入實時聯想推薦關聯地點信息, 實現精準智能的地點發現與檢索功能。 ??效果 ??開發環境 unibest2.5.4nodev18.20.5pnpm9.14.2wot-des…

牛客:AB4 逆波蘭表達式求值

鏈接&#xff1a;逆波蘭表達式求值_牛客題霸_牛客網 題解&#xff1a; 利用棧&#xff0c;遍歷字符串數組&#xff0c;遇到運算數則入棧&#xff0c;遇到運算符則取出棧頂兩個運算數進行運算&#xff0c;并將運算結果入棧。 class Solution { public:/*** 代碼中的類名、方法…

Ant(Ubuntu 18.04.6 LTS)安裝筆記

一、前言 本文與【MySQL 8&#xff08;Ubuntu 18.04.6 LTS&#xff09;安裝筆記】同批次&#xff1a;先搭建數據庫&#xff0c;再安裝JDK&#xff0c;后面肯定就是部署Web應用。其中Web應用的部署使用 Ant 方式&#xff0c;善始善終&#xff0c;特以筆記。 二、準備 &#xf…

ultralytics 目標檢測 混淆矩陣 背景圖像 沒被記錄

修改 utils/metrics.py ConfusionMatrix def process_batch(self, detections, gt_bboxes, gt_cls):"""Update confusion matrix for object detection task.Args:detections (Array[N, 6] | Array[N, 7]): Detected bounding boxes and their associated inf…

iview 如何設置sider寬度

iview layout組件中&#xff0c;sider設置了默認寬度和最大寬度&#xff0c;在css樣式文件中修改無效&#xff0c;原因是iview默認樣式設置在了element.style中&#xff0c;只能通過行內樣式修改 樣式如下&#xff1a; image.png image.png 修改方式&#xff1a; 1.官方文檔中寫…

go-zero(十七)結合DTM :實現分布式事務

1. 基礎概念介紹 1.1 什么是分布式事務 在微服務架構中&#xff0c;一個業務操作常常需要調用多個服務來完成。例如&#xff0c;在電商系統中下單時&#xff0c;需要同時操作訂單服務和庫存服務。這種跨服務的操作就需要分布式事務來保證數據一致性。 分布式事務面臨以下挑戰…

2025 簡易Scrum指南(簡體中文版)

Scrum是一個輕量級的、以團隊為中心的框架&#xff0c;用于解決復雜的問題并創造價值。Scrum有意保持非完整性&#xff0c;Scrum的設計初衷旨在依靠使用者的集體智慧來不斷演進構建。 Scrum建立在實驗主義和精益思想的基礎上&#xff0c;它賦能團隊靈活巧妙地工作&#xff0c;…

2025最新福昕PDF編輯器,PDF萬能處理工具

軟件介紹 Foxit PDF Editor Pro 2025 中文特別版&#xff08;以前稱為 Foxit PhantomPDF Business&#xff09;是一款專為滿足各種辦公需求而設計的業務就緒的PDF工具包。 軟件特點 1. 強大的PDF編輯能力 創建新文檔&#xff1a;用戶可以從無到有地構建PDF文檔&#xff0c;添…

ollama的若干實踐

1. 本地ollama 1.1 本地安裝ollama 方法 1&#xff1a;手動檢查最新版本并下載 訪問 Ollama 的 GitHub Releases 頁面&#xff1a; 打開 https://github.com/ollama/ollama/releases 查看最新的穩定版本&#xff08;如 v0.7.0 或更高&#xff09; 手動下載最新版本&#xff08…

Spring Security源碼解析

秒懂SpringBoot之全網最易懂的Spring Security教程 SpringBoot整合Spring-Security 認證篇&#xff08;保姆級教程&#xff09; SpringBoot整合Spring Security【超詳細教程】 spring security 超詳細使用教程&#xff08;接入springboot、前后端分離&#xff09; Security 自…

LeetCode 3392.統計符合條件長度為 3 的子數組數目:一次遍歷模擬

【LetMeFly】3392.統計符合條件長度為 3 的子數組數目&#xff1a;一次遍歷模擬 力扣題目鏈接&#xff1a;https://leetcode.cn/problems/count-subarrays-of-length-three-with-a-condition/ 給你一個整數數組 nums &#xff0c;請你返回長度為 3 的 子數組&#xff0c;滿足…

讀論文筆記-CoOp:對CLIP的handcrafted改進

讀論文筆記-Learning to Prompt for Vision-Language Models Problems 現有基于prompt engineering的多模態模型在設計合適的prompt時有很大困難&#xff0c;從而設計了一種更簡單的方法來制作prompt。 Motivations prompt engineering雖然促進了視覺表示的學習&#xff0c…

從零構建 MCP Server 與 Client:打造你的第一個 AI 工具集成應用

目錄 &#x1f680; 從零構建 MCP Server 與 Client&#xff1a;打造你的第一個 AI 工具集成應用 &#x1f9f1; 1. 準備工作 &#x1f6e0;? 2. 構建 MCP Server&#xff08;服務端&#xff09; 2.1 初始化服務器 &#x1f9e9; 3. 添加自定義工具&#xff08;Tools&…

Django 自定義celery-beat調度器,查詢自定義表的Cron表達式進行任務調度

學習目標&#xff1a; 通過自定義的CronScheduler調度器在兼容標準的調度器的情況下&#xff0c;查詢自定義任務表去生成調度任務并分配給celery worker進行執行 不了解Celery框架的小伙伴可以先看一下我的上一篇文章&#xff1a;Celery框架組件分析及使用 學習內容&#xff…

藍橋杯 1. 確定字符串是否包含唯一字符

確定字符串是否包含唯一字符 原題目鏈接 題目描述 實現一個算法來識別一個字符串的字符是否是唯一的&#xff08;忽略字母大小寫&#xff09;。 若唯一&#xff0c;則輸出 YES&#xff0c;否則輸出 NO。 輸入描述 輸入一行字符串&#xff0c;長度不超過 100。 輸出描述 輸…