如果人生也能存檔——C#中的備忘錄模式

大家好,老胡又和大家見面了。首先承認今天的博客有點標題黨了,人生是沒有存檔,也沒有後悔葯的。有存檔和後悔葯的,那是遊戲,不知道這是不是遊戲讓人格外放鬆的原因之一。

今天恰逢端午放假,就讓我們來試着做一個小遊戲吧,順帶看看備忘錄模式是如何在這種情況下面工作的。

遊戲背景

這是一個簡單的打怪遊戲,有玩家,有怪獸,玩家作為主角光環,有如下三個特殊能力

  • 攻擊怪獸有暴擊幾率
  • 有幾率迴避怪獸攻擊
  • 可以自己治療一定生命值

遊戲實現

角色類
角色基類

首先是角色類,角色類提供玩家和怪獸最基本的抽象,比如血量、攻擊力、攻擊和治療。(對於怪獸來說,治療是沒有提供實現的,壞人肯定不能再治療了)

class Character
{
    public int HealthPoint { get; set; }
    public int AttackPoint { get; set; }        

    public virtual void AttackChracter(Character opponent)
    {
        opponent.HealthPoint -= this.AttackPoint;
        if (opponent.HealthPoint < 0)
        {
            opponent.HealthPoint = 0;
        }
    }

    public virtual void Cure()
    {
		//故意留空給子類實現
    }
}
玩家類

玩家實現了治療功能並且有暴擊幾率。

class Player : Character
{
    private float playerCriticalPossible;
    public Player(float critical)
    {
        playerCriticalPossible = critical;
    }

    public override void AttackChracter(Character opponent)
    {
        base.AttackChracter(opponent);
        Console.WriteLine("Player Attacked Monster");

        Random r = new Random();
        bool critical = r.Next(0, 100) < playerCriticalPossible * 100;
        if (critical)
        {
            base.AttackChracter(opponent);
            Console.WriteLine("Player Attacked Monster again");
        }
    }

    public override void Cure()
    {
        Random r = new Random();
        HealthPoint += r.Next(5, 10);
        Console.WriteLine("Player cured himself");
    }
}
怪獸類

怪獸沒有治療能力但是有一定的幾率丟失攻擊目標。

class Monster : Character
{
    private float monsterMissingPossible;
    public Monster(float missing)
    {
        monsterMissingPossible = missing;
    }

    public override void AttackChracter(Character opponent)
    {
        Random r = new Random();
        bool missing = r.Next(0, 100) < monsterMissingPossible * 100;
        if (missing)
        {
            Console.WriteLine("Monster missed it");
        }
        else
        {
            base.AttackChracter(opponent);
            Console.WriteLine("Monster Attacked player");
        }
    }
}
遊戲類

遊戲類負責實例化玩家和怪獸、記錄回合數、判斷遊戲是否結束,暴露可調用的公共方法給遊戲操作類。

class Game
{
    private Character m_player;
    private Character m_monster;
    private int m_round;
    private float playerCriticalPossible = 0.6f;
    private float monsterMissingPossible = 0.2f;
    
    public Game()
    {
        m_player = new Player(playerCriticalPossible)
        {
            HealthPoint = 15,
            AttackPoint = 2
        };
        m_monster = new Monster(monsterMissingPossible)
        {
            HealthPoint = 20,
            AttackPoint = 6
        };
    }

    public bool IsGameOver => m_monster.HealthPoint == 0 || m_player.HealthPoint == 0;

    public void AttackMonster()
    {            
        m_player.AttackChracter(m_monster);
    }

    public void AttackPlayer()
    {
        m_monster.AttackChracter(m_player);
    }

    public void CurePlayer()
    {
        m_player.Cure();
    }

    public void BeginNewRound()
    {
        m_round++;
    }

    public void ShowGameState()
    {
        Console.WriteLine("".PadLeft(20, '-'));
        Console.WriteLine("Round:{0}", m_round);
        Console.WriteLine("player health:{0}", "".PadLeft(m_player.HealthPoint, '*'));
        Console.WriteLine("monster health:{0}", "".PadLeft(m_monster.HealthPoint, '*'));
    }
}
遊戲操作類

在我們這個簡易遊戲中,沒有UI代碼,遊戲操作類負責在用戶輸入和遊戲中搭建一個橋樑,解釋用戶的輸入。

class GameRunner
{
    private Game m_game;
    public GameRunner(Game game)
    {
        m_game = game;
    }

    public void Run()
    {
        while (!m_game.IsGameOver)
        {
            m_game.BeginNewRound();
            bool validSelection = false;
            while (!validSelection)
            {
            	m_game.ShowGameState();
                Console.WriteLine("Make your choice: 1. attack 2. Cure");
                var str = Console.ReadLine();
                if (str.Length != 1)
                {
                    continue;
                }
                switch (str[0])
                {
                    case '1':
                        {
                            validSelection = true;
                            m_game.AttackMonster();
                            break;
                        }
                    case '2':
                        {
                            validSelection = true;
                            m_game.CurePlayer();
                            break;
                        }
                    default:
                        break;
                }
            }
            if(!m_game.IsGameOver)
            {
                m_game.AttackPlayer();
            }
        }            
    }
}
客戶端

客戶端的代碼就非常簡單了,只需要實例化一個遊戲操作類,然後讓其運行就可以了。

class Program
{
    static void Main(string[] args)
    {
        Game game = new Game();
        GameRunner runner = new GameRunner(game);
        runner.Run();
    }
}

試着運行一下,

看起來一切都好。

 

加上存檔

雖然遊戲可以正常運行,但是總感覺還是少了點什麼。嗯,存檔功能,一個遊戲沒有存檔是不健全的,畢竟,人生雖然沒有存檔,但是遊戲可是有的!讓我們加上存檔功能吧,首先想想怎麼設計。
 

需要存檔的數據

首先我們要明確,有哪些數據是需要存檔的,在這個遊戲中,玩家的生命值、攻擊力、暴擊率;怪獸的生命值、攻擊力和丟失率,遊戲的回合數,都是需要存儲的對象。
 

存檔定義

這是一個需要仔細思考的地方,一般來說,需要考慮以下幾個地方:

  • 存檔需要訪問一些遊戲中的私有字段,比如暴擊率,需要在不破壞遊戲封裝的情況下實現這個功能
  • 存檔自身需要實現信息隱藏,即除了遊戲,其他類不應該訪問存檔的詳細信息
  • 存檔不應該和遊戲存放在一起,以防不經意間遊戲破壞了存檔數據,應該有專門的類存放存檔

 

備忘錄模式出場

這個時候應該是主角出場的時候了。看看備忘錄模式的定義

在不破壞封閉的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態。這樣以後就可將該對象恢復到原先保存的狀態

再看看UML,

看起來完全符合我們的需求啊,Originator就是遊戲類,知道如何創造存檔和從存檔中恢復狀態,Memento類就是存檔類,Caretaker是一個新類,負責保存存檔。

經過思考,我們決定採取備忘錄模式,同時加入以下措施:

  • 將存檔定義為遊戲中的私有嵌套類,這樣存檔可以毫無壓力的訪問遊戲中的私有字段,同時外界永遠沒有辦法去實例化或者嘗試通過轉型來獲得這個類,完美的保護了存檔類
  • 存檔類是一個簡單的數據集合,不包含任何其他邏輯
  • 添加一個存檔管理器,可以放在遊戲操作類中,可以通過它看到我們當前有沒有存檔
  • 存檔放在存檔管理器中
  • 存檔實現一個空接口,在存檔管理器中以空接口形式出現,這樣外部類在訪問存檔的時候,僅能看到這個空接口。而在遊戲類內部,我們在使用存檔之前先通過向下轉型實現類型轉換(是的,向下轉型不怎麼好,但是偶爾可以用一下)
代碼實現
空接口
interface IGameSave
{

}
私有嵌套存檔類

該類存放在game裏面,無壓力地在不破壞封裝的情況下訪問game私有字段

private class GameSave : IGameSave
{
    public int PlayerHealth { get; set; }
    public int PlayerAttack { get; set; }
    public float PlayerCritialAttackPossible { get; set; }
    public int MonsterHealth { get; set; }
    public int MonsterAttack { get; set; }
    public float MonsterMissingPossible { get; set; }
    public int GameRound { get; set; }
}
創建存檔和從存檔恢復

game中添加創建存檔和從存檔恢復的代碼,在從存檔恢復的時候,使用了向下轉型,因為從存檔管理器讀出來的只是空接口而已

public IGameSave CreateSave()
{
    var save = new GameSave()
    {
        PlayerHealth = m_player.HealthPoint,
        PlayerAttack = m_player.AttackPoint,
        PlayerCritialAttackPossible = playerCriticalPossible,
        MonsterAttack = m_monster.AttackPoint,
        MonsterHealth = m_monster.HealthPoint,
        MonsterMissingPossible = monsterMissingPossible,
        GameRound = m_round
    };
    Console.WriteLine("game saved");
    return save;
}

public void RestoreFromGameSave(IGameSave gamesave)
{
    GameSave save = gamesave as GameSave;
    if(save != null)
    {
        m_player = new Player(save.PlayerCritialAttackPossible) { HealthPoint = save.PlayerHealth, AttackPoint = save.PlayerAttack };
        m_monster = new Player(save.MonsterMissingPossible) { HealthPoint = save.MonsterHealth, AttackPoint = save.MonsterAttack };
        m_round = save.GameRound;
    }
    Console.WriteLine("game restored");
}	
存檔管理器類

添加一個類專門管理存檔,此類非常簡單,只有一個存檔,要支持多存檔可以考慮使用List

    class GameSaveStore
    {
        public IGameSave GameSave { get; set; }
    }
在遊戲操作類添加玩家選項

首先在遊戲操作類中添加一個存檔管理器

private GameSaveStore m_gameSaveStore = new GameSaveStore();

接着修改Run方法添加用戶操作

public void Run()
{
    while (!m_game.IsGameOver)
    {
        m_game.BeginNewRound();
        bool validSelection = false;
        while (!validSelection)
        {
            m_game.ShowGameState();
            Console.WriteLine("Make your choice: 1. attack 2. Cure 3. Save 4. Load");
            var str = Console.ReadLine();
            if (str.Length != 1)
            {
                continue;
            }
            switch (str[0])
            {
                case '1':
                    {
                        validSelection = true;
                        m_game.AttackMonster();
                        break;
                    }
                case '2':
                    {
                        validSelection = true;
                        m_game.CurePlayer();
                        break;
                    }
                case '3':
                    {
                        validSelection = false;
                        m_gameSaveStore.GameSave = m_game.CreateSave();
                        break;
                    }
                case '4':
                    {
                        validSelection = false;
                        if(m_gameSaveStore.GameSave == null)
                        {
                            Console.WriteLine("no save to load");
                        }
                        else
                        {
                            m_game.RestoreFromGameSave(m_gameSaveStore.GameSave);
                        }
                        break;
                    }
                default:
                    break;
            }
        }
        if(!m_game.IsGameOver)
        {
            m_game.AttackPlayer();
        }
    }            
}

注意,上面的3和4是新添加的存檔相關的操作。試着運行一下。

看起來一切正常,這樣我們就使用備忘錄模式,完成了存檔讀檔的功能。

 

結語

這就是備忘錄模式的使用,如果大家以後遇到這種場景

  • 想要保存狀態,又不想破壞封裝
  • 需要把狀態保存到其他地方

那麼就可以考慮使用這個模式。

遊戲有存檔,人生沒存檔,願我們把握當下,天天努力。
祝大家端午安康,下次見。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

「敢檢討核電海嘯對策就開除你喔!」 解析處處矛盾的福島核災判決

文:宋瑞文(媽媽監督核電廠聯盟特約撰述)

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

綠色認證棕櫚油需要支持 監督機構訂新規 企業採購量不足將受罰

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

林火肆虐、霾害有感 蘇門答臘村落反思傳統 自主禁燒

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

2020-為什麼換了工作

摘要

經歷了一個特殊的2020上半年,疫情出乎意料的持續了半年之久,還是沒有看到結束的趨勢。雖然外部環境很惡劣,還是做出了個人的重大選擇,換工作。期間糾結了很久,畢竟工作就是生活,換工作就是對未來的期待,對過去的總結,對自己的人生的深度思考。這裏回顧下當時的個人思考,供後續復盤參考。

當前的狀況

2020,本科畢業的第六年,不再像剛畢業那會,覺得換公司是輕而易舉的事,考慮的事情越來越多。
畢業五年開始就越發的焦慮,這是當時的心境
2019年春-當前的困境、
2019秋-走的太久忘記了為什麼出發

總結起來

  1. 在擁抱變化的過程中,沒有匹配上自己的個人目標
  2. 工作多年還依然在糾結擅長和喜歡

老馬說的員工想要離職無非兩個原因

  1. 錢沒給到位
  2. 心委屈了
    分別是物質和精神方面。算是高度概括了。細分起來就是三方面錢、人、事。

錢的計算相對比較複雜了。

  1. 時薪和月薪
    月薪20K 的996,和15K的965相比,時薪還要低不少。但是如何選擇,每個人的情況不一樣,選擇不一樣
  2. 月薪和總包
    互聯網公司的總包除了現金部分還包括股票或者期權。一般分4年歸屬,待滿兩年才能拿到一半。股票的價值不好估算,畢竟二級市場波動很大。對比股票,期權價值就更不好估算了。因為沒上市的公司估值的水分很大,另外不一定能兌現,畢竟上市沒那麼容易。但是機會和風險並存,創業公司會給很多期權來吸引(忽悠)人才加入。你想獲得高額的回報甚至是財務自由,就得冒很大的風險。

所以總收入: 現金 + 風險係數 * 股票/期權價值。 同時考慮你的時間比。

那對於當前的我來說,缺錢,但是不是缺工作跳槽的錢。怎麼說呢,就是生活基本物質得到了保障,但是更高的需求無法滿足,而這些多出來的需求靠換工作是滿足不了。所以錢不是重點考慮的。

互聯網的從業人員的個人素質相對比較高,加上工作專業度比較高,所以都比較簡單,沒那麼多亂七八糟的人情世故需要去處理。所以相處起來還比較愉快。

但是人與人之間是有磁場的,不是每個人都能和你對脾氣、遇到合適的領導,相近的同事也沒那麼容易,另外建立關係也需要時間。這個對於跳槽是減分項。

事包含兩部分,一部分是成就感,一部分個人成長機會。也就是通常說的借人成事、借事修人。

馬斯洛需求理論最高層級就是自我實現。對於互聯網人,每天除了睡覺,70%以上時間都給了工作,那自我實現肯定要在工作中實現。

那對於我個人而已,相對穩定的業務,確定性比較高,沒什麼發展前景的就沒什麼吸引力。希望是有挑戰性的工作,能把事情做成功,並且自己能夠在其中發揮重要的作用。

個人成長其實就是未來。個人成長不是一蹴而就的,短期內不易覺察。但是非常重要,互聯網更新很快,你沒跟上就被淘汰了。對於廣泛流傳的程序員成長路線,3年高工,7年架構,10年外賣。

雖然是調侃,但是也說明了幾點內容

  1. 更新迭代很快的互聯網對大齡人不是很友好,你必須要持續成長。恍惚中我就工作6年,在互聯網行業中都算一個老兵了。
  2. 互聯網人的職業發展與其他行業發展曲線不一樣。一個發展很快,新知識很多的領域,個體是永遠追不上行業的發展水平的。

總結來說,發展是當前跳槽的主要考慮因素。

跳槽的期望

有了換工作的想法,是對現狀的不滿,但是換一個環境並不意味着問題的解決。這也是為什麼很多人經常抱怨公司的不好,但是不挪窩的原因。我們不能寄希望於未知的事情。尤其是從18年底開始互聯網整體再走下坡路,就業環境並不好,加上今年疫情的原因,外部環境更加惡化。

但是大環境不好,不代表個體就不好。不確定性很多不代表沒有確定性的東西。人的一生就是在不斷選擇中度過的,我們必須要了解自己,抓住重點,匹配自己與環境。

那麼我的個人期望是怎麼樣的,新的工作能滿足我的期望嗎?

職業發展

之前迷茫過還要不要做程序員,能不能轉行到產品,諮詢,售前。現在不能說篤定了一直做技術,但是找到了一些發展規律。

  1. 不會再去做偏底層的技術
    以前做過大數據,BI,甚至3年前還拿到過大數據工程師的offer,現在也深入了解了運維、中間件相關的工作內容。徹底打消了偏技術側的技術開發。
  2. 解決問題為驅動
    問題為導向,離業務越近越好。通過技術來驅動業務,非技術手段來解決業務問題。

這個算是近兩年個人的一個不小的突破,排除了哪些事不會去做,哪些事要去嘗試探索。

行業

行業涉及面太廣了,現在toC不好做,巨頭林立把用戶時間都擠佔了。toB 更難了,工具效率型的,國內目前付費意願並不強,能幫助企業帶來收入的才能發展的下去。

個人目前對具體做哪個行業的並沒有那麼執着。更多的期望是能夠以某個點切入到某個行業,然後看到如何通過互聯網技術把問題解決了,形成閉環,把事情做成功了。這種成就感足以讓我興奮。因為這些年,在不同的公司做各種創業項目,都沒有做成功的,都因為各種原因死掉了。正因如此,才知道創業是如何的艱難與不易。

通過自我的剖析和明確當前職業發展目標,在2020年春夏之際,我成功換了工作。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心

Jmeter系列(32)- 詳解 CSV 數據文件設置

如果你想從頭學習Jmeter,可以看看這個系列的文章哦

https://www.cnblogs.com/poloyy/category/1746599.html

 

了解一哈什麼是 CSV 文件

  • 為了實現簡單的數據存儲,是一個純文本的文件
  • 最通用的一種文件格式,它可以非常容易地被導入各種PC表格及數據庫中
  • CSV 文件可以用記事本、excel打開;用記事本打開的話,每一列數據都用逗號隔開

 

為什麼要用 CSV 數據文件?

  • 從外部導入測試數據,相當於數據參數化
  • 通過從文件中導入大量的測試數據,來模擬大量真實用戶發送併發請求

 

CSV 數據文件設置

 

CSV 數據文件設置界面介紹

 

字段含義

字段 含義
Filename 文件名
File encoding 文件編碼
Variable Names
  • 變量名稱
  • 多個變量用 , 分隔
Ignore first line
  • 忽略首行
  • 只在設置了變量名稱后才生效
Delimiter
  • 分隔符
  • 默認 , 
Allow quoted data? 是否允許帶引號
Recycle on EOF? 遇到文件結束符EOF 后再次循環
Stop thread on EOF? 遇到文件結束符EOF 后停止運行線程?
Sharing mode 線程共享模式

後續通過各種栗子來深入理解常用字段的含義

 

單個字段的栗子

csv 測試數據

這裏用記事本方式當 CSV 數據文件,共有 10 條記錄

 

線程組結構樹

${num} 是計數器裏面聲明的變量,從 1 開始遞增到 15

 

線程組屬性

線程數和數據量一致,都是 15

 

csv 數據文件設置

 

運行結果

 

知識點

  • 忽略首行 True:一般首行都是字段名字,比如栗子的 mobile,一般都需要忽略除非沒有字段名
  • 是否允許帶引號 False:可以看到有引號的三條記錄 8、9、10,都還是保留了引號
  • 再次循環 True:csv 文件共有 10 條記錄,但線程數有 15 個,循環 10 次后,重頭開始循環;可以看到 11-15的手機號和1-5的手機號
  • 停止線程 False:取了 10 次值之後就到了文件尾部,但並不會停止運行線程,後面會舉個反例

 

多個字段的綜合栗子

csv 測試數據

兩個字段,共有 10 條記錄,最後三條記錄有分別有三種引號

 

csv 數據文件設置

線程組結構樹和上面栗子差不多一樣,線程數仍然 = 15

和第一個例子的配置項相反:不忽略首行,允許帶引號,遇到文件結束符不再循環

 

運行結果

  • 不忽略首行就會把首行的字段名都返回回來,如:1-mobile-age
  • 數據有雙引號 “” 時,會把雙引號忽略掉,  單引號不算
  • EOF 是文件結束符,沒有開啟再次循環時,會直接返回 EOF

 

開啟遇到文件結束符停止線程

還是上個栗子的線程組,只是改了下配置項

 

運行結果

可以看到,線程數 = 15,但只有 10 條數據,當跑了 10 個線程后,沒有數據了,所以停止運行

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

※回頭車貨運收費標準

Java 多線程基礎(十二)生產者與消費者

 Java 多線程基礎(十二)生產者與消費者

一、生產者與消費者模型

生產者與消費者問題是個非常典型的多線程問題,涉及到的對象包括“生產者”、“消費者”、“倉庫”和“產品”。他們之間的關係如下:

①、生產者僅僅在倉儲未滿時候生產,倉滿則停止生產。
②、消費者僅僅在倉儲有產品時候才能消費,倉空則等待。
③、當消費者發現倉儲沒產品可消費時候會通知生產者生產。
④、生產者在生產出可消費產品時候,應該通知等待的消費者去消費。

生產者消費者模型具體來講,就是在一個系統中,存在生產者和消費者兩種角色,他們通過內存緩衝區進行通信,生產者生產消費者需要的資料,消費者把資料做成產品。生產消費者模式如下圖。

在日益發展的服務類型中,譬如註冊用戶這種服務,它可能解耦成好幾種獨立的服務(賬號驗證,郵箱驗證碼,手機短信碼等)。它們作為消費者,等待用戶輸入數據,在前台數據提交之後會經過分解併發送到各個服務所在的url,分發的那個角色就相當於生產者。消費者在獲取數據時候有可能一次不能處理完,那麼它們各自有一個請求隊列,那就是內存緩衝區了。做這項工作的框架叫做消息隊列。

二、生產者與消費者實現

下面通過生產包子的例子及wait()/notify()方式實現該模型(後面學習線程池相關內容之後,再通過其它方式實現生產/者消費者模型)。

麵包類:

public class Bread {
    private int capacity;    // 麵包的容量
    private int size;        // 麵包的實際數量
    public Bread(int capacity) {
        this.capacity = capacity;
        this.size = 0;
    }
    
    // 生產麵包
    public synchronized void produce(int val) {
        try {
             // left 表示“想要生產的數量”(有可能生產量太多,需多此生產)
            int left = val;
            while (left > 0) {
                // 庫存已滿時,等待“消費者”消費產品。
                while (size >= capacity)
                    wait();
                // 獲取“實際生產的數量”(即庫存中新增的數量)
                // 如果“庫存”+“想要生產的數量”>“總的容量”,則“實際增量”=“總的容量”-“當前容量”。(此時填滿倉庫)
                // 否則“實際增量”=“想要生產的數量”
                int inc = (size+left)>capacity ? (capacity-size) : left;
                size += inc;
                left -= inc;
                System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d\n",
                        Thread.currentThread().getName(), val, left, inc, size);
                // 通知“消費者”可以消費了。
                notifyAll();
            }
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
    // 消費麵包
    public synchronized void consume(int val) {
        try {
             // left 表示“客戶要消費數量”(有可能消費量太大,庫存不夠,需多此消費)
            int left = val;
            while (left > 0) {
                // 庫存為0時,等待“生產者”生產產品。
                while (size <= 0)
                    wait();
                // 獲取“實際消費的數量”(即庫存中實際減少的數量)
                // 如果“庫存”<“客戶要消費的數量”,則“實際消費量”=“庫存”;
                // 否則,“實際消費量”=“客戶要消費的數量”。
                int dec = (size<left) ? size : left;
                size -= dec;
                left -= dec;
                System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d\n",
                        Thread.currentThread().getName(), val, left, dec, size);
                notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

生產者類

public class Producer{
    Bread bread;
    public Producer(Bread bread) {
        this.bread = bread;
    }
    public void produce(final int val) {
        new Thread(() -> {
            bread.produce(val);
        }).start();;
    }
}

消費者類

public class Customer {
    private Bread bread;
    public Customer(Bread bread) {
        this.bread = bread;
    }
    public void consume(final int val) {
        new Thread(() -> {
            bread.consume(val);
        }).start();;
    }
}

測試類代碼

public class Demo {
    public static void main(String[] args) {
        Bread bread = new Bread(100);
        Producer producer = new Producer(bread);
        Cunstomer customer = new Customer(bread);
        
        producer.produce(60);
        producer.produce(120);
        consumer.consume(90);
        consumer.consume(150);
        producer.produce(110);
    }
}
// 運行結果
Thread-1 produce( 60) --> left=  0, inc= 60, size= 60
Thread-5 produce(110) --> left= 70, inc= 40, size=100
Thread-4 consume(150) <-- left= 50, dec=100, size=  0
Thread-2 produce(120) --> left= 20, inc=100, size=100
Thread-3 consume( 90) <-- left=  0, dec= 90, size= 10
Thread-4 consume(150) <-- left= 40, dec= 10, size=  0
Thread-5 produce(110) --> left=  0, inc= 70, size= 70
Thread-4 consume(150) <-- left=  0, dec= 40, size= 30
Thread-2 produce(120) --> left=  0, inc= 20, size= 50

說明:

①、Producer是“生產者”類,它與“麵包(bread)”關聯。當調用“生產者”的produce()方法時,它會新建一個線程並向“麵包類”中生產產品。
②、Customer是“消費者”類,它與“麵包(bread)”關聯。當調用“消費者”的consume()方法時,它會新建一個線程並消費“麵包類”中的產品。
③、Bread是麵包類,記錄“麵包的產量(capacity)”以及麵包當前實際數目(size)”。
        麵包類的生產方法produce()和消費方法consume()方法都是synchronized方法,進入synchronized方法體,意味着這個線程獲取到了該“麵包”對象的同步鎖。這也就是說,同一時間,生產者和消費者線程只能有一個能運行。通過同步鎖,實現了對“殘酷”的互斥訪問。
       對於生產方法 produce() 而言:當麵包量滿時,生產者線程等待,需要等待消費者消費產品之後,生產線程才能生產;生產者線程生產完麵包之後,會通過 notifyAll() 喚醒同步鎖上的所有線程,包括“消費者線程”,即我們所說的“通知消費者進行消費”。
      對於消費方法consume()而言:當倉庫為空時,消費者線程等待,需要等待生產者生產產品之後,消費者線程才能消費;消費者線程消費完產品之後,會通過 notifyAll() 喚醒同步鎖上的所有線程,包括“生產者線程”,即我們所說的“通知生產者進行生產”。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

基於用戶的協同過濾來構建推薦系統

1.概述

之前介紹了如何構建一個推薦系統,今天給大家介紹如何基於用戶的協同過濾來構建推薦的實戰篇。

2.內容

協同過濾技術在推薦系統中應用的比較廣泛,它是一個快速發展的研究領域。它比較常用的兩種方法是基於內存(Memory-Based)和基於模型(Model-Based)。

  • 基於內存:主要通過計算近似度來進行推薦,比如基於用戶(Used-Based)和基於物品(Item-Based)的協同過濾,這兩個模式中都會首先構建用戶交互矩陣,然後矩陣的行向量和列向量可以用來表示用戶和物品,然後計算用戶和物品的相似度來進行推薦;
  • 基於模型:主要是對交互矩陣進行填充,預測用戶購買某個物品的可能性。

為了解決這些問題,可以通過建立協同過濾模型,利用購買數據向客戶推薦產品。下面,我們通過基於用戶的協同過濾(基於內存),通過實戰來一步步實現其中的細節。基於用戶的系統過濾體現在具有相似特徵的人擁有相似的喜好。比如,用戶A向用戶B推薦了物品C,而B購買過很多類似C的物品,並且評價也高。那麼,在未來,用戶B也會有很大的可能會去購買物品C,並且用戶B會基於相似度度量來推薦物品C。

2.1 基於用戶與用戶的協同過濾

這種方式識別與查詢用戶相似的用戶,並估計期望的評分為這些相似用戶評分的加權平均值。實戰所使用的Python語言,這裏需要依賴的庫如下:

  • pandas
  • numpy
  • sklearn

Python環境:

  • 版本3.7.6
  • Anaconda3

2.2 評分函數

這裏給非個性化協同過濾(不包含活躍用戶的喜歡、不喜歡、以及歷史評分),返回一個以用戶U和物品I作為輸入參數的分數。該函數輸出一個分數,用於量化用戶U喜歡 / 偏愛物品I的程度。這通常是通過對與用戶相似的人的評分來完成的。涉及的公式如下:

 

 這裏其中s為預測得分,u為用戶,i為物品,r為用戶給出的評分,w為權重。在這種情況下,我們的分數等於每個用戶對該項目的評價減去該用戶的平均評價再乘以某個權重的總和,這個權重表示該用戶與其他用戶有多少相似之處,或者對其他用戶的預測有多少貢獻。這是用戶u和v之間的權重,分數在0到1之間,其中0是最低的,1是最高的。理論上看起來非常完美,那為啥需要從每個用戶的評分中減去平均評分,為啥要使用加權平均而不是簡單平均?這是因為我們所處理的用戶類型,首先,人們通常在不同的尺度上打分,用戶A可能是一個积極樂觀的用戶,會給用戶A自己喜歡的電影平均高分(例如4分、或者5分)。而用戶B是一個不樂觀或者對評分標準比較高的用戶,他可能對最喜歡的電影評分為2分到5分之間。用戶B的2分對應到用戶A的4分。改進之處是可以通過規範化用戶評分來提高算法效率。一種方法是計算s(u,i)的分數,它是用戶對每件物品的平均評價加上一些偏差。通過使用餘弦相似度來計算上述公式中給出的權重,同時,按照上述方式對數據進行歸一化,在pandas中進行一些數據分析。

2.2.1 導入Python依賴包

import pandas as pd
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.metrics import pairwise_distances

2.2.2 加載數據源

加載數據示例代碼如下所示:

movies = pd.read_csv("data/movies.csv")
Ratings = pd.read_csv("data/ratings.csv")
Tags = pd.read_csv("data/tags.csv")

結果預覽如下:

print(movies.head())
print(Ratings.head())
print(Tags.head())

 

 構建數據:

Mean = Ratings.groupby(by="userId", as_index=False)['rating'].mean()
Rating_avg = pd.merge(Ratings, Mean, on='userId')
Rating_avg['adg_rating'] = Rating_avg['rating_x'] - Rating_avg['rating_y']
print(Rating_avg.head())

結果如下:

 

2.3 餘弦相似度

 對於上面的公式,我們需要找到有相似想法的用戶。找到一個喜歡和不喜歡的用戶聽起來很有意思,但是我們如何找到相似性呢?那麼這裏我們就需要用到餘弦相似度,看看用戶有多相似。它通常是根據用戶過去的評分來計算的。

這裏使用到Python的的sklearn的cosine_similarity函數來計算相似性,並做一些數據預處理和數據清洗。實例代碼如下:

check = pd.pivot_table(Rating_avg,values='rating_x',index='userId',columns='movieId')
print(check.head())
final = pd.pivot_table(Rating_avg,values='adg_rating',index='userId',columns='movieId')
print(final.head())

結果如下:

上圖中包含了很多NaN的值,這是因為每個用戶都沒有看過所有的電影,所以這種類型的矩陣被稱為稀疏矩陣。類似矩陣分解的方法被用來處理這種稀疏性,接下來,我們來對這些NaN值做相關替換。

這裏通常有兩種方式:

  1. 使用行上的用戶平均值;
  2. 用戶在列上的電影平均值

代碼如下:

# Replacing NaN by Movie Average
final_movie = final.fillna(final.mean(axis=0))
print(final_movie.head())

# Replacing NaN by user Average
final_user = final.apply(lambda row: row.fillna(row.mean()), axis=1)
print(final_user.head())

結果如下:

 

 接着,我們開始計算用戶之間的相似性,代碼如下:

# user similarity on replacing NAN by item(movie) avg
cosine = cosine_similarity(final_movie)
np.fill_diagonal(cosine, 0)
similarity_with_movie = pd.DataFrame(cosine, index=final_movie.index)
similarity_with_movie.columns = final_user.index
# print(similarity_with_movie.head())

# user similarity on replacing NAN by user avg
b = cosine_similarity(final_user)
np.fill_diagonal(b, 0 )
similarity_with_user = pd.DataFrame(b,index=final_user.index)
similarity_with_user.columns=final_user.index
# print(similarity_with_user.head())

結果如下:

 

 然後,我們來檢驗一下我們的相似度是否有效,代碼如下:

def get_user_similar_movies( user1, user2 ):
    common_movies = Rating_avg[Rating_avg.userId == user1].merge(
    Rating_avg[Rating_avg.userId == user2],
    on = "movieId",
    how = "inner" )
    return common_movies.merge( movies, on = 'movieId' )

a = get_user_similar_movies(370,86309)
a = a.loc[ : , ['rating_x_x','rating_x_y','title']]
print(a.head())

結果如下:

 

 從上圖中,我們可以看出產生的相似度幾乎是相同的,符合真實性。

2.4 相鄰用戶

在2.3中計算了所有用戶的相似度,但是在大數據領域,推薦系統與大數據相結合是至關重要的。以電影推薦為例子,構建一個矩陣(862 * 862),這個與實際的用戶數據(百萬、千萬或者更多)相比,這是一個很小的矩陣。因此在計算任何物品的分數時,如果總是查看所有其他用戶將不是一個好的解決方案或者方法。因此,採用相鄰用戶的思路,對於特定用戶,只取K個類似用戶的集合。

下面,我們對K取值30,所有的用戶都有30個相鄰用戶,代碼如下:

def find_n_neighbours(df,n):
    order = np.argsort(df.values, axis=1)[:, :n]
    df = df.apply(lambda x: pd.Series(x.sort_values(ascending=False)
           .iloc[:n].index, 
          index=['top{}'.format(i) for i in range(1, n+1)]), axis=1)
    return df

# top 30 neighbours for each user
sim_user_30_u = find_n_neighbours(similarity_with_user,30)
print(sim_user_30_u.head())

sim_user_30_m = find_n_neighbours(similarity_with_movie,30)
print(sim_user_30_m.head())

結果如下:

 

 2.5 計算最後得分

實現代碼如下所示:

def User_item_score(user,item):
    a = sim_user_30_m[sim_user_30_m.index==user].values
    b = a.squeeze().tolist()
    c = final_movie.loc[:,item]
    d = c[c.index.isin(b)]
    f = d[d.notnull()]
    avg_user = Mean.loc[Mean['userId'] == user,'rating'].values[0]
    index = f.index.values.squeeze().tolist()
    corr = similarity_with_movie.loc[user,index]
    fin = pd.concat([f, corr], axis=1)
    fin.columns = ['adg_score','correlation']
    fin['score']=fin.apply(lambda x:x['adg_score'] * x['correlation'],axis=1)
    nume = fin['score'].sum()
    deno = fin['correlation'].sum()
    final_score = avg_user + (nume/deno)
    return final_score

score = User_item_score(320,7371)
print("score (u,i) is",score)

結果如下:

 

這裏我們算出來的預測分數是4.25,因此可以認為用戶(370),可能喜歡ID(7371)的電影。接下來,我們給用戶(370)做電影推薦,實現代碼如下:

Rating_avg = Rating_avg.astype({"movieId": str})
Movie_user = Rating_avg.groupby(by = 'userId')['movieId'].apply(lambda x:','.join(x))

def User_item_score1(user):
    Movie_seen_by_user = check.columns[check[check.index==user].notna().any()].tolist()
    a = sim_user_30_m[sim_user_30_m.index==user].values
    b = a.squeeze().tolist()
    d = Movie_user[Movie_user.index.isin(b)]
    l = ','.join(d.values)
    Movie_seen_by_similar_users = l.split(',')
    Movies_under_consideration = list(set(Movie_seen_by_similar_users)-set(list(map(str, Movie_seen_by_user))))
    Movies_under_consideration = list(map(int, Movies_under_consideration))
    score = []
    for item in Movies_under_consideration:
        c = final_movie.loc[:,item]
        d = c[c.index.isin(b)]
        f = d[d.notnull()]
        avg_user = Mean.loc[Mean['userId'] == user,'rating'].values[0]
        index = f.index.values.squeeze().tolist()
        corr = similarity_with_movie.loc[user,index]
        fin = pd.concat([f, corr], axis=1)
        fin.columns = ['adg_score','correlation']
        fin['score']=fin.apply(lambda x:x['adg_score'] * x['correlation'],axis=1)
        nume = fin['score'].sum()
        deno = fin['correlation'].sum()
        final_score = avg_user + (nume/deno)
        score.append(final_score)
    data = pd.DataFrame({'movieId':Movies_under_consideration,'score':score})
    top_5_recommendation = data.sort_values(by='score',ascending=False).head(5)
    Movie_Name = top_5_recommendation.merge(movies, how='inner', on='movieId')
    Movie_Names = Movie_Name.title.values.tolist()
    return Movie_Names

user = int(input("Enter the user id to whom you want to recommend : "))
predicted_movies = User_item_score1(user)
print(" ")
print("The Recommendations for User Id : 370")
print("   ")
for i in predicted_movies:
    print(i)

結果如下:

 

3.總結

基於用戶的協同過濾,流程簡述如下:

  1. 採集數據 & 存儲數據
  2. 加載數據
  3. 數據建模(數據預處理 & 數據清洗)
  4. 計算相似性(餘弦相似度、相鄰計算)
  5. 得分預測(預測和最終得分計算)
  6. 物品推薦

4.結束語

這篇博客就和大家分享到這裏,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或發送郵件給我,我會盡我所能為您解答,與君共勉!

另外,博主出書了《Kafka並不難學》和《Hadoop大數據挖掘從入門到進階實戰》,喜歡的朋友或同學, 可以在公告欄那裡點擊購買鏈接購買博主的書進行學習,在此感謝大家的支持。關注下面公眾號,根據提示,可免費獲取書籍的教學視頻

,

這篇博客就和大家分享到這裏,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或發送郵件給我,我會盡我所能為您解答,與君共勉!

另外,博主出書了《Kafka並不難學》和《Hadoop大數據挖掘從入門到進階實戰》,喜歡的朋友或同學, 可以在公告欄那裡點擊購買鏈接購買博主的書進行學習,在此感謝大家的支持。關注下面公眾號,根據提示,可免費獲取書籍的教學視頻

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

程序員不能說自己不行啊

二哥,最近我剛進了一家公司,之前跟你說過,培訓出身剛剛畢業,打算在北京打拚。最近進公司,給安排了工作,今天第一次沒人帶,自己上手搞代碼,搞不出,明明挺簡單的功能,自己還是做不出,不知道從哪裡學習,想趕快熟悉工作,可是自己的能力不行,在地鐵上常看二哥原創的作品,平常积極在看,超級希望能自己學到本事,但自己的能力真的有點問題,工作搞不完,害怕被問,害怕任務完不成被辭退。

以上是讀者西瓜向我提的一個問題,我覺得挺具有代表性的,所以決定拉出來單獨寫一篇文章答疑解惑一下。

可以肯定的一點是,任何時候都要說自己不行啊,尤其是男性同胞,可以認慫,但是“不行”這個兩個字千萬不要輕易說出口,為什麼?你懂吧?

人的能力各有不同,但如果你自己都不自信,那又能做好什麼事情呢?心理建設非常重要。

記得之前看一個短片,一個小男孩跳了無數次,都無法越過障礙物,但是呢,他身邊的同學一直為他加油吶喊,小男孩呢,也從來沒有放棄的打算,最後的結果我都快看哭了,他真的跳過去了,他出色地完成了自我挑戰。

他的成功,離不開同學們的鼓勵,但更重要的是他鍥而不舍的精神,心裏素質比一般的成年人都要強大。

我現在已經為人父了,雖然我一直標榜自己只有 18 歲,但叫二叔的讀者真的越來越多,我已經逆來順受了。在我的教育觀念里,我覺得我家女兒最優秀的一點品質,就是,如果她喜歡一件事,她就會主動去鑽研,去摸索,在沒有任何外人的幫助下。

你比如說,現在比較流行的平衡車,就是不帶腳踏板和鏈條的自行車。一開始,我想給她報個班,至少有個老師教教,對吧?

但是,她的表現完全出乎我的意料,我只要把車買回來,放到她的面前,怎麼騎,完全靠她自己去體驗。一開始小心翼翼,很保本,但她不滿足於現狀,就找一些小坡騎,然後是再大一點的坡,就這樣,挑戰一次又一次,自己就完全掌握了騎行的技術。

對於我來說,我沒有騎平衡車的經驗,小時候也沒有這玩意。我能做的除了買車,就是給她鼓勵,摔倒了沒事,哭了也沒事,有些事情,痛苦的同時,伴隨着挑戰和突破。

對於我們成年人來說,其實道理都懂的,但人與人之間的差距之所以拉開,除了選擇的正確有否,最大的因素我想就是,你有沒有自己主動去做

西瓜說自己明明很簡單的功能,就是做不出來。這種感覺我也有過,即便是現在有了十年多的編程經驗,仍然在某些時刻感到举手無措,無從下手。

對,這就是為什麼人要終身學習的原因啊。我們做不出來,除了思維上、認知上的局限性,另外一個重要的點就在於,你有沒有經驗。

對於新人來說,經驗肯定是欠缺的,這點毫無疑問,對吧?但是只要公司招你進去了,無論是不是培訓班出身,負責任的公司都會給你充足的時間和空間去進步,就看你自己有沒有主動。

我大三出去實習的時候,公司要求我做一個計算器,那時候覺得好難啊,因為加減乘除,再帶上小括號,運算是有優先級的,還要考慮到小括號的自動補齊,對於那時候菜得一筆的我來說,真特么難啊。

但能怎麼辦?做不出來就意味着要被辭退,那只有一個辦法,就是上網搜,找別人的例子模仿,拆分,融化,把它變成是自己的。

那時候,我還不會玩 GitHub、碼雲和開源中國,私下里主動學習的地方只有一個,好像是叫編程入門網,現在已經沒有了。我就是照着上面的例子,一個個手敲,當你例子敲多了,很多編程知識就融會貫通了。

現在好了,優秀的案例數不勝數。我的兩個好朋友,macrozheng 開源了他的電商平台 mall,江南一點雨開源了他的微人事系統 vhr,這兩個開源項目我一直強烈推薦新手去下載到本地,去學習。

很多時候,對於編程天賦一般的我們來說,不需要主動去造輪子,我們只需要去發現輪子,對吧?

我在一開始做 Web 管理系統的時候,找了一個企業級的開源系統,叫做 DWZ,不知道有沒有讀者朋友用過,當年非常火,我們公司的後台管理系統現在還在用,雖然說界面已經很古董了,但對於我們公司來說,足夠用了。

這套 DWZ 就封裝了很多前端組件,對於我一個 Java 程序員來說,非常友好,直接可以上手操作,如果一些組件不滿足,我就去改造。改造的過程中,就積攢了大把解決問題的實戰經驗,這是彌足珍貴的。

我在《Web全棧開發進階之路》這本書里,就借鑒了不少 DWZ 的優秀思想。不要覺得不會造輪子是可恥的,會用輪子也是真本領啊。

就西瓜來說,平常喜歡看我的原創文,那我文章涉及到的例子有沒有去敲呢?如果你敲了,你就會發現,文章里涉及到的例子能解決大部分新人在工作中遇到的問題,直接把這些作為自己的工具包,下次遇到拿來即用就可以了。

對於 Java 程序員來說,JDK 的原生 API 不能滿足需求的話,還有很多第三方的類庫,比如說 Apache 的,封裝了大量常用的工具類和方法。前提條件是,你必須得知道有這些東西,如果不知道的話,那就無從下手了,對吧?

那怎麼見多識廣呢?這就回到了之前所說的,你得去練,動手去練,無論是書本里的,還是文章里的,還是開源項目里的,你得去手操一遍,不要眼高手低,敲多了,自然就形成了自己解決問題的思路和方法。

擔心自己被辭退是一件好事,這會督促我們前進,對吧?有的人,有自驅力,不需要外力的干預就能奮發圖強,有的人,就需要一條看不見的鞭子抽打着,才會有前進的動力。

別懷疑自己,真的,人嘛,總是有能力強弱之分的,要學會接納自己,像二哥一樣自信點,腳踏實地,一點一點去進步,當你取得一點成績的時候就把這些當做是里程碑,隨着時間的推移,你就會發現,自己變禿了,不不不,變強了。

加油,西瓜!

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀。

本文已收錄 GitHub,傳送門~ ,裏面更有大廠面試完整考點,歡迎 Star。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

線性表的鏈式存儲–單鏈表

Java之線性表的鏈式存儲——單鏈表

我們都知道,線性表的存儲結構分為兩種,順序存儲結構和鏈式存儲結構,線性表的分類可以參考下圖來學習記憶。今天我們主要來學習一下鏈式存儲結構。

一、鏈式存儲介紹

“鏈式存儲結構,地址可以連續也可以不連續的存儲單元存儲數據元素”——來自定義。

其實,你可以想象這樣一個場景,你想找一個人(他的名字叫小譚),於是你首先去問 A , A 說他不知道,但是他說 B 可能知道,並告訴了你 B 在哪裡,於是你找到 B ,B 說他不知道,但是他說 C 可能知道,並告訴了你 C 的地址,於是你去找到 C ,C 真的知道小譚在何處。

上面場景其實可以幫助我們去理解鏈表,其實每一個鏈表都包含多個節點,節點又包含兩個部分,一個是數據域(儲存節點含有的信息),一個是指針域(儲存下一個節點或者上一個節點的地址),而這個指針域就相當於你去問B,B知道C的地址,這個指針域就是存放的 C 的地址。

鏈表下面其實又細分了3種:單鏈表、雙向鏈表和循環鏈表。今天我們先講單鏈表。

二、單鏈表介紹

什麼是單鏈表呢?單鏈表就是每一個節點只有一個指針域的鏈表。如下圖所示,就是一個帶頭節點的單鏈表。下面我們需要知道什麼是頭指針,頭節點和首元節點。

頭指針:指向鏈表節點的第一個節點的指針

頭節點:指在鏈表的首元節點之前附設的一個節點

首元節點:指在鏈表中存儲第一個實際數據元素的節點(比如上圖的 a1 節點)

三、單鏈表的創建

單鏈表的創建有兩種方式,分別是頭插法和尾插法。

1、頭插法

頭插法,顧名思義就是把新元素插入到頭部的位置,每次新加的元素都作為鏈表的第一個節點。那麼頭插入法在Java中怎麼實現呢。首先我們需要定義一個節點,如下

public class ListNode {
  public int val; //數據域
  public ListNode next;//指針域
}

然後我們就創建一個頭指針(不帶頭節點)

//元素個數
int n = 5;
//創建一個頭指針
ListNode headNode = new ListNode();
//頭插入法
headNode= createHead(headNode, n);

然後創建一個私有方法去實現頭插法,這裏我們插入5個新元素,頭插入的核心是要先斷開首元節點和頭指針的連接,也就是需要先將原來首元節點的地址存放到新節點的指針域里,也就是 newNode.next = headNode.next,然後再讓頭指針指向新的節點 headNode.next = newNode,這兩步是頭插入的核心,一定要理解。

/**
 * 頭插法
 * 新的節點放在頭節點的後面,之前的就放在新節點的後面
 * @param headNode 頭指針
 * @return
 */
private static ListNode createHead(ListNode headNode, int n) {
  //插入5個新節點
  for (int i = 1; i <= n; i++) {
    ListNode newNode = new ListNode();
    newNode.val = i;
    //將之前的所有節點指向新的節點(也就是新節點指向之前的所有節點)
    newNode.next = headNode.next;
    //將頭指針指向新的節點
    headNode.next = newNode;
  }
  return headNode;
}

最後我把鏈表打印輸出一下(其實也是單鏈表的遍歷),判斷條件就是只有當指針域為空的時候才是最後一個節點。

private static void printLinkedList(ListNode headNode) {
  int countNode = 0;
  while (headNode.next != null){
    countNode++;
    System.out.println(headNode.next.val);
    headNode = headNode.next;
  }
  System.out.println("該單鏈表的節點總數:" +countNode);
}

最後的輸出結果顯然是逆序,因為沒一個新的元素都是從頭部插入的,自然第一個就是最後一個,最後一個就是第一個:

2、尾插法

尾插法,顧名思義就是把新元素插入到尾部的位置(也就是最後一個位置),每次新加的元素都作為鏈表的第最後節點。那麼尾插法在 Java 中怎麼實現呢,這裏還是採用不帶頭節點的實現方式,頭節點和頭指針和頭插入的實現方式一樣,這裏我就直接將如何實現:

/**
 * 尾插法
 * 找到鏈表的末尾結點,把新添加的數據作為末尾結點的後續結點
 * @param headNode
 */
private static ListNode createByTail(ListNode headNode, int n) {
  //讓尾指針也指向頭指針
  ListNode tailNode = headNode;
  for (int i = 1; i <= n; i++) {
    ListNode newNode = new ListNode();
    newNode.val = i;
    newNode.next = null;

    //插入到鏈表尾部
    tailNode.next = newNode;
    //指向新的尾節點,tailer永遠存儲最後一個節點的地址
    tailNode = newNode;

  }
  return headNode;
}

和頭插入不同的是,我們需要聲明一個尾指針來輔助我們實現,最開始,尾指針指向頭指針,每插入一個元素,尾指針就后移一下,這裏我們來講一下原理:每次往末尾新加一個節點,我們就需要把原來的連接斷開,那怎麼斷開呢,我們首先需要讓尾指針指向新的節點,也就是 tailNode.next = newNode; 然後再讓尾指針后移一個位置,讓尾指針指向最後一個節點。也就是尾指針始終指向最後一個節點,最後將頭指針返回,輸出最後結果:

四、單鏈表的刪除

既然單鏈表創建好了,怎麼在鏈表裡面刪除元素呢,單鏈表的刪除,我分為了兩種情況刪除,分別是刪除第i個節點和刪除指定元素的節點。

1、刪除第i個節點

我們可以先來理一下思路:在單鏈表裡,節點與節點之間都是通過指針域鏈接起來的,所以如果我們想實現刪除的操作,實際上是需要我們去改變相應指針域對應得地址的。當想去刪除第i個元素的時候,比如要刪除上圖的第3個元素(也就是3),實際上我們要做的就是要讓2號元素指向4號元素(其實就是需要修改2號元素的指針域,讓2號元素的指針域存儲4號元素)。那麼怎麼做才能實現這一步呢?很顯然,要實現這個步驟,我們必須要找到4號元素和2號元素,但是再仔細想一下,其實我們只需要找到2號元素就可以了,因為4號元素的地址存儲再2號的下一個元素的指針域裏面。

所以綜上所述分析我們可以得出刪除的兩個核心步驟:

1.刪除第i個節點,需要先找到第 i-1 個個節點,也就是第i個節點的前一個節點;

2.然後讓第 i-1 個節點指向第 i-1 個節點的下下個節點

下面的代碼具體實現了怎麼刪除第i個元素。

/**
 * 刪除第i個節點
 * 1,2 4,4,5
 * 刪除之後應該是1,2,4,5
 * @param headNode
 * @param index
 * @return
 */
public static ListNode deleteNodeByIndex(ListNode headNode, int index) {
  int count = 1;
  //將引用給它
  ListNode preNode = headNode;
  //看計數器是不是到了i-1,如果到了i-1,就找到了第i-1個節點
  while (preNode.next != null && count <= index -1){
    //尋找要刪除的當前節點的前一個節點
    count++;
    preNode = preNode.next;
  }
  if (preNode != null){
    preNode.next = preNode.next.next;
  }
  return headNode;
}

2、刪除指定元素的那個節點

刪除指定元素節點的實現方法有兩種,第一種就是先找到指定元素對應的鏈表的位置( index ),然後再按照刪除第 i 個節點的思路刪除即可。實現方法如下圖所示:

/**
 * 刪除鏈表指定數值的節點
 * @param headNode
 * @param val
 * @return
 */
private static ListNode deleteNodeByNum(ListNode headNode, int val) {
  ListNode deleteOne = headNode;
  int countByDeleteOne = 1;
  while (deleteOne.next != null){
    if (deleteOne.next.val == val){
      deleteOne = deleteOne.next;
      break;
    }
    countByDeleteOne ++;
    deleteOne = deleteOne.next;
  }
  return deleteNodeByIndex(headNode, countByDeleteOne);
}

第二種方法的實現就很精妙(前提是此節點不是尾節點)

public void deleteNode(ListNode node) {
  //刪除node即通過將後面的值賦給node,然後更改node的指針指向下下一個結點即可
  node.val = node.next.val;
  node.next = node.next.next;
}

五、單鏈表的查詢(及修改)

單鏈表的查詢實現很簡單,就是遍歷當前單鏈表,然後用一個計數器累加到當前下標,那麼當前的這個節點就是要查詢的那個節點,然後再返回即可,當然需要判斷傳過來的這個下標是否合法。當然如果需要修改,就需要把當前找到的節點的數據域重新賦上需要修改的值即可,這裏就不上代碼了。具體實現如下:

private static ListNode searchLinkedList(ListNode headNode, int index) {
  //如果下標是不合法的下標就表示找不到
  if (index < 1 || index > getLinkedListLength(headNode)){
      return null;
  }
  for (int i = 0; i < index; i++) {
    headNode = headNode.next;
  }
  return headNode;
}

獲取單鏈表的長度(注意我這裏定義的 headNode 是頭指針不是頭節點)

/**
 * 求單鏈表長度
 * @param headNode
 * @return
 */
private static int getLinkedListLength(ListNode headNode) {
  int countNode = 0;
  while (headNode.next != null){
    countNode++;
    headNode = headNode.next;
  }
 return countNode;
}

六、小結

單鏈表的相關操作就講解完了,其實通過上面對單鏈表的相關操作,我們不難發現,單鏈表的刪除和插入其實很方便,只需要改變指針的指向就可以完成,但是查找元素的時候就比較麻煩,因為在查找的時候,需要把整個鏈表從頭到尾遍歷一次。

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?