From 39ea7e9cf3471a3c7d9e2d19cc257a323cf2f0c7 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Tue, 8 Nov 2022 15:46:33 +0000 Subject: [PATCH] deploy(blog): version 11/8/2022, 3:46:31 PM --- 200.html | 6 +++--- _nuxt/{497cc94.js => 5383f1f.js} | 2 +- _nuxt/{1e1241f.js => b5cc0d6.js} | 2 +- _nuxt/static/1667843362/manifest.js | 1 - _nuxt/static/1667843362/posts/34/payload.js | 1 - .../{1667843362 => 1667922389}/categories/2/payload.js | 0 .../{1667843362 => 1667922389}/categories/2/state.js | 2 +- .../{1667843362 => 1667922389}/categories/3/payload.js | 0 .../{1667843362 => 1667922389}/categories/3/state.js | 2 +- .../{1667843362 => 1667922389}/categories/4/payload.js | 0 .../{1667843362 => 1667922389}/categories/4/state.js | 2 +- .../{1667843362 => 1667922389}/categories/5/payload.js | 0 .../{1667843362 => 1667922389}/categories/5/state.js | 2 +- _nuxt/static/{1667843362 => 1667922389}/cv/payload.js | 0 _nuxt/static/1667922389/manifest.js | 1 + _nuxt/static/{1667843362 => 1667922389}/payload.js | 0 .../static/{1667843362 => 1667922389}/posts/11/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/11/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/12/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/12/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/13/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/13/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/14/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/14/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/15/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/15/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/16/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/16/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/17/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/17/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/20/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/20/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/21/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/21/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/22/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/22/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/25/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/25/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/26/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/26/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/27/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/27/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/28/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/28/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/29/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/29/state.js | 2 +- _nuxt/static/{1667843362 => 1667922389}/posts/3/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/3/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/30/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/30/state.js | 2 +- .../static/{1667843362 => 1667922389}/posts/31/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/31/state.js | 2 +- _nuxt/static/1667922389/posts/34/payload.js | 1 + _nuxt/static/{1667843362 => 1667922389}/posts/34/state.js | 2 +- _nuxt/static/{1667843362 => 1667922389}/posts/4/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/4/state.js | 2 +- _nuxt/static/{1667843362 => 1667922389}/posts/5/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/5/state.js | 2 +- _nuxt/static/{1667843362 => 1667922389}/posts/6/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/6/state.js | 2 +- _nuxt/static/{1667843362 => 1667922389}/posts/8/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/8/state.js | 2 +- _nuxt/static/{1667843362 => 1667922389}/posts/9/payload.js | 0 _nuxt/static/{1667843362 => 1667922389}/posts/9/state.js | 2 +- categories/2.html | 4 ++-- categories/3.html | 4 ++-- categories/4.html | 4 ++-- categories/5.html | 4 ++-- cv.html | 4 ++-- index.html | 4 ++-- posts/11.html | 4 ++-- posts/12.html | 4 ++-- posts/13.html | 4 ++-- posts/14.html | 4 ++-- posts/15.html | 4 ++-- posts/16.html | 4 ++-- posts/17.html | 4 ++-- posts/20.html | 4 ++-- posts/21.html | 4 ++-- posts/22.html | 4 ++-- posts/25.html | 4 ++-- posts/26.html | 4 ++-- posts/27.html | 4 ++-- posts/28.html | 4 ++-- posts/29.html | 4 ++-- posts/3.html | 4 ++-- posts/30.html | 4 ++-- posts/31.html | 4 ++-- posts/34.html | 7 +++++-- posts/4.html | 4 ++-- posts/5.html | 4 ++-- posts/6.html | 4 ++-- posts/8.html | 4 ++-- posts/9.html | 4 ++-- 94 files changed, 98 insertions(+), 95 deletions(-) rename _nuxt/{497cc94.js => 5383f1f.js} (97%) rename _nuxt/{1e1241f.js => b5cc0d6.js} (86%) delete mode 100644 _nuxt/static/1667843362/manifest.js delete mode 100644 _nuxt/static/1667843362/posts/34/payload.js rename _nuxt/static/{1667843362 => 1667922389}/categories/2/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/categories/2/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/categories/3/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/categories/3/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/categories/4/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/categories/4/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/categories/5/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/categories/5/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/cv/payload.js (100%) create mode 100644 _nuxt/static/1667922389/manifest.js rename _nuxt/static/{1667843362 => 1667922389}/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/11/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/11/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/12/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/12/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/13/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/13/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/14/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/14/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/15/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/15/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/16/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/16/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/17/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/17/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/20/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/20/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/21/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/21/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/22/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/22/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/25/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/25/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/26/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/26/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/27/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/27/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/28/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/28/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/29/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/29/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/3/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/3/state.js (74%) rename _nuxt/static/{1667843362 => 1667922389}/posts/30/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/30/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/31/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/31/state.js (75%) create mode 100644 _nuxt/static/1667922389/posts/34/payload.js rename _nuxt/static/{1667843362 => 1667922389}/posts/34/state.js (75%) rename _nuxt/static/{1667843362 => 1667922389}/posts/4/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/4/state.js (74%) rename _nuxt/static/{1667843362 => 1667922389}/posts/5/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/5/state.js (74%) rename _nuxt/static/{1667843362 => 1667922389}/posts/6/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/6/state.js (74%) rename _nuxt/static/{1667843362 => 1667922389}/posts/8/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/8/state.js (74%) rename _nuxt/static/{1667843362 => 1667922389}/posts/9/payload.js (100%) rename _nuxt/static/{1667843362 => 1667922389}/posts/9/state.js (74%) diff --git a/200.html b/200.html index fe261dd..a54a5be 100644 --- a/200.html +++ b/200.html @@ -1,9 +1,9 @@ - + -
Loading...
- +
Loading...
+ diff --git a/_nuxt/497cc94.js b/_nuxt/5383f1f.js similarity index 97% rename from _nuxt/497cc94.js rename to _nuxt/5383f1f.js index d5b8cd3..3fba191 100644 --- a/_nuxt/497cc94.js +++ b/_nuxt/5383f1f.js @@ -1 +1 @@ -!function(e){function r(data){for(var r,n,f=data[0],l=data[1],d=data[2],i=0,h=[];i1?arguments[1]:void 0,e),h=o>2?arguments[2]:void 0,d=void 0===h?e:r(h,e);d>c;)t[c++]=n;return t}},256:function(n,t,e){"use strict";e(251)},257:function(n,t,e){var l=e(105)(!1);l.push([n.i,'.footer[data-v-2c1886a6]{background:#333;color:#fff}.footer .footer-content[data-v-2c1886a6]{padding:20px 0;display:flex;flex-wrap:wrap;justify-content:space-between;color:hsla(0,0%,100%,.6)}.footer .footer-content .power[data-v-2c1886a6]{font-style:normal}.footer .footer-content .logo[data-v-2c1886a6]{font-size:16px;padding:5px 10px;border:1px dashed #fff;display:inline-block;margin-bottom:20px;color:#fff}.footer .footer-content .logo[data-v-2c1886a6]:hover{background-color:#fff;color:#000}.footer .footer-content .footer-title[data-v-2c1886a6]{color:#fff;font-size:12px;margin-bottom:5px;font-weight:bolder;transform:scaley(.8);text-shadow:1px 1px 1px rgba(0,0,0,.4)}.footer .footer-content .footer-text[data-v-2c1886a6]{font-size:12px;margin-bottom:20px}.footer .footer-content .footer-count[data-v-2c1886a6]{font-size:12px;opacity:0;font-size:0}.footer .footer-content .links a.link[data-v-2c1886a6]{color:hsla(0,0%,100%,.6);margin-right:10px;text-decoration:underline}.footer .footer-content .links a.link[data-v-2c1886a6]:hover{background-color:#fff;color:#000}.counter[data-v-2c1886a6]{margin-bottom:10px}.counter .counter-title[data-v-2c1886a6]{color:#fff;font-size:12px;margin-bottom:5px;font-weight:bolder;transform:scaley(.8);text-shadow:1px 1px 1px rgba(0,0,0,.4)}.counter .counter-content[data-v-2c1886a6]{border-radius:2px;padding:5px 10px;background:#fff;box-shadow:inset 1px 2px 10px rgba(0,0,0,.6);border-color:#444 #555 #444 rgba(0,0,0,.6);border-style:solid;border-width:5px}.counter .counter-content .counter-number[data-v-2c1886a6]{display:inline-block;padding:3px 5px;position:relative;background:#333;color:#fff;border-radius:5px;min-width:10px;text-align:center;margin-right:5px}.counter .counter-content .counter-number[data-v-2c1886a6]:last-child{margin:0}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(3){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(3):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(6){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(6):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(9){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(9):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(12){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(12):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(15){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(15):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(18){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(18):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(21){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(21):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(24){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(24):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(27){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(27):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(30){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(30):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(33){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(33):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(36){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(36):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(39){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(39):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(42){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(42):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(45){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(45):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(48){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(48):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(51){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(51):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(54){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(54):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(57){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(57):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(60){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(60):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:first-child{background-color:#8b0000;margin-left:0}.counter .counter-content .counter-number[data-v-2c1886a6]:first-child:before{display:none}',""]),n.exports=l},258:function(n,t,e){"use strict";e(157),e(23);var l=e(107),r=e.n(l),m={data:function(){return{nav:r.a.themeConfig.nav,headTitle:r.a.themeConfig.headTitle}}},o=(e(252),e(41)),component=Object(o.a)(m,(function(){var n=this,t=n._self._c;return t("div",{staticClass:"header-wrap"},[t("div",{staticClass:"header page"},[t("div",{staticClass:"left"},[t("div",{staticClass:"motto"},n._l(n.headTitle,(function(line,e){return t("div",{key:e,staticClass:"line"},[n._v(n._s(line))])})),0),n._v(" "),t("div",{staticClass:"nav"},n._l(n.nav,(function(link,e){return t("a",{key:e,staticClass:"nav-item",attrs:{href:link.link}},[n._v(n._s(link.name))])})),0)]),n._v(" "),t("div",{staticClass:"right"})])])}),[],!1,null,"249ad96f",null);t.a=component.exports},259:function(n,t,e){"use strict";e(157),e(23),e(254);var l=e(107),r=e.n(l),m={data:function(){return{year:(new Date).getFullYear(),friendLinks:r.a.themeConfig.friendLinks,extraFooters:r.a.themeConfig?r.a.themeConfig.extraFooters:[],pvNumber:"00000",uvNumber:"00000",pageCount:r.a.themeConfig.pageCount}},mounted:function(){var n=this,t=document.querySelector("#busuanzi_container_site_pv");new MutationObserver((function(t){var e=t[0].target,l=e.firstChild.textContent,r=e.lastChild.textContent;e.style.display="none";var m=Math.max(l.length,r.length,6);n.pvNumber=new Array(m-l.length).fill("0").join("")+l,n.uvNumber=new Array(m-r.length).fill("0").join("")+r})).observe(t,{attributes:!0})}},o=(e(256),e(41)),component=Object(o.a)(m,(function(){var n=this,t=n._self._c;return t("div",{staticClass:"footer"},[t("div",{staticClass:"footer-content page"},[t("div",{staticClass:"left"},[t("div",{staticClass:"footer-title"},[n._v("FRIEND LINKS")]),n._v(" "),t("div",{staticClass:"links footer-text"},n._l(n.friendLinks,(function(link,e){return t("a",{key:e,staticClass:"link",attrs:{href:link.link}},[n._v(n._s(link.name))])})),0),n._v(" "),n.pageCount?t("div",{staticClass:"footer-text"},[n._m(0),n._v(" "),t("div",{staticClass:"counter"},[t("div",{staticClass:"counter-title"},[n._v("PAGE VIEWS")]),n._v(" "),t("div",{staticClass:"counter-content"},n._l(n.pvNumber,(function(e,l){return t("span",{key:l,staticClass:"counter-number"},[n._v(n._s(e))])})),0)]),n._v(" "),t("div",{staticClass:"counter"},[t("div",{staticClass:"counter-title"},[n._v("USER VIEWS")]),n._v(" "),t("div",{staticClass:"counter-content"},n._l(n.uvNumber,(function(e,l){return t("span",{key:l,staticClass:"counter-number"},[n._v(n._s(e))])})),0)])]):n._e(),n._v(" "),n._l(n.extraFooters,(function(e,l){return t("div",{key:l},[t("div",{staticClass:"footer-title"},[n._v(n._s(e.title))]),n._v(" "),t("div",{staticClass:"footer-text"},[n._v(n._s(e.text))])])}))],2),n._v(" "),n._m(1)])])}),[function(){var n=this._self._c;return n("span",{staticClass:"footer-count",attrs:{id:"busuanzi_container_site_pv"}},[n("span",{attrs:{id:"busuanzi_value_site_pv"}}),this._v(" "),n("span",{attrs:{id:"busuanzi_value_site_uv"}})])},function(){var n=this,t=n._self._c;return t("div",{staticClass:"right"},[t("div",{staticClass:"footer-title power"},[n._v("POWERED BY")]),n._v(" "),t("a",{staticClass:"logo",attrs:{href:"https://github.com/Yidadaa/Issue-Blog-With-Github-Action"}},[n._v("ISSUE BLOG")])])}],!1,null,"2c1886a6",null);t.a=component.exports},266:function(n,t,e){var content=e(286);content.__esModule&&(content=content.default),"string"==typeof content&&(content=[[n.i,content,""]]),content.locals&&(n.exports=content.locals);(0,e(106).default)("2dd2eefc",content,!0,{sourceMap:!1})},284:function(n){n.exports=JSON.parse('{"3":{"id":3,"date":"2017/08/18","author":"Yidadaa","title":"经典统计学习方法——决策树(ID3/C4.5/CART)","mdContent":"最近开始技术♂转型,开始搞机器学习,使用教材是李航老师的《统计学习方法》,基本涵盖了经典的机器学习方法,只不过缺少神经网络部分,准备看完这本书之后,继续学习 [CS231n](http://cs231n.stanford.edu/) 。 \\r\\n\\r\\n前三章的内容都比较基础,第一章介绍了统计学习的基本要素以及基本步骤,第二、三章分别介绍了两种基础的模型:感知机和k近邻法,都比较简单,50行代码就能搞定。 \\r\\n\\r\\n从第四章开始,本书的难度开始逐渐拔高,书中出现了越来越多的数学证明,第四章的朴素贝叶斯分类器中,用到了很多概率论的知识,比如极大似然估计、 贝叶斯估计等。如果要深入机器学习原理的话,还是要回归到线性代数和概率论的学习中。 \\r\\n\\r\\n本文着重讲第五章的内容——决策树。\\r\\n## 决策树简述 \\r\\n决策树是本书出现的第一种真正意义上的高可用方法,当然前一章的朴素贝叶斯分类器也算,但从复杂度熵来讲,决策树更胜一筹。 \\r\\n\\r\\n决策树是一种基本的分类与回归方法,决策树的学习包括:特征选择、决策树的生成和决策树的修剪。其中特征选择决定了决策树的结构,决策树的生成和修剪阶段主要决定决策树的复杂度(深度)。 \\r\\n## 决策树的特征选择\\r\\n决策树特征选择方式的不同,衍生出了不同的算法,比如ID3、C4.5、CART等。以这三种为例,其实ID3与C4.5只是特征选择函数不同,前者依照信息增益,后者依照信息增益比,生成的树的结构很相似,同时也使用相同的剪枝算法。CART较为特殊,后文将其分开来讲。\\r\\n## ID3与C4.5算法\\r\\n决策树生成过程中,通过计算不同特征的划分带来的信息增益,可以决定是否继续生成决策树。 \\r\\n\\r\\n这两种算法生成的决策树,每一支子树都是一个特征值的可能值,使用时只需要按照标定的顺序,对输入变量各个特征值沿着树从根节点一直比对,延续到的叶节点就标定了该输入变量的最终分类。","content":"

最近开始技术♂转型,开始搞机器学习,使用教材是李航老师的《统计学习方法》,基本涵盖了经典的机器学习方法,只不过缺少神经网络部分,准备看完这本书之后,继续学习 CS231n

\\n

前三章的内容都比较基础,第一章介绍了统计学习的基本要素以及基本步骤,第二、三章分别介绍了两种基础的模型:感知机和k近邻法,都比较简单,50行代码就能搞定。

\\n

从第四章开始,本书的难度开始逐渐拔高,书中出现了越来越多的数学证明,第四章的朴素贝叶斯分类器中,用到了很多概率论的知识,比如极大似然估计、 贝叶斯估计等。如果要深入机器学习原理的话,还是要回归到线性代数和概率论的学习中。

\\n

本文着重讲第五章的内容——决策树。

\\n

决策树简述

\\n

决策树是本书出现的第一种真正意义上的高可用方法,当然前一章的朴素贝叶斯分类器也算,但从复杂度熵来讲,决策树更胜一筹。

\\n

决策树是一种基本的分类与回归方法,决策树的学习包括:特征选择、决策树的生成和决策树的修剪。其中特征选择决定了决策树的结构,决策树的生成和修剪阶段主要决定决策树的复杂度(深度)。

\\n

决策树的特征选择

\\n

决策树特征选择方式的不同,衍生出了不同的算法,比如ID3、C4.5、CART等。以这三种为例,其实ID3与C4.5只是特征选择函数不同,前者依照信息增益,后者依照信息增益比,生成的树的结构很相似,同时也使用相同的剪枝算法。CART较为特殊,后文将其分开来讲。

\\n

ID3与C4.5算法

\\n

决策树生成过程中,通过计算不同特征的划分带来的信息增益,可以决定是否继续生成决策树。

\\n

这两种算法生成的决策树,每一支子树都是一个特征值的可能值,使用时只需要按照标定的顺序,对输入变量各个特征值沿着树从根节点一直比对,延续到的叶节点就标定了该输入变量的最终分类。

\\n"},"4":{"id":4,"date":"2017/08/29","author":"Yidadaa","title":"物欲","mdContent":"初中时清心寡欲,最大的愿望是能每天从午饭钱中剩下五毛钱,然后周末的时候能有钱去一趟网吧,在有限的一两个小时内下满那张2G的内存卡,那时的同龄人经常有人问我去不去网吧,我说去,但是不玩游戏,只下载东西。仅有的网吧时光,启蒙了我对计算机的认识,但仅仅停留在使用的阶段,但那时我已有了一台学习机,供我娱乐以及编程。彼时的物欲,仅仅局限在3.5寸的屏幕内。\\r\\n\\r\\n高中时安卓设备崛起,我也第一次离家寄宿,可以掌控更多的生活费,正是如此,我的物欲开始膨胀。我十分渴望一台智能设备,并最终接二连三地缩减三餐费用,把这些钱拿来买各种各样的电子设备。数码杂志上朝思暮想的东西握在手里,我激动异常,把每一个用来睡眠和休息的夜晚和课间花在了各种各样的游戏上,我也渐渐变成了夜猫子。彼时,我的物欲渐渐膨胀,从3.5寸到4.3寸,再到7寸。\\r\\n\\r\\n高考前夕,我拥有了自己的笔记本电脑。进入大学后,这台电脑帮助我产出了各色的海报和成千上万行代码,当第一笔实习工资打到工资卡上时,我长舒一口气——我仿佛经能掌控自己的生活了。可是好景不长,我察觉到并不是这么回事,一不留神,白条就负债累累,更强大更智能的硬件层出不穷,自己的钱包和身体一样虚弱不堪,长年累月的熬夜犹如一场凌迟,一点一点掏空身体。此时,物欲已成长为庞然大物,不仅开始反噬,而且蒙蔽了我的双眼。\\r\\n\\r\\n我在最难熬的时候,装模作样地看过一些哲学书籍,也时常思索些高深的命题,虽然看起来很可笑,但这种思考让我明白,我是一种智慧生命,我会思考自己为何存在。但更多时候,我还是被物欲所支配着,渴望这个,渴望那个,时不时地拖延偷懒,到了真正考验自己本事的时候,被人打脸打的啪啪响,心里还不服,暗搓搓地想着等我哪天认真起来了,让你们都好看。**可是我知道,那始终是一句空话,就像我过去立的这么多flag一样,毫无意义。**\\r\\n\\r\\n转念一想,自己这幅德行,全都赖到“物欲”头上吗?物欲始终是个人为定义的概念,无法脱离行为而存在,若是没有那些虚与委蛇和敷衍了事时种下的因,又拿来这般扭曲不堪的果呢?我不得不承认我的心态已经有了“跑偏”的迹象,彼时对金钱嗤之以鼻,此时为求果腹不择手段(当然这是夸张的说法,只是觉得把大学的时光花费在乱七八糟的东西上实在该打),若不自省,日后不留神就会犯下大错。\\r\\n\\r\\n我还没想明白,自己究竟想要什么,站在这边的山上,觉得那边的山高,思来想去却没半点行路的意思。","content":"

初中时清心寡欲,最大的愿望是能每天从午饭钱中剩下五毛钱,然后周末的时候能有钱去一趟网吧,在有限的一两个小时内下满那张2G的内存卡,那时的同龄人经常有人问我去不去网吧,我说去,但是不玩游戏,只下载东西。仅有的网吧时光,启蒙了我对计算机的认识,但仅仅停留在使用的阶段,但那时我已有了一台学习机,供我娱乐以及编程。彼时的物欲,仅仅局限在3.5寸的屏幕内。

\\n

高中时安卓设备崛起,我也第一次离家寄宿,可以掌控更多的生活费,正是如此,我的物欲开始膨胀。我十分渴望一台智能设备,并最终接二连三地缩减三餐费用,把这些钱拿来买各种各样的电子设备。数码杂志上朝思暮想的东西握在手里,我激动异常,把每一个用来睡眠和休息的夜晚和课间花在了各种各样的游戏上,我也渐渐变成了夜猫子。彼时,我的物欲渐渐膨胀,从3.5寸到4.3寸,再到7寸。

\\n

高考前夕,我拥有了自己的笔记本电脑。进入大学后,这台电脑帮助我产出了各色的海报和成千上万行代码,当第一笔实习工资打到工资卡上时,我长舒一口气——我仿佛经能掌控自己的生活了。可是好景不长,我察觉到并不是这么回事,一不留神,白条就负债累累,更强大更智能的硬件层出不穷,自己的钱包和身体一样虚弱不堪,长年累月的熬夜犹如一场凌迟,一点一点掏空身体。此时,物欲已成长为庞然大物,不仅开始反噬,而且蒙蔽了我的双眼。

\\n

我在最难熬的时候,装模作样地看过一些哲学书籍,也时常思索些高深的命题,虽然看起来很可笑,但这种思考让我明白,我是一种智慧生命,我会思考自己为何存在。但更多时候,我还是被物欲所支配着,渴望这个,渴望那个,时不时地拖延偷懒,到了真正考验自己本事的时候,被人打脸打的啪啪响,心里还不服,暗搓搓地想着等我哪天认真起来了,让你们都好看。可是我知道,那始终是一句空话,就像我过去立的这么多flag一样,毫无意义。

\\n

转念一想,自己这幅德行,全都赖到“物欲”头上吗?物欲始终是个人为定义的概念,无法脱离行为而存在,若是没有那些虚与委蛇和敷衍了事时种下的因,又拿来这般扭曲不堪的果呢?我不得不承认我的心态已经有了“跑偏”的迹象,彼时对金钱嗤之以鼻,此时为求果腹不择手段(当然这是夸张的说法,只是觉得把大学的时光花费在乱七八糟的东西上实在该打),若不自省,日后不留神就会犯下大错。

\\n

我还没想明白,自己究竟想要什么,站在这边的山上,觉得那边的山高,思来想去却没半点行路的意思。

\\n"},"5":{"id":5,"date":"2017/09/07","author":"Yidadaa","title":"LaTex on Linux配置指南 - TexLive","mdContent":"> 系统:Ubuntu 17.04 \\r\\n> 软件搭配:texLive2017 + VSCode\\r\\n\\r\\n## 简述 \\r\\n\\r\\n导师要求我用英文写周报,而且强制要求用LaTex完成,于是乎就花了点时间配置了一下LaTex环境。目前有很多IDE形式的LaTex集成环境了,但是我喜欢用一个编辑器搞定所有事情,于是选用了TexLive + VSCode的配置,其他配置可以参考这个[知乎答案](https://www.zhihu.com/question/19954023)。\\r\\n\\r\\n![2017-09-07 19-21-20](https://user-images.githubusercontent.com/16968934/30162000-78822d32-9405-11e7-9094-7d234b2a1b2a.png)\\r\\n\\r\\n\\r\\n## 安装VSCode \\r\\n\\r\\nVSCode是微软出的新一代编辑器,拥有很强大的插件市场支持,几乎可以胜任所有的开发工作。安装步骤如下:\\r\\n\\r\\n1. 从[官网](https://code.visualstudio.com/)下载`.deb`安装包;\\r\\n2. 在命令行中,切换到安装包所在目录,执行`dpkg -i 安装包名字.deb`;\\r\\n3. 安装安装包所需依赖,继续输入`apt-get -f install`。\\r\\n\\r\\n至此VSCode就安装完成了,打开VSCode,按下快捷键`CTRL+SHIFT+X`,搜索插件**LaTex Workshop**,安装并重启编辑器。\\r\\n\\r\\n至此,编辑器就装好了。\\r\\n\\r\\n## 安装TexLive\\r\\n\\r\\nTexLive是一个tex发行版,其他的介绍就不啰嗦了,直接开始安装。\\r\\n\\r\\n安装过程很简单,执行以下命令:\\r\\n```\\r\\nsudo apt-get install texlive-xetex texlive-latex-extra texlive-science\\r\\n```\\r\\n\\r\\n## 使用\\r\\n打开VSCode,新建一个目录,然后在目录中新建一个`.tex`文件,即可使用tex进行写作了,如果要开启即时预览功能,需要先编译该tex文件,按下`F1`,输入命令`build LaTex project`并回车,你会发现目录下面生成了一堆乱七八糟的东西,不用管,再次按下`F1`,输入命令`View PDF file in new tab`,就可以开启即时预览了,每一次保存后,预览窗口就会自动刷新。\\r\\n\\r\\n此外,为了支持中文,需要修改`Latex Workshop`的设置项,在VScode的设置文件中添加下面几行:\\r\\n```json\\r\\n\\"latex-workshop.latex.recipes\\": [\\r\\n {\\r\\n \\"name\\": \\"xelatex\\",\\r\\n \\"tools\\": [\\r\\n \\"xelatex\\"\\r\\n ]\\r\\n },\\r\\n {\\r\\n \\"name\\": \\"bibtex->xelatex\\",\\r\\n \\"tools\\": [\\r\\n \\"xelatex\\",\\r\\n \\"bibtex\\",\\r\\n \\"xelatex\\",\\r\\n \\"xelatex\\"\\r\\n ]\\r\\n }\\r\\n],\\r\\n\\"latex-workshop.latex.tools\\": [\\r\\n {\\r\\n \\"name\\": \\"xelatex\\",\\r\\n \\"command\\": \\"xelatex\\",\\r\\n \\"args\\": [\\r\\n \\"%DOC%.tex\\"\\r\\n ]\\r\\n }, {\\r\\n \\"name\\": \\"bibtex\\",\\r\\n \\"command\\": \\"bibtex\\",\\r\\n \\"args\\": [\\r\\n \\"%DOCFILE%\\"\\r\\n ]\\r\\n }\\r\\n]\\r\\n```\\r\\n这里笔者提供了两条编译命令流,一个是不需要引用的纯`xelatex`,一般用于课程设计报告的书写;另一个是包含了`bibtex`的命令流,一般用于会议论文的书写。\\r\\n\\r\\n然后新建一个文件测试一下:\\r\\n```latex\\r\\n\\\\documentclass[12pt]{article}\\r\\n\\\\usepackage{fontspec}\\r\\n\\\\setmainfont{宋体}\\r\\n\\\\title{镜中}\\r\\n\\\\author{张枣}\\r\\n\\\\date{}\\r\\n\\r\\n\\\\begin{document}\\r\\n\\\\begin{center}\\r\\n 只要想起一生中后悔的事情\\\\\\\\\\r\\n 梅花便落了下来\\\\\\\\\\r\\n 比如看她游泳到河的另一岸\\\\\\\\\\r\\n 比如登上一株松木梯子\\\\\\\\\\r\\n 危险的事情固然美丽\\\\\\\\\\r\\n 不如看她骑马归来\\\\\\\\\\r\\n 面颊温暖,\\\\\\\\\\r\\n 羞愧。低下头,回答着皇帝\\\\\\\\\\r\\n 一面镜子永远等候她\\\\\\\\\\r\\n 让她坐到镜中常坐的地方\\\\\\\\\\r\\n 望着窗外,只要想起一生中后悔的事情\\\\\\\\\\r\\n 梅花便落满了南山\\\\\\\\\\r\\n\\\\end{center}\\r\\n\\r\\n\\\\end{document}\\r\\n```\\r\\n![_code_20171125165943](https://user-images.githubusercontent.com/16968934/33229051-22acaa0a-d202-11e7-9eb8-a7f9e9d1719f.png)\\r\\n\\r\\n\\r\\n## 参考链接\\r\\n1. [Linux下texLive的安装和配置](http://blog.csdn.net/matricer/article/details/52253658)\\r\\n2. [使用xelatex生成中文pdf](http://blog.jqian.net/post/xelatex.html)","content":"
\\n

系统:Ubuntu 17.04
\\n软件搭配:texLive2017 + VSCode

\\n
\\n

简述

\\n

导师要求我用英文写周报,而且强制要求用LaTex完成,于是乎就花了点时间配置了一下LaTex环境。目前有很多IDE形式的LaTex集成环境了,但是我喜欢用一个编辑器搞定所有事情,于是选用了TexLive + VSCode的配置,其他配置可以参考这个知乎答案

\\n

\\"2017-09-07

\\n

安装VSCode

\\n

VSCode是微软出的新一代编辑器,拥有很强大的插件市场支持,几乎可以胜任所有的开发工作。安装步骤如下:

\\n
    \\n
  1. 官网下载.deb安装包;
  2. \\n
  3. 在命令行中,切换到安装包所在目录,执行dpkg -i 安装包名字.deb
  4. \\n
  5. 安装安装包所需依赖,继续输入apt-get -f install
  6. \\n
\\n

至此VSCode就安装完成了,打开VSCode,按下快捷键CTRL+SHIFT+X,搜索插件LaTex Workshop,安装并重启编辑器。

\\n

至此,编辑器就装好了。

\\n

安装TexLive

\\n

TexLive是一个tex发行版,其他的介绍就不啰嗦了,直接开始安装。

\\n

安装过程很简单,执行以下命令:

\\n
sudo apt-get install texlive-xetex texlive-latex-extra texlive-science\\n
\\n

使用

\\n

打开VSCode,新建一个目录,然后在目录中新建一个.tex文件,即可使用tex进行写作了,如果要开启即时预览功能,需要先编译该tex文件,按下F1,输入命令build LaTex project并回车,你会发现目录下面生成了一堆乱七八糟的东西,不用管,再次按下F1,输入命令View PDF file in new tab,就可以开启即时预览了,每一次保存后,预览窗口就会自动刷新。

\\n

此外,为了支持中文,需要修改Latex Workshop的设置项,在VScode的设置文件中添加下面几行:

\\n
"latex-workshop.latex.recipes": [\\n    {\\n        "name": "xelatex",\\n        "tools": [\\n            "xelatex"\\n        ]\\n    },\\n    {\\n        "name": "bibtex->xelatex",\\n        "tools": [\\n            "xelatex",\\n            "bibtex",\\n            "xelatex",\\n            "xelatex"\\n        ]\\n    }\\n],\\n"latex-workshop.latex.tools": [\\n    {\\n        "name": "xelatex",\\n        "command": "xelatex",\\n        "args": [\\n            "%DOC%.tex"\\n        ]\\n    }, {\\n        "name": "bibtex",\\n        "command": "bibtex",\\n        "args": [\\n            "%DOCFILE%"\\n        ]\\n    }\\n]\\n
\\n

这里笔者提供了两条编译命令流,一个是不需要引用的纯xelatex,一般用于课程设计报告的书写;另一个是包含了bibtex的命令流,一般用于会议论文的书写。

\\n

然后新建一个文件测试一下:

\\n
\\\\documentclass[12pt]{article}\\n\\\\usepackage{fontspec}\\n\\\\setmainfont{宋体}\\n\\\\title{镜中}\\n\\\\author{张枣}\\n\\\\date{}\\n\\n\\\\begin{document}\\n\\\\begin{center}\\n    只要想起一生中后悔的事情\\\\\\\\\\n    梅花便落了下来\\\\\\\\\\n    比如看她游泳到河的另一岸\\\\\\\\\\n    比如登上一株松木梯子\\\\\\\\\\n    危险的事情固然美丽\\\\\\\\\\n    不如看她骑马归来\\\\\\\\\\n    面颊温暖,\\\\\\\\\\n    羞愧。低下头,回答着皇帝\\\\\\\\\\n    一面镜子永远等候她\\\\\\\\\\n    让她坐到镜中常坐的地方\\\\\\\\\\n    望着窗外,只要想起一生中后悔的事情\\\\\\\\\\n    梅花便落满了南山\\\\\\\\\\n\\\\end{center}\\n\\n\\\\end{document}\\n
\\n

\\"_code_20171125165943\\"

\\n

参考链接

\\n
    \\n
  1. Linux下texLive的安装和配置
  2. \\n
  3. 使用xelatex生成中文pdf
  4. \\n
\\n"},"6":{"id":6,"date":"2017/09/23","author":"Yidadaa","title":"如何优雅地使用服务器","mdContent":"最近师兄给我分配了一个实验室服务器地账号,自己就琢磨着怎么好好地折腾一下这个服务器,但无奈我的账号没有root权限,安装软件只能通过自己编译的方式完成,并且师兄只分配给我一个端口,我要想运行什么web服务,只能绑定到这一个端口上面,一头雾水之际,突然看到了一个神器:SSH端口转发。\\r\\n\\r\\n## 什么是SSH端口转发?\\r\\n\\r\\n让我们先来了解一下端口转发的概念吧。我们知道,SSH 会自动加密和解密所有 SSH 客户端与服务端之间的网络数据。但是,SSH 还同时提供了一个非常有用的功能,这就是端口转发。它能够将其他 TCP 端口的网络数据通过 SSH 链接来转发,并且自动提供了相应的加密及解密服务。这一过程有时也被叫做“隧道”(tunneling),这是因为 SSH 为其他 TCP 链接提供了一个安全的通道来进行传输而得名。例如,Telnet,SMTP,LDAP 这些 TCP 应用均能够从中得益,避免了用户名,密码以及隐私信息的明文传输。而与此同时,如果您工作环境中的防火墙限制了一些网络端口的使用,但是允许 SSH 的连接,那么也是能够通过将 TCP 端口转发来使用 SSH 进行通讯。总的来说 SSH 端口转发能够提供两大功能:\\r\\n- 加密 SSH Client 端至 SSH Server 端之间的通讯数据。\\r\\n- 突破防火墙的限制完成一些之前无法建立的 TCP 连接。\\r\\n\\r\\n上面是我复制粘贴[别人的](https://www.ibm.com/developerworks/cn/linux/l-cn-sshforward/),总而言之,端口转发就是将所有发送到server_1的m端口的请求转发到server_2的n端口上去,并且两个端口之间的转发,通过SSH加密完成,也就是说,只需要涉密服务器上的一个SSH端口,就可以通过端口转发功能来为我们提供各种各样的服务。\\r\\n\\r\\n## 如何使用端口转发?\\r\\n\\r\\n首先要确定你电脑装了ssh,命令行里输一下ssh即可,然后使用下面的命令:\\r\\n```bash\\r\\nssh -L :: \\r\\n```\\r\\n举个例子,我服务器的IP地址是`123.123.1.123`,SSH端口号是`88`,我在服务器的`8888`端口上开了一个`jupyter-notebook`的服务,那么我要想在我的笔记本上使用这个服务,就要将本地的`8888`端口给映射到服务器的`8888`端口去,那么我只需要输入命令`ssh -L 8888:localhost:8888 username@123.123.1.123 -p 88`,然后输入密码,映射就成功了,这时候访问本地的`8888`端口,就相当于访问服务器的`8888`端口了。\\r\\n\\r\\n## 还有其他的吗?\\r\\n当然有啦,为了全面模拟xshell的使用体验(大雾),当然还需要一个静态文件服务啦,可以用一行代码`python -m SimpleHTTPServer 100`在100端口开一个静态文件服务,然后用`ssh`转发100端口,这样就就可以在本地查看服务器的文件了,可以在调参的时候看生成的图片文件,当然了,我的野心不止于此,之后开始跑网络的时候,可以也用来扩展TensorBoard的功能。\\r\\n\\r\\n除此之外,还有其他的神奇的命令行可以使用,比如终端管理软件tmux,功能上与screen差不过,如果不不了解screen,可以查阅一下,tmux赋予了命令行的窗口化功能,可以分割、调整当前的终端窗口,经过配置之后可以获得非常不错的效果,当然啦,具体的配置还是去网上搜吧,一千人的电脑里有一千个终端配置,程序员还是要有自己的风格才行。","content":"

最近师兄给我分配了一个实验室服务器地账号,自己就琢磨着怎么好好地折腾一下这个服务器,但无奈我的账号没有root权限,安装软件只能通过自己编译的方式完成,并且师兄只分配给我一个端口,我要想运行什么web服务,只能绑定到这一个端口上面,一头雾水之际,突然看到了一个神器:SSH端口转发。

\\n

什么是SSH端口转发?

\\n

让我们先来了解一下端口转发的概念吧。我们知道,SSH 会自动加密和解密所有 SSH 客户端与服务端之间的网络数据。但是,SSH 还同时提供了一个非常有用的功能,这就是端口转发。它能够将其他 TCP 端口的网络数据通过 SSH 链接来转发,并且自动提供了相应的加密及解密服务。这一过程有时也被叫做“隧道”(tunneling),这是因为 SSH 为其他 TCP 链接提供了一个安全的通道来进行传输而得名。例如,Telnet,SMTP,LDAP 这些 TCP 应用均能够从中得益,避免了用户名,密码以及隐私信息的明文传输。而与此同时,如果您工作环境中的防火墙限制了一些网络端口的使用,但是允许 SSH 的连接,那么也是能够通过将 TCP 端口转发来使用 SSH 进行通讯。总的来说 SSH 端口转发能够提供两大功能:

\\n
    \\n
  • 加密 SSH Client 端至 SSH Server 端之间的通讯数据。
  • \\n
  • 突破防火墙的限制完成一些之前无法建立的 TCP 连接。
  • \\n
\\n

上面是我复制粘贴别人的,总而言之,端口转发就是将所有发送到server_1的m端口的请求转发到server_2的n端口上去,并且两个端口之间的转发,通过SSH加密完成,也就是说,只需要涉密服务器上的一个SSH端口,就可以通过端口转发功能来为我们提供各种各样的服务。

\\n

如何使用端口转发?

\\n

首先要确定你电脑装了ssh,命令行里输一下ssh即可,然后使用下面的命令:

\\n
ssh -L <local port>:<remote host>:<remote port> <SSH hostname>\\n
\\n

举个例子,我服务器的IP地址是123.123.1.123,SSH端口号是88,我在服务器的8888端口上开了一个jupyter-notebook的服务,那么我要想在我的笔记本上使用这个服务,就要将本地的8888端口给映射到服务器的8888端口去,那么我只需要输入命令ssh -L 8888:localhost:8888 username@123.123.1.123 -p 88,然后输入密码,映射就成功了,这时候访问本地的8888端口,就相当于访问服务器的8888端口了。

\\n

还有其他的吗?

\\n

当然有啦,为了全面模拟xshell的使用体验(大雾),当然还需要一个静态文件服务啦,可以用一行代码python -m SimpleHTTPServer 100在100端口开一个静态文件服务,然后用ssh转发100端口,这样就就可以在本地查看服务器的文件了,可以在调参的时候看生成的图片文件,当然了,我的野心不止于此,之后开始跑网络的时候,可以也用来扩展TensorBoard的功能。

\\n

除此之外,还有其他的神奇的命令行可以使用,比如终端管理软件tmux,功能上与screen差不过,如果不不了解screen,可以查阅一下,tmux赋予了命令行的窗口化功能,可以分割、调整当前的终端窗口,经过配置之后可以获得非常不错的效果,当然啦,具体的配置还是去网上搜吧,一千人的电脑里有一千个终端配置,程序员还是要有自己的风格才行。

\\n"},"8":{"id":8,"date":"2017/10/28","author":"Yidadaa","title":"如何有效地预估工作量","mdContent":"在实际开发过程中,难免要对自己手头的工作进行工作量预估,其实笔者一开始预估工作量的时候总是感到没谱,往往会得出过于乐观的结论,也就是所谓的“程序员的乐观”,老鸟程序员经常告诫我们说:“宁可多算一周,不可少估一天”。过于乐观地估计工作量,不仅会让自己疲于赶进度,还会连累其他的开发伙伴。所以本文就着重讲一下如何行之有效地做出正确的工作量预估。\\r\\n\\r\\n由于笔者能力有限,本文只讲单个开发者的情况,如果是团队leader进行规划的话,要考虑的东西只会更多,笔者也没有团队规划的经验,就不在此强答了。\\r\\n\\r\\n### 第一步:读懂需求,细分步骤。\\r\\n\\r\\n对需求一知半解就盲目地进行开发是**是非常危险**的行为,轻则遗漏细节,重则全盘皆错。很多开发者都不怎么重视这一阶段,觉得只有开始做之后才能逐渐地理解需求,其实不然,这一阶段要求对需求全貌有一个把握,把整个需求的难点和耗时的点给找出来,方便开发时认真对待。\\r\\n\\r\\n对于大部分需求,都能找出明确的步骤划分。比如我要做一个表格展示页,用来展示一些从后端取回的数据,那么就可以将之划分为编写页面和集成后端接口两个部分,而且根据自己以往的开发经验,还能大概知道哪里比较耗时,哪里还需要进一步地了解。\\r\\n\\r\\n### 第二步:把握轻重缓急,优先估计熟悉部分。\\r\\n\\r\\n无论项目复杂与否,肯定有些部分是自己最熟悉的,那么我们就优先从这些地方下手,根据以往的开发经验,确定一个时间点A,并将此确定为开发的第一阶段。\\r\\n\\r\\n### 第三步:肢解生疏需求,消灭项目盲点。\\r\\n\\r\\n除却熟悉的部分,下面就是真正的耗时耗力的阶段了,不熟悉的部分可以有三种形式:对技术细节不熟悉、对具体需求不熟悉和对团队协作过程不熟悉。\\r\\n\\r\\n1. **不熟悉的技术细节**。这时要单独估算学习新的技术知识所需的时间,根据未知知识与自己已有的储备知识的不同疏离程度,要分配不同的时间段B,比如对于前端来说,如果要学习的部分属于后端范畴或者是自己没有接触过的算法部分,那么就要相应的预估长一点。\\r\\n\\r\\n2. **不熟悉的需求细节**。如果拿到的需求文档有些部分语焉不详,自己就要长个心眼,把与制定需求的人沟通的过程也要估算在内,划定一个时间段C。\\r\\n\\r\\n3. **不熟悉的协调过程**。这种情况一般发生在刚接触新项目时,自己对项目的管理规范等等不熟悉,这时也要将沟通时间估算进去,划定时间段D。\\r\\n\\r\\n经过以上一番考虑,基本上就可以确定出一个比较具体的时间范围了。一定要切记,很多时候团队leader让成员估算开发时间时,最关心的往往并不是某个人能不能及时把手头的工作做完,而是如何分配,使得所有成员能够在指定的时间内完成一整个任务。所以错误的预估工作量和耗时,很可能会延误和其他人员的对接,造成整个项目完成时间的延后。\\r\\n\\r\\n*注:以上文字整理自笔者与某位年长十年的程序员的聊天记录,可能不适用于某些开发情况。*","content":"

在实际开发过程中,难免要对自己手头的工作进行工作量预估,其实笔者一开始预估工作量的时候总是感到没谱,往往会得出过于乐观的结论,也就是所谓的“程序员的乐观”,老鸟程序员经常告诫我们说:“宁可多算一周,不可少估一天”。过于乐观地估计工作量,不仅会让自己疲于赶进度,还会连累其他的开发伙伴。所以本文就着重讲一下如何行之有效地做出正确的工作量预估。

\\n

由于笔者能力有限,本文只讲单个开发者的情况,如果是团队leader进行规划的话,要考虑的东西只会更多,笔者也没有团队规划的经验,就不在此强答了。

\\n

第一步:读懂需求,细分步骤。

\\n

对需求一知半解就盲目地进行开发是是非常危险的行为,轻则遗漏细节,重则全盘皆错。很多开发者都不怎么重视这一阶段,觉得只有开始做之后才能逐渐地理解需求,其实不然,这一阶段要求对需求全貌有一个把握,把整个需求的难点和耗时的点给找出来,方便开发时认真对待。

\\n

对于大部分需求,都能找出明确的步骤划分。比如我要做一个表格展示页,用来展示一些从后端取回的数据,那么就可以将之划分为编写页面和集成后端接口两个部分,而且根据自己以往的开发经验,还能大概知道哪里比较耗时,哪里还需要进一步地了解。

\\n

第二步:把握轻重缓急,优先估计熟悉部分。

\\n

无论项目复杂与否,肯定有些部分是自己最熟悉的,那么我们就优先从这些地方下手,根据以往的开发经验,确定一个时间点A,并将此确定为开发的第一阶段。

\\n

第三步:肢解生疏需求,消灭项目盲点。

\\n

除却熟悉的部分,下面就是真正的耗时耗力的阶段了,不熟悉的部分可以有三种形式:对技术细节不熟悉、对具体需求不熟悉和对团队协作过程不熟悉。

\\n
    \\n
  1. \\n

    不熟悉的技术细节。这时要单独估算学习新的技术知识所需的时间,根据未知知识与自己已有的储备知识的不同疏离程度,要分配不同的时间段B,比如对于前端来说,如果要学习的部分属于后端范畴或者是自己没有接触过的算法部分,那么就要相应的预估长一点。

    \\n
  2. \\n
  3. \\n

    不熟悉的需求细节。如果拿到的需求文档有些部分语焉不详,自己就要长个心眼,把与制定需求的人沟通的过程也要估算在内,划定一个时间段C。

    \\n
  4. \\n
  5. \\n

    不熟悉的协调过程。这种情况一般发生在刚接触新项目时,自己对项目的管理规范等等不熟悉,这时也要将沟通时间估算进去,划定时间段D。

    \\n
  6. \\n
\\n

经过以上一番考虑,基本上就可以确定出一个比较具体的时间范围了。一定要切记,很多时候团队leader让成员估算开发时间时,最关心的往往并不是某个人能不能及时把手头的工作做完,而是如何分配,使得所有成员能够在指定的时间内完成一整个任务。所以错误的预估工作量和耗时,很可能会延误和其他人员的对接,造成整个项目完成时间的延后。

\\n

注:以上文字整理自笔者与某位年长十年的程序员的聊天记录,可能不适用于某些开发情况。

\\n"},"9":{"id":9,"date":"2017/11/02","author":"Yidadaa","title":"数据挖掘复习内容","mdContent":"# 数据挖掘期末复习\\r\\n## 第一章内容\\r\\n1. 什么是数据挖掘,数据挖掘与其他学科的联系。\\r\\n2. 知识发现的流程。数据挖掘是数据发现的核心。\\r\\n3. 数据挖掘的主要任务。关联规则挖掘、分类或回归、聚类和异常点检测。\\r\\n\\r\\n## 第二章 认识数据\\r\\n\\r\\n### 相似度计算\\r\\n计算欧氏距离以及另外一个算法。\\r\\n\\r\\n### 数据的统计描述\\r\\n包括数据的中心性描述(中位数、众数)和散度(极值、方差、百分位点)。\\r\\n\\r\\n### 数据预处理\\r\\n1. 数据清洗。噪声检测及缺失值处理。\\r\\n2. 数据集成。冗余分析和相关分析(卡方分析)。\\r\\n\\r\\n### 数据变换\\r\\n1. 最小最大归一化。\\r\\n2. (X-期望)/标准差\\r\\n\\r\\n## 第三章 数据仓库\\r\\n\\r\\n### 什么是数据仓库\\r\\n数据仓库是面向主题的、非易失的、随时间变化的、集成的。\\r\\n\\r\\n### 多维数据模型\\r\\n星型模型、雪花模型和事实星座模型。\\r\\n\\r\\n## 第四章 关联规则\\r\\n\\r\\n### 什么是频繁项集,如何从项集中获取关联规则\\r\\n数据中支持度大于最小支持度的的项集为频繁项集。\\r\\n\\r\\n### Apriori算法(重点必考)\\r\\n题型是从给定事务集合中计算关联规则。\\r\\n\\r\\n## 第五章 分类\\r\\n1. 监督学习和非监督学习\\r\\n2. 生成模型和判别模型。\\r\\n3. 分类和回归的异同。都是监督学习。一个是离散,一个是连续。\\r\\n\\r\\n### 决策树(重点)\\r\\n避免过拟合:增加数据量,降低模型复杂度。\\r\\n决策树通过剪枝来避免过拟合。\\r\\n\\r\\n### KNN(重点)\\r\\n属于懒惰学习,没有训练过程。 \\r\\n缺点:对K敏感 \\r\\n优点:无需训练 \\r\\n\\r\\n### 其他算法\\r\\n朴素贝叶斯、SVM、ANN和BP网络。\\r\\n\\r\\n### 评价指标\\r\\n准确率,召回率,敏感度,精度,F1\\r\\n\\r\\n## 第六章 聚类\\r\\n\\r\\n### 什么是聚类\\r\\n\\r\\n### 聚类的分类及相应算法\\r\\n1. 基于划分的算法。k-means,k中心\\r\\n2. 基于密度。DB scan\\r\\n3. 基于层次。层次聚类\\r\\n4. 基于网格。\\r\\n\\r\\n### k-means(重点)\\r\\nk-means的流程。\\r\\n\\r\\n## 第七章 异常检测\\r\\n\\r\\n### 什么是异常\\r\\n\\r\\n### 异常的类型\\r\\n全局、局部、集体、情景\\r\\n\\r\\n### LOF\\r\\n","content":"

数据挖掘期末复习

\\n

第一章内容

\\n
    \\n
  1. 什么是数据挖掘,数据挖掘与其他学科的联系。
  2. \\n
  3. 知识发现的流程。数据挖掘是数据发现的核心。
  4. \\n
  5. 数据挖掘的主要任务。关联规则挖掘、分类或回归、聚类和异常点检测。
  6. \\n
\\n

第二章 认识数据

\\n

相似度计算

\\n

计算欧氏距离以及另外一个算法。

\\n

数据的统计描述

\\n

包括数据的中心性描述(中位数、众数)和散度(极值、方差、百分位点)。

\\n

数据预处理

\\n
    \\n
  1. 数据清洗。噪声检测及缺失值处理。
  2. \\n
  3. 数据集成。冗余分析和相关分析(卡方分析)。
  4. \\n
\\n

数据变换

\\n
    \\n
  1. 最小最大归一化。
  2. \\n
  3. (X-期望)/标准差
  4. \\n
\\n

第三章 数据仓库

\\n

什么是数据仓库

\\n

数据仓库是面向主题的、非易失的、随时间变化的、集成的。

\\n

多维数据模型

\\n

星型模型、雪花模型和事实星座模型。

\\n

第四章 关联规则

\\n

什么是频繁项集,如何从项集中获取关联规则

\\n

数据中支持度大于最小支持度的的项集为频繁项集。

\\n

Apriori算法(重点必考)

\\n

题型是从给定事务集合中计算关联规则。

\\n

第五章 分类

\\n
    \\n
  1. 监督学习和非监督学习
  2. \\n
  3. 生成模型和判别模型。
  4. \\n
  5. 分类和回归的异同。都是监督学习。一个是离散,一个是连续。
  6. \\n
\\n

决策树(重点)

\\n

避免过拟合:增加数据量,降低模型复杂度。\\n决策树通过剪枝来避免过拟合。

\\n

KNN(重点)

\\n

属于懒惰学习,没有训练过程。
\\n缺点:对K敏感
\\n优点:无需训练

\\n

其他算法

\\n

朴素贝叶斯、SVM、ANN和BP网络。

\\n

评价指标

\\n

准确率,召回率,敏感度,精度,F1

\\n

第六章 聚类

\\n

什么是聚类

\\n

聚类的分类及相应算法

\\n
    \\n
  1. 基于划分的算法。k-means,k中心
  2. \\n
  3. 基于密度。DB scan
  4. \\n
  5. 基于层次。层次聚类
  6. \\n
  7. 基于网格。
  8. \\n
\\n

k-means(重点)

\\n

k-means的流程。

\\n

第七章 异常检测

\\n

什么是异常

\\n

异常的类型

\\n

全局、局部、集体、情景

\\n

LOF

\\n"},"11":{"id":11,"date":"2018/01/02","author":"Yidadaa","title":"2017年过去了,我很怀念它","mdContent":"> 你温柔如故,三月的柳絮纷飞,那副光景像一把红糖入水,初春的雪终于化了。\\r\\n\\r\\n### 前言\\r\\n\\r\\n今年注定是纪念意义非凡的一年:90一代的年轻人全部成年;属于很多人青春记忆的苍老师也结婚了;而对于我们来说,再过不到六个月,就要各奔东西,继续各自的人生。对于很多人,今年才是真正意义上的成年。\\r\\n\\r\\n不想以后的人生再浑浑噩噩下去,下定决心从今年开始有计划地生活起来,于是就有了这篇年终总结,而且以后每年都会做这样一个总结。\\r\\n\\r\\n### 摘要\\r\\n\\r\\n回望2017,许多事情如浮云飘过。\\r\\n\\r\\n年初的一次实习令我感触良多,不仅磨练了自己的技术,也促使我思考了很多形而上的问题。实习过后,我开始通过各种渠道接项目赚钱,其实后半年的主旋律就是接活-赚钱-接活-赚钱……\\r\\n\\r\\n那些形而上的问题就包括了未来的职业规划,我毕业后从事前端肯定是不行的,很快就会遇到天花板,工作内容也容易流于表面,所以个人倾向于从事高门槛的工作,比如算法工程师,但自己目前的算法能力还不够,于是在五月份决定留校读研。\\r\\n\\r\\n至于感情方面,没有进展,但已然列入2018的年度计划里,毕竟[孤独的人是可耻的](http://music.163.com/m/song?id=5279718)。\\r\\n\\r\\n此外就是健康和学业,这两个统统一蹶不振,后文会有详述。\\r\\n\\r\\n总而言之,本文总共包括五个部分,分别从财务、工程、学术、品格、感情、健康等方面对过去一年进行总结,并对未来一年做出期望。\\r\\n\\r\\n### 财务\\r\\n\\r\\n对于大多数大学生来说,所有烦恼的根源莫过于没钱。所幸自己有一些编程手艺,得以自己养活自己,还有余力支撑自己偶尔的挥霍。\\r\\n\\r\\n![2017](https://user-images.githubusercontent.com/16968934/34976947-bb6dd468-fad3-11e7-8d84-ff264097554a.JPG)\\r\\n\\r\\n从4月份开始,我养成了记账的习惯,刚开始的时候会很不适应,但是渐渐的就形成了条件反射,一开始觉得每一笔支出都记下来会特别繁琐,后来经过摸索,渐渐地形成了一些记账规则:\\r\\n\\r\\n1. 饭卡消费详情不记,只记充值金额。饭卡基本只能用来支付在食堂、浴室、洗衣和校内交通的消费,其中的大头是食堂花费,于是饭卡归到餐饮类中,每次只需要在充饭卡的时候记账即可;\\r\\n2. 朋友间借入借出不记。因为朋友间借账并没有产生实际的消费,记下来只会徒增流水,不利于年终分析,此类资金转换可以单开一个账本记录;\\r\\n3. 诸如日常使用支付宝或者微信支出的零食、饮料等消费,一般都会即刻记录下来,长久以来,可成习惯。\\r\\n\\r\\n回到2017全年消费状况。第四个月的入账金额,基本是实习期间发放的工资。其实实习工资给的很少,百度只给了150元/天的工资,然后每月按出勤天数发放工资,也就是说,每月实际入账不到3000,刚开始实习的三个月会有住房补助,金额是1650/月。当时我租的房子是2300/月,抛去房租和日常花费,工资所剩无几,所以说基本是存不下钱的,年初租房时还要靠父亲打钱资助。\\r\\n\\r\\n从五月份开始,我通过码市外包接活产生收入。五月份到七月份之间,替一个很豪爽的金主出钱开发浏览器,这段时间入账特别多,但好景不长,之后的金主都比较抠门,而且有一次与金主发生争执,项目不了了之,这一点其实是我自己的问题,在技术选型时固执己见,没有结合实际需求,导致项目流产。不过还好金主比较仁义,付了一部分钱。\\r\\n\\r\\n九月份开学之后,生活比较窘迫。有一个月没有接到什么活,很穷,不得已找老爸求助,老爸资助了3000块生活费,得以度过难关。\\r\\n\\r\\n10月份发生了两件事,一件是参加了小白健康这个众包组织,参与小程序开发,收入有限但相对来说比较稳定;另一件是接了一个算法项目,项目经历不可谓不坎坷,这件事放到下一章节详述,此项目分文未进,而且消耗了大量时间精力。\\r\\n\\r\\n11月份卖掉了性能孱弱的小米笔记本12.5寸低配版,购入华为Matebook X,所以这个月流水显得比较多。12月份和室友接了另外一个算法项目,产生了一些收入。\\r\\n\\r\\n2017各月收入大抵如上,再来看一看收支详情——我的钱从哪儿来,又到哪去了。\\r\\n\\r\\n![2017](https://user-images.githubusercontent.com/16968934/34977806-f2fc500a-fad6-11e7-895d-e44902703749.JPG)\\r\\n\\r\\n通过收支类目汇总可以看到,在自己“为什么这么穷”这个问题上,数码产品难辞其咎。这一年买了不少电子产品,更换了两台笔记本、一部手机,还买了手持稳定器和运动相机等各种小玩意儿。这些小玩意儿买来后不久就放在储物箱里吃灰了,实在是一大浪费。\\r\\n\\r\\n收入方面,来源比较单一,外包加工资占了总收入的70%,另外一些来自学校和家里的补助。\\r\\n\\r\\n**对于2018的展望**:\\r\\n1. 攒够自己和妹妹毕业旅行的钱;\\r\\n2. 保证自给自足。\\r\\n\\r\\n### 工程\\r\\n\\r\\n![3](https://user-images.githubusercontent.com/16968934/35086214-c374f6b6-fc66-11e7-8471-f53498671365.JPG)\\r\\n\\r\\n编程这项技能,如果不去用它,就会像刀刃一样生锈。目前的技术栈依然以前端为主,但慢慢的往算法方向转,要说有什么清晰的目标,就是争取能实现一个比较靠谱的工程项目。\\r\\n\\r\\n教务系统的插件是从大二下学期就开始做了,但是去年一整年进展都很慢,从 Github 的贡献频度图就可以看出,前三个月基本没有再动过代码,因为实习时很忙,工作上的内容总是掏空我的精力。返校后终于有时间写自己的代码了,但项目基本都托管在码市上,所以这部分的工作量也无法统计到底有多少。\\r\\n\\r\\n总体来说,虽然从数据上看,每周都能有代码产出,但是对比 Github 上其他热爱编程的用户,自己还有很大差距,个人理想的贡献频度是每周至少有5天产出,全年频度向1000进发。\\r\\n\\r\\n**对于2018的展望**:\\r\\n1. Coding 平均频度不低于 2/ 天,每周 Coding 记录不少于5天;\\r\\n2. 全年频度向1000进发;\\r\\n3. 4月份之前将教务系统插件完善放出,组织新生力量投入开发;\\r\\n4. lintCode 题库两遍以上。\\r\\n\\r\\n### 学术\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/35086717-970a4138-fc68-11e7-962f-0e72ee85faf0.png)\\r\\n\\r\\n说来惭愧,我在大学生里面,尤其是同届生里,算是不学无术的典范。\\r\\n\\r\\n先说成绩吧,大学四年一路“高歌猛降”,尤其是数学相关科目,基本都在60分线上徘徊,这对以后的转型是十分致命的。**如果想在研究生三年做出学术成果,一定要注意在数学这门课上花些功夫补救。**\\r\\n\\r\\n计算机基础课学得还算扎实,虽说成绩没有那么亮眼,但是很多概念都在写代码的时候重新实践了,也有了不一样的体会,自认为要比不少同龄人学得好些。\\r\\n\\r\\n未来一年的重点是科研能力,自己在出去实习之前基本没打算保研,出去实习之后,没有大学这个保护膜罩着,生活的锐利让我几近崩溃,最后不得不屁滚尿流地回了学校。同时也在大四上学期的关键时候决定保研,但是自己又对学校老师一无所知,最后投身于BMC门下,跟了一个专做工程的教授。但就个人而言,还是希望能在研究生水个几篇Paper。\\r\\n\\r\\n另外一个就是软技能的培养,做科研需要阅读大量外文文献,同时产出的也是英文文章,所以自己的英文水平也需要进行锻炼。大四上重修的《科技写作》让我认识到自己的写作已经退回到了初中生水平,实在汗颜。\\r\\n\\r\\n**对于2018的展望**:\\r\\n1. 系统地锻炼科研能力;\\r\\n2. 目标是产出一篇较高水平的论文;\\r\\n3. 保证每月足量的英语锻炼,包括背单词、阅读英文文献等。\\r\\n\\r\\n### 品格\\r\\n\\r\\n![35087661-f3e9c588-fc6b-11e7-8373-974891be6d94](https://user-images.githubusercontent.com/16968934/35088907-3ad9e438-fc70-11e7-91b1-6925b74478c7.jpg)\\r\\n\\r\\n我的书桌前的墙上贴着这样两张纸,一张是“九型人格测试表”,一张是“目光短浅、自以为是”。\\r\\n\\r\\n前者是《心理健康》课上做的一个测试,且不论可信度如何,其对我来讲还是有一些参考意义的。最下方一行是我的得分,我在“爱独特”、“爱知识”和“爱和平”三项上得到了很高的分数,这很符合我过去的习性,我一直奉行“技术至上”,避免与别人正面打交道,专心发展自己的技术水平,以便以后在职场上有足够的竞争力,我想这也是多数理工科学生的通性。而“爱权力”、“爱成功”和“爱秩序”的低分,则表示着我并没有太强的进取心,也就是没有太大的“野心”。这一点是表现得十分明显:大四上选择保研目标时,我草草决定了保本校,“事后诸葛亮”地想一下,当然是去外校更好一点。不过“事在人为”嘛,几个好朋友都留校了,只要刻苦努力,一定可以取得不错的成果。\\r\\n\\r\\n后者是一直以来用于警戒自己的警句,我视其为自己最大的两个缺点,在这上面吃过苦头,今后一定要努力避免。\\r\\n\\r\\n此外,在接外包项目的过程中,我接触到了形形色色的人,有的成熟老练,有的则平庸无趣。接触他人得同时也认识到了自身的不足。古语有云:“以铜为鉴,可以正衣冠;以人为鉴,可以明得失。”目前为止,自己对自身的品行有了一个大概了解,最大的缺点就是固执,六月份和何老师谈话时也被点出来过,她原话说:“你和张靖义虽然看起来截然不同,但有个最大的共性就是都比较固执。”我深以为然。\\r\\n\\r\\n一直以来,我有意识地控制自己,在对待问题时多考虑别人的感受,免得被自己一时鲁莽误事。同时也很感谢身边的朋友,每个人身上都有着值得学习的闪光之处,新的一年,不仅要保持谦逊,更要善待亲友,并向他们学习长处。\\r\\n\\r\\n**2018的展望**:\\r\\n1. 明确缺点:“自以为是,目光短浅”;\\r\\n2. 明确目标:“保持谦逊,锐意进取”;\\r\\n3. 小小的鸡汤:和老铁们一起努力成为群星中最闪耀的!\\r\\n\\r\\n### 感情\\r\\n\\r\\n[待完善]\\r\\n\\r\\n### 健康\\r\\n\\r\\n活着并不是一件容易的事情,尤其是健康地活着。\\r\\n\\r\\n由于作息持续紊乱,我身体整体的状况都下降得厉害,尤其是腰背部,稍不注意就会饱受腰肌劳损的折磨。另外精力也下降得厉害,所幸平常保持了一定的运动量,隔三差五就会去跑跑步,健康水平算是维持住了。\\r\\n\\r\\n这一章原本是有数据支撑的,但是小米手环的数据导出特别麻烦,就没有做。简单地概括一下,从五月份起,我的作息时间基本维持在上午10点到凌晨一点之间,有时会在这两个时间点前后浮动。这样的作息极其不健康,我这大半年吃早餐的次数屈指可数,体重也始终在60左右徘徊,我个人是希望体重能够达到65kg的,这样稍微练一下就可以练出比较漂亮的肌肉,比现在瘦如竹竿要强很多。\\r\\n\\r\\n**2018的展望**:\\r\\n1. 坚持跑步的好习惯,带动室友一起跑;\\r\\n2. 逐渐修正作息至00:00入睡,9:00起床;\\r\\n3. 体重向65kg进发。\\r\\n\\r\\n### 展望2018\\r\\n\\r\\n我是一个非常庸俗的人,这一点从本文的编排顺序就可以看出——我把“财务”放到了第一位,在平常的时候,如果连续一个月都没有进账,我会十分不开心。这样的表现与本人对自己的期望大相径庭,我理想中的自己是一个出世的居士,可以为院子里的花开了而高兴,也可以为满庭的落叶而哭泣,不以物喜,不以己悲,为享受生活而活,为成为自己而活。可如今自己如此痴迷金钱,我深感自责,但又一想,如果没有金钱,我可能连种花的院子都没有,到时候只能对着街头的垃圾堆而哭泣,想到这里,心里又释然了起来。\\r\\n\\r\\n所以2018年最大的愿望,就是**挣更多的钱**。所谓要想出世,必先入世,这是我为这个庸俗的愿望提出的歪理。\\r\\n\\r\\n其他的愿望都分散在文章各处了,这些是写出来的愿望,还有些不想写出的愿望,比如缺失的那个章节,因为我实在没有想到要写些什么,而且就算不写自己也会时刻惦记着,反而是那些对自己有督促作用而自己又不怎么上心的才需要写出来,并且在这里我还是要重复一遍:多多coding,多多运动,多多学习。\\r\\n\\r\\n2018年没有什么大不了的,大胆地活着吧。","content":"
\\n

你温柔如故,三月的柳絮纷飞,那副光景像一把红糖入水,初春的雪终于化了。

\\n
\\n

前言

\\n

今年注定是纪念意义非凡的一年:90一代的年轻人全部成年;属于很多人青春记忆的苍老师也结婚了;而对于我们来说,再过不到六个月,就要各奔东西,继续各自的人生。对于很多人,今年才是真正意义上的成年。

\\n

不想以后的人生再浑浑噩噩下去,下定决心从今年开始有计划地生活起来,于是就有了这篇年终总结,而且以后每年都会做这样一个总结。

\\n

摘要

\\n

回望2017,许多事情如浮云飘过。

\\n

年初的一次实习令我感触良多,不仅磨练了自己的技术,也促使我思考了很多形而上的问题。实习过后,我开始通过各种渠道接项目赚钱,其实后半年的主旋律就是接活-赚钱-接活-赚钱……

\\n

那些形而上的问题就包括了未来的职业规划,我毕业后从事前端肯定是不行的,很快就会遇到天花板,工作内容也容易流于表面,所以个人倾向于从事高门槛的工作,比如算法工程师,但自己目前的算法能力还不够,于是在五月份决定留校读研。

\\n

至于感情方面,没有进展,但已然列入2018的年度计划里,毕竟孤独的人是可耻的

\\n

此外就是健康和学业,这两个统统一蹶不振,后文会有详述。

\\n

总而言之,本文总共包括五个部分,分别从财务、工程、学术、品格、感情、健康等方面对过去一年进行总结,并对未来一年做出期望。

\\n

财务

\\n

对于大多数大学生来说,所有烦恼的根源莫过于没钱。所幸自己有一些编程手艺,得以自己养活自己,还有余力支撑自己偶尔的挥霍。

\\n

\\"2017\\"

\\n

从4月份开始,我养成了记账的习惯,刚开始的时候会很不适应,但是渐渐的就形成了条件反射,一开始觉得每一笔支出都记下来会特别繁琐,后来经过摸索,渐渐地形成了一些记账规则:

\\n
    \\n
  1. 饭卡消费详情不记,只记充值金额。饭卡基本只能用来支付在食堂、浴室、洗衣和校内交通的消费,其中的大头是食堂花费,于是饭卡归到餐饮类中,每次只需要在充饭卡的时候记账即可;
  2. \\n
  3. 朋友间借入借出不记。因为朋友间借账并没有产生实际的消费,记下来只会徒增流水,不利于年终分析,此类资金转换可以单开一个账本记录;
  4. \\n
  5. 诸如日常使用支付宝或者微信支出的零食、饮料等消费,一般都会即刻记录下来,长久以来,可成习惯。
  6. \\n
\\n

回到2017全年消费状况。第四个月的入账金额,基本是实习期间发放的工资。其实实习工资给的很少,百度只给了150元/天的工资,然后每月按出勤天数发放工资,也就是说,每月实际入账不到3000,刚开始实习的三个月会有住房补助,金额是1650/月。当时我租的房子是2300/月,抛去房租和日常花费,工资所剩无几,所以说基本是存不下钱的,年初租房时还要靠父亲打钱资助。

\\n

从五月份开始,我通过码市外包接活产生收入。五月份到七月份之间,替一个很豪爽的金主出钱开发浏览器,这段时间入账特别多,但好景不长,之后的金主都比较抠门,而且有一次与金主发生争执,项目不了了之,这一点其实是我自己的问题,在技术选型时固执己见,没有结合实际需求,导致项目流产。不过还好金主比较仁义,付了一部分钱。

\\n

九月份开学之后,生活比较窘迫。有一个月没有接到什么活,很穷,不得已找老爸求助,老爸资助了3000块生活费,得以度过难关。

\\n

10月份发生了两件事,一件是参加了小白健康这个众包组织,参与小程序开发,收入有限但相对来说比较稳定;另一件是接了一个算法项目,项目经历不可谓不坎坷,这件事放到下一章节详述,此项目分文未进,而且消耗了大量时间精力。

\\n

11月份卖掉了性能孱弱的小米笔记本12.5寸低配版,购入华为Matebook X,所以这个月流水显得比较多。12月份和室友接了另外一个算法项目,产生了一些收入。

\\n

2017各月收入大抵如上,再来看一看收支详情——我的钱从哪儿来,又到哪去了。

\\n

\\"2017\\"

\\n

通过收支类目汇总可以看到,在自己“为什么这么穷”这个问题上,数码产品难辞其咎。这一年买了不少电子产品,更换了两台笔记本、一部手机,还买了手持稳定器和运动相机等各种小玩意儿。这些小玩意儿买来后不久就放在储物箱里吃灰了,实在是一大浪费。

\\n

收入方面,来源比较单一,外包加工资占了总收入的70%,另外一些来自学校和家里的补助。

\\n

对于2018的展望

\\n
    \\n
  1. 攒够自己和妹妹毕业旅行的钱;
  2. \\n
  3. 保证自给自足。
  4. \\n
\\n

工程

\\n

\\"3\\"

\\n

编程这项技能,如果不去用它,就会像刀刃一样生锈。目前的技术栈依然以前端为主,但慢慢的往算法方向转,要说有什么清晰的目标,就是争取能实现一个比较靠谱的工程项目。

\\n

教务系统的插件是从大二下学期就开始做了,但是去年一整年进展都很慢,从 Github 的贡献频度图就可以看出,前三个月基本没有再动过代码,因为实习时很忙,工作上的内容总是掏空我的精力。返校后终于有时间写自己的代码了,但项目基本都托管在码市上,所以这部分的工作量也无法统计到底有多少。

\\n

总体来说,虽然从数据上看,每周都能有代码产出,但是对比 Github 上其他热爱编程的用户,自己还有很大差距,个人理想的贡献频度是每周至少有5天产出,全年频度向1000进发。

\\n

对于2018的展望

\\n
    \\n
  1. Coding 平均频度不低于 2/ 天,每周 Coding 记录不少于5天;
  2. \\n
  3. 全年频度向1000进发;
  4. \\n
  5. 4月份之前将教务系统插件完善放出,组织新生力量投入开发;
  6. \\n
  7. lintCode 题库两遍以上。
  8. \\n
\\n

学术

\\n

\\"image\\"

\\n

说来惭愧,我在大学生里面,尤其是同届生里,算是不学无术的典范。

\\n

先说成绩吧,大学四年一路“高歌猛降”,尤其是数学相关科目,基本都在60分线上徘徊,这对以后的转型是十分致命的。如果想在研究生三年做出学术成果,一定要注意在数学这门课上花些功夫补救。

\\n

计算机基础课学得还算扎实,虽说成绩没有那么亮眼,但是很多概念都在写代码的时候重新实践了,也有了不一样的体会,自认为要比不少同龄人学得好些。

\\n

未来一年的重点是科研能力,自己在出去实习之前基本没打算保研,出去实习之后,没有大学这个保护膜罩着,生活的锐利让我几近崩溃,最后不得不屁滚尿流地回了学校。同时也在大四上学期的关键时候决定保研,但是自己又对学校老师一无所知,最后投身于BMC门下,跟了一个专做工程的教授。但就个人而言,还是希望能在研究生水个几篇Paper。

\\n

另外一个就是软技能的培养,做科研需要阅读大量外文文献,同时产出的也是英文文章,所以自己的英文水平也需要进行锻炼。大四上重修的《科技写作》让我认识到自己的写作已经退回到了初中生水平,实在汗颜。

\\n

对于2018的展望

\\n
    \\n
  1. 系统地锻炼科研能力;
  2. \\n
  3. 目标是产出一篇较高水平的论文;
  4. \\n
  5. 保证每月足量的英语锻炼,包括背单词、阅读英文文献等。
  6. \\n
\\n

品格

\\n

\\"35087661-f3e9c588-fc6b-11e7-8373-974891be6d94\\"

\\n

我的书桌前的墙上贴着这样两张纸,一张是“九型人格测试表”,一张是“目光短浅、自以为是”。

\\n

前者是《心理健康》课上做的一个测试,且不论可信度如何,其对我来讲还是有一些参考意义的。最下方一行是我的得分,我在“爱独特”、“爱知识”和“爱和平”三项上得到了很高的分数,这很符合我过去的习性,我一直奉行“技术至上”,避免与别人正面打交道,专心发展自己的技术水平,以便以后在职场上有足够的竞争力,我想这也是多数理工科学生的通性。而“爱权力”、“爱成功”和“爱秩序”的低分,则表示着我并没有太强的进取心,也就是没有太大的“野心”。这一点是表现得十分明显:大四上选择保研目标时,我草草决定了保本校,“事后诸葛亮”地想一下,当然是去外校更好一点。不过“事在人为”嘛,几个好朋友都留校了,只要刻苦努力,一定可以取得不错的成果。

\\n

后者是一直以来用于警戒自己的警句,我视其为自己最大的两个缺点,在这上面吃过苦头,今后一定要努力避免。

\\n

此外,在接外包项目的过程中,我接触到了形形色色的人,有的成熟老练,有的则平庸无趣。接触他人得同时也认识到了自身的不足。古语有云:“以铜为鉴,可以正衣冠;以人为鉴,可以明得失。”目前为止,自己对自身的品行有了一个大概了解,最大的缺点就是固执,六月份和何老师谈话时也被点出来过,她原话说:“你和张靖义虽然看起来截然不同,但有个最大的共性就是都比较固执。”我深以为然。

\\n

一直以来,我有意识地控制自己,在对待问题时多考虑别人的感受,免得被自己一时鲁莽误事。同时也很感谢身边的朋友,每个人身上都有着值得学习的闪光之处,新的一年,不仅要保持谦逊,更要善待亲友,并向他们学习长处。

\\n

2018的展望

\\n
    \\n
  1. 明确缺点:“自以为是,目光短浅”;
  2. \\n
  3. 明确目标:“保持谦逊,锐意进取”;
  4. \\n
  5. 小小的鸡汤:和老铁们一起努力成为群星中最闪耀的!
  6. \\n
\\n

感情

\\n

[待完善]

\\n

健康

\\n

活着并不是一件容易的事情,尤其是健康地活着。

\\n

由于作息持续紊乱,我身体整体的状况都下降得厉害,尤其是腰背部,稍不注意就会饱受腰肌劳损的折磨。另外精力也下降得厉害,所幸平常保持了一定的运动量,隔三差五就会去跑跑步,健康水平算是维持住了。

\\n

这一章原本是有数据支撑的,但是小米手环的数据导出特别麻烦,就没有做。简单地概括一下,从五月份起,我的作息时间基本维持在上午10点到凌晨一点之间,有时会在这两个时间点前后浮动。这样的作息极其不健康,我这大半年吃早餐的次数屈指可数,体重也始终在60左右徘徊,我个人是希望体重能够达到65kg的,这样稍微练一下就可以练出比较漂亮的肌肉,比现在瘦如竹竿要强很多。

\\n

2018的展望

\\n
    \\n
  1. 坚持跑步的好习惯,带动室友一起跑;
  2. \\n
  3. 逐渐修正作息至00:00入睡,9:00起床;
  4. \\n
  5. 体重向65kg进发。
  6. \\n
\\n

展望2018

\\n

我是一个非常庸俗的人,这一点从本文的编排顺序就可以看出——我把“财务”放到了第一位,在平常的时候,如果连续一个月都没有进账,我会十分不开心。这样的表现与本人对自己的期望大相径庭,我理想中的自己是一个出世的居士,可以为院子里的花开了而高兴,也可以为满庭的落叶而哭泣,不以物喜,不以己悲,为享受生活而活,为成为自己而活。可如今自己如此痴迷金钱,我深感自责,但又一想,如果没有金钱,我可能连种花的院子都没有,到时候只能对着街头的垃圾堆而哭泣,想到这里,心里又释然了起来。

\\n

所以2018年最大的愿望,就是挣更多的钱。所谓要想出世,必先入世,这是我为这个庸俗的愿望提出的歪理。

\\n

其他的愿望都分散在文章各处了,这些是写出来的愿望,还有些不想写出的愿望,比如缺失的那个章节,因为我实在没有想到要写些什么,而且就算不写自己也会时刻惦记着,反而是那些对自己有督促作用而自己又不怎么上心的才需要写出来,并且在这里我还是要重复一遍:多多coding,多多运动,多多学习。

\\n

2018年没有什么大不了的,大胆地活着吧。

\\n"},"12":{"id":12,"date":"2018/02/11","author":"Yidadaa","title":"设计师如何与程序员进行有效沟通?","mdContent":"### 摘要\\r\\n需求方如何清楚地表达自己的新需求,设计师和程序员如何清楚地理解需求,程序员如何高效地将需求落地实现,是实际团队开发工作中每天都要面对的问题。本文描述一个需求从提出到落地的建议流程,旨在提高团队的沟通效率,提高总体生产力。\\r\\n\\r\\n### 需求的生命周期\\r\\n实际开发都是以需求为中心,所以需求是绝对的主角。一个完整的开发流程,往往包括**提出需求**、**理解需求**、**细化需求**、**实现需求**这样几个大的阶段,本文假设一个小型团队存在需求方、设计师和程序员三个主要角色,下面将对各个阶段以及各个角色的分工进行详解。\\r\\n\\r\\n**1. 提出需求** \\r\\n> 参与者:需求方 \\r\\n> 涉及材料:需求文档、原型图 \\r\\n> 建议使用工具:[墨刀](http://www.modao.cc/)、Microsoft PowerPoint \\r\\n\\r\\n此阶段,需求方应尽可能详尽并不失条理地描述出自己的需求,并形成文档以便后续使用。本文建议需求方在需求文档列出功能要点,并辅以原型图加以解释。\\r\\n\\r\\n![1](https://user-images.githubusercontent.com/16968934/36070792-86883648-0f3d-11e8-92d0-cc2b262845a7.JPG)\\r\\n\\r\\n原型图的作用仅仅是为了说明功能,应当尽可能地使用最简单直白的方式展现。如果提出的需求与现有项目无关,那么可以使用线框或者手绘;如果是在现有项目基础上提出,那么尽可能使用现有项目中的视觉元素来构建原型图。\\r\\n\\r\\n当涉及到多页面跳转时,应使用相应标识如连接线、跳转标注符号等来展示页面间的跳转关系。\\r\\n\\r\\n本文建议使用[墨刀](http://www.modao.cc/)或者PPT来绘制原型图。\\r\\n\\r\\n![default](https://user-images.githubusercontent.com/16968934/36070970-bb240770-0f41-11e8-8b6b-d856b695b504.jpg)\\r\\n\\r\\n**2. 理解需求** \\r\\n> 参与者:需求方、设计师、程序员 \\r\\n> 涉及材料:需求文档、原型图 \\r\\n> 建议使用工具:纸或者白板 \\r\\n\\r\\n此阶段,需求方组织设计师和程序员参与小型会议,需求方根据阶段一中的文档和原型图对需求进行详解,此时设计师和程序员应尽可能多地提出疑问并及时调整需求。\\r\\n\\r\\n当各方意见一致以后,进入需求细化阶段,设计师准备提供高保真设计图。\\r\\n\\r\\n**3. 细化需求** \\r\\n> 参与者:需求方、设计师、程序员 \\r\\n> 涉及材料:高保真设计稿 \\r\\n> 建议使用工具:Photoshop、Adobe Illustrator、Sketch \\r\\n\\r\\n此阶段,设计师着手制作高保真图像,高保真图像是最接近成品界面的设计稿,在绘制高保真图像过程中,设计师应及时就设计效果询问程序员是否有实现难度,比如某些动画效果可能会带来性能上的问题、某种阴影或者模糊效果无法用代码实现等等,这些风险应该在设计稿完成之前尽可能地消除掉,以免后续再次进行修改。\\r\\n\\r\\n高保真图像完成后,由需求方和程序员进行review,一定要保证各方无异议后再进入下一阶段。\\r\\n\\r\\n**4. 实现需求**\\r\\n> 参与者:设计师、程序员 \\r\\n> 涉及材料:标注图、DEMO\\r\\n> 建议使用工具:[PxCook像素大厨](http://www.fancynode.com.cn/pxcook)(Windows)、[Sketch Measure](http://sketch.im/plugins/1)(Mac) \\r\\n\\r\\n当高保真设计稿由各方review无异议后,应由设计师提供标注图给程序员,一份规范的标注图可以大幅提高程序员的编码效率,并且可以减少设计师与程序员的冗余沟通。\\r\\n\\r\\n一份规范的标注图应当清晰地展示出设计图中各个元素的**大小、位置、颜色、字号**等信息。\\r\\n\\r\\n![2](https://user-images.githubusercontent.com/16968934/36071511-42025d5c-0f4a-11e8-8399-8d83f90dbd21.JPG)\\r\\n\\r\\n程序员拿到标注图后,则立即投入开发。**为了提高效率,此阶段应尽量避免对标注图进行修改,即设计图中的所有的修改都尽量保证在此阶段之前完成。**\\r\\n\\r\\n### 附录\\r\\n- [在线原型工具墨刀快速上手指南](https://modao.cc/tutorials/%E5%BF%AB%E9%80%9F%E5%88%9B%E5%BB%BA%E5%BA%94%E7%94%A8)\\r\\n- [如何用科学的方法做出专业的原型图?](http://www.woshipm.com/rp/917962.html)\\r\\n- [移动端界面标注:如何理清标注的思路?](http://www.woshipm.com/pd/598672.html)\\r\\n- [UI设计师在跟程序员对接的时候,需要做到哪些能让沟通最高效的完成?](https://www.zhihu.com/question/30791244/answer/49523933)","content":"

摘要

\\n

需求方如何清楚地表达自己的新需求,设计师和程序员如何清楚地理解需求,程序员如何高效地将需求落地实现,是实际团队开发工作中每天都要面对的问题。本文描述一个需求从提出到落地的建议流程,旨在提高团队的沟通效率,提高总体生产力。

\\n

需求的生命周期

\\n

实际开发都是以需求为中心,所以需求是绝对的主角。一个完整的开发流程,往往包括提出需求理解需求细化需求实现需求这样几个大的阶段,本文假设一个小型团队存在需求方、设计师和程序员三个主要角色,下面将对各个阶段以及各个角色的分工进行详解。

\\n

1. 提出需求

\\n
\\n

参与者:需求方
\\n涉及材料:需求文档、原型图
\\n建议使用工具:墨刀、Microsoft PowerPoint

\\n
\\n

此阶段,需求方应尽可能详尽并不失条理地描述出自己的需求,并形成文档以便后续使用。本文建议需求方在需求文档列出功能要点,并辅以原型图加以解释。

\\n

\\"1\\"

\\n

原型图的作用仅仅是为了说明功能,应当尽可能地使用最简单直白的方式展现。如果提出的需求与现有项目无关,那么可以使用线框或者手绘;如果是在现有项目基础上提出,那么尽可能使用现有项目中的视觉元素来构建原型图。

\\n

当涉及到多页面跳转时,应使用相应标识如连接线、跳转标注符号等来展示页面间的跳转关系。

\\n

本文建议使用墨刀或者PPT来绘制原型图。

\\n

\\"default\\"

\\n

2. 理解需求

\\n
\\n

参与者:需求方、设计师、程序员
\\n涉及材料:需求文档、原型图
\\n建议使用工具:纸或者白板

\\n
\\n

此阶段,需求方组织设计师和程序员参与小型会议,需求方根据阶段一中的文档和原型图对需求进行详解,此时设计师和程序员应尽可能多地提出疑问并及时调整需求。

\\n

当各方意见一致以后,进入需求细化阶段,设计师准备提供高保真设计图。

\\n

3. 细化需求

\\n
\\n

参与者:需求方、设计师、程序员
\\n涉及材料:高保真设计稿
\\n建议使用工具:Photoshop、Adobe Illustrator、Sketch

\\n
\\n

此阶段,设计师着手制作高保真图像,高保真图像是最接近成品界面的设计稿,在绘制高保真图像过程中,设计师应及时就设计效果询问程序员是否有实现难度,比如某些动画效果可能会带来性能上的问题、某种阴影或者模糊效果无法用代码实现等等,这些风险应该在设计稿完成之前尽可能地消除掉,以免后续再次进行修改。

\\n

高保真图像完成后,由需求方和程序员进行review,一定要保证各方无异议后再进入下一阶段。

\\n

4. 实现需求

\\n
\\n

参与者:设计师、程序员
\\n涉及材料:标注图、DEMO\\n建议使用工具:PxCook像素大厨(Windows)、Sketch Measure(Mac)

\\n
\\n

当高保真设计稿由各方review无异议后,应由设计师提供标注图给程序员,一份规范的标注图可以大幅提高程序员的编码效率,并且可以减少设计师与程序员的冗余沟通。

\\n

一份规范的标注图应当清晰地展示出设计图中各个元素的大小、位置、颜色、字号等信息。

\\n

\\"2\\"

\\n

程序员拿到标注图后,则立即投入开发。为了提高效率,此阶段应尽量避免对标注图进行修改,即设计图中的所有的修改都尽量保证在此阶段之前完成。

\\n

附录

\\n\\n"},"13":{"id":13,"date":"2018/03/08","author":"Yidadaa","title":"品酒日记[长期更新]","mdContent":"> 本文用于记录笔者品尝的各种酒类的历程,纯娱乐。\\r\\n\\r\\n### 杜苏·阿斯蒂甜白低醇高泡葡萄酒\\r\\n**品种**:白葡萄酒 \\r\\n**口味** :甜型、果香 \\r\\n**产地**:意大利 \\r\\n**购买途径**:京东直采 \\r\\n**价格**:¥150 \\r\\n**品酒记录**:诚如酒名所言,入杯时泡沫四起,状如雪碧,片刻后泡沫消去,酒体为无色透明,有少许气泡着壁(其实这个酒真的挺像雪碧的)。初入口时有充气汽水的刺激感,同时伴有类似于原浆啤酒的香味,咽下时同样有轻微刺激感,其实到目前为止与RIO差不太多,但是咽下后口腔残留香味很浓,有点像蜜饯的味道,甜得有点腻那种感觉。总体来说,比较适合作为饮料来喝。 \\r\\n**记录日期**:2018-03-08\\r\\n\\r\\n----------\\r\\n\\r\\n### 桃乐丝特选公牛血干红葡萄酒 \\r\\n**品种**:红葡萄酒 \\r\\n**口味**:干型 \\r\\n**产地**:西班牙 \\r\\n**购买途径**:京东直采 \\r\\n**价格**:¥189 \\r\\n**品酒记录**:入杯的观感的确如血一样红,在灯光照耀下犹如宝石般闪耀,陶菊说能闻到香味,但是我只能闻到干红特有的刺鼻气味。第一口并无太多感觉,有些干,但并不涩,这一点要比涩如木屑的几十块的张裕解百纳好很多,入肚后一股温热涌上来,峙龙评价说像白酒,确实如此。西方人酿红酒时努力将糖分剔除,因为红酒是作为佐餐佳品,糖分过多会掩盖肉类的味道,很可惜中国人并没有这么讲究,喝酒只是为了一解忧愁,为了容易入口,才有下酒菜这一说,相对西方来说,中国的酒是主角,菜是配角,而西方人为了佐菜而喝酒,这点的确有些不同,这也造就了中国人喝红酒总是喜欢掺雪碧的“奇观”,不过转念一想,西方人喝茶还掺糖加奶呢!所以啊,干红还是得配雪碧才得以下咽,当然,有肉就另说啦! \\r\\n**记录日期**:2018-03-24 \\r\\n\\r\\n----------\\r\\n\\r\\n### 年华半甜葡萄酒 \\r\\n**品种**:白葡萄酒 \\r\\n**口味**:半甜型 \\r\\n**产地**:德国 \\r\\n**购买途径**:永辉超市 \\r\\n**价格**:¥59 \\r\\n**品酒记录**:酒体金黄,比较清澈,果香要比桃红酒淡上不少,刚入口有少许苦味,片刻后苦味散去,圆润的果香开始充斥口腔。总体来说跟桃红葡萄酒喝起来差不多。 \\r\\n**记录日期**:2018-04-21 \\r\\n\\r\\n----------\\r\\n\\r\\n### 甄爱五星白兰地\\r\\n**品种**:葡萄蒸馏酒 \\r\\n**口味**:N/A \\r\\n**产地**:青岛 \\r\\n**购买途径**:永辉超市 \\r\\n**价格**:¥48 \\r\\n**品酒记录**:好久没有喝过酒了,最近失去了经济来源,手头属实紧张,趁助学金发下来,去永辉整了瓶国产白兰地,这是我第一次喝国外的高度酒,实不相瞒,国内的白酒和各种梅子酒以及清酒完全喝不来,酒精味令我难以下咽,然而这瓶国产白兰地度数虽然高达40度,口感却格外清爽。开瓶后可以闻到葡萄酒特有的醇类和有机酸混合而成的味道,入口后会首先品尝到单宁多酚的苦味,随后混合着未蒸馏完全的单糖的甜味,酒体流过喉咙,乙醇的辛辣随之涌进鼻腔,然而却不似中国白酒那样猛烈,这种恰到好处的温柔让人回想起高中时代前桌女生的巴掌和脸庞接触后那种微微涨红的感觉,然后酒体穿肠过肚,一股暖流升腾而起,寒夜里升腾而起的火焰,窜到夜空里> 本文用于记录笔者品尝的各种酒类的历程,纯娱乐。\\r\\n\\r\\n### 杜苏·阿斯蒂甜白低醇高泡葡萄酒\\r\\n**品种**:白葡萄酒 \\r\\n**口味** :甜型、果香 \\r\\n**产地**:意大利 \\r\\n**购买途径**:京东直采 \\r\\n**价格**:¥150 \\r\\n**品酒记录**:诚如酒名所言,入杯时泡沫四起,状如雪碧,片刻后泡沫消去,酒体为无色透明,有少许气泡着壁(其实这个酒真的挺像雪碧的)。初入口时有充气汽水的刺激感,同时伴有类似于原浆啤酒的香味,咽下时同样有轻微刺激感,其实到目前为止与RIO差不太多,但是咽下后口腔残留香味很浓,有点像蜜饯的味道,甜得有点腻那种感觉。总体来说,比较适合作为饮料来喝。 \\r\\n**记录日期**:2018-03-08\\r\\n\\r\\n----------\\r\\n\\r\\n### 桃乐丝特选公牛血干红葡萄酒 \\r\\n**品种**:红葡萄酒 \\r\\n**口味**:干型 \\r\\n**产地**:西班牙 \\r\\n**购买途径**:京东直采 \\r\\n**价格**:¥189 \\r\\n**品酒记录**:入杯的观感的确如血一样红,在灯光照耀下犹如宝石般闪耀,陶菊说能闻到香味,但是我只能闻到干红特有的刺鼻气味。第一口并无太多感觉,有些干,但并不涩,这一点要比涩如木屑的几十块的张裕解百纳好很多,入肚后一股温热涌上来,峙龙评价说像白酒,确实如此。西方人酿红酒时努力将糖分剔除,因为红酒是作为佐餐佳品,糖分过多会掩盖肉类的味道,很可惜中国人并没有这么讲究,喝酒只是为了一解忧愁,为了容易入口,才有下酒菜这一说,相对西方来说,中国的酒是主角,菜是配角,而西方人为了佐菜而喝酒,这点的确有些不同,这也造就了中国人喝红酒总是喜欢掺雪碧的“奇观”,不过转念一想,西方人喝茶还掺糖加奶呢!所以啊,干红还是得配雪碧才得以下咽,当然,有肉就另说啦! \\r\\n**记录日期**:2018-03-24 \\r\\n\\r\\n----------\\r\\n\\r\\n### 年华半甜葡萄酒 \\r\\n**品种**:白葡萄酒 \\r\\n**口味**:半甜型 \\r\\n**产地**:德国 \\r\\n**购买途径**:永辉超市 \\r\\n**价格**:¥59 \\r\\n**品酒记录**:酒体金黄,比较清澈,果香要比桃红酒淡上不少,刚入口有少许苦味,片刻后苦味散去,圆润的果香开始充斥口腔。总体来说跟桃红葡萄酒喝起来差不多。 \\r\\n**记录日期**:2018-04-21 \\r\\n\\r\\n----------\\r\\n\\r\\n### 甄爱五星白兰地\\r\\n**品种**:葡萄蒸馏酒 \\r\\n**口味**:N/A \\r\\n**产地**:青岛 \\r\\n**购买途径**:永辉超市 \\r\\n**价格**:¥48 \\r\\n**品酒记录**:好久没有喝过酒了,最近失去了经济来源,手头属实紧张,趁助学金发下来,去永辉整了瓶国产白兰地,这是我第一次喝国外的高度酒,实不相瞒,国内的白酒和各种梅子酒以及清酒完全喝不来,酒精味令我难以下咽,然而这瓶国产白兰地度数虽然高达40度,口感却格外清爽。开瓶后可以闻到葡萄酒特有的醇类和有机酸混合而成的味道,入口后会首先品尝到单宁多酚的苦味,随后混合着未蒸馏完全的单糖的甜味,酒体流过喉咙,乙醇的辛辣随之涌进鼻腔,然而却不似中国白酒那样猛烈,这种恰到好处的温柔让人回想起高中时代前桌女生的巴掌和脸庞接触后那种微微涨红的感觉,然后酒体穿肠过肚,一股暖流涌来,寒夜里升腾而起的火焰,窜到夜空里攸尔消失不见。 \\r\\n**饮酒提示**: 适合独饮,若用于朋友聚会,可与果味饮料一同调制饮用,目前试过的最佳选择有水溶C100、果粒橙、柠檬味雪碧。\\r\\n**记录日期**:2019-10-11 ","content":"
\\n

本文用于记录笔者品尝的各种酒类的历程,纯娱乐。

\\n
\\n

杜苏·阿斯蒂甜白低醇高泡葡萄酒

\\n

品种:白葡萄酒
\\n口味 :甜型、果香
\\n产地:意大利
\\n购买途径:京东直采
\\n价格:¥150
\\n品酒记录:诚如酒名所言,入杯时泡沫四起,状如雪碧,片刻后泡沫消去,酒体为无色透明,有少许气泡着壁(其实这个酒真的挺像雪碧的)。初入口时有充气汽水的刺激感,同时伴有类似于原浆啤酒的香味,咽下时同样有轻微刺激感,其实到目前为止与RIO差不太多,但是咽下后口腔残留香味很浓,有点像蜜饯的味道,甜得有点腻那种感觉。总体来说,比较适合作为饮料来喝。
\\n记录日期:2018-03-08

\\n
\\n

桃乐丝特选公牛血干红葡萄酒

\\n

品种:红葡萄酒
\\n口味:干型
\\n产地:西班牙
\\n购买途径:京东直采
\\n价格:¥189
\\n品酒记录:入杯的观感的确如血一样红,在灯光照耀下犹如宝石般闪耀,陶菊说能闻到香味,但是我只能闻到干红特有的刺鼻气味。第一口并无太多感觉,有些干,但并不涩,这一点要比涩如木屑的几十块的张裕解百纳好很多,入肚后一股温热涌上来,峙龙评价说像白酒,确实如此。西方人酿红酒时努力将糖分剔除,因为红酒是作为佐餐佳品,糖分过多会掩盖肉类的味道,很可惜中国人并没有这么讲究,喝酒只是为了一解忧愁,为了容易入口,才有下酒菜这一说,相对西方来说,中国的酒是主角,菜是配角,而西方人为了佐菜而喝酒,这点的确有些不同,这也造就了中国人喝红酒总是喜欢掺雪碧的“奇观”,不过转念一想,西方人喝茶还掺糖加奶呢!所以啊,干红还是得配雪碧才得以下咽,当然,有肉就另说啦!
\\n记录日期:2018-03-24

\\n
\\n

年华半甜葡萄酒

\\n

品种:白葡萄酒
\\n口味:半甜型
\\n产地:德国
\\n购买途径:永辉超市
\\n价格:¥59
\\n品酒记录:酒体金黄,比较清澈,果香要比桃红酒淡上不少,刚入口有少许苦味,片刻后苦味散去,圆润的果香开始充斥口腔。总体来说跟桃红葡萄酒喝起来差不多。
\\n记录日期:2018-04-21

\\n
\\n

甄爱五星白兰地

\\n

品种:葡萄蒸馏酒
\\n口味:N/A
\\n产地:青岛
\\n购买途径:永辉超市
\\n价格:¥48
\\n品酒记录:好久没有喝过酒了,最近失去了经济来源,手头属实紧张,趁助学金发下来,去永辉整了瓶国产白兰地,这是我第一次喝国外的高度酒,实不相瞒,国内的白酒和各种梅子酒以及清酒完全喝不来,酒精味令我难以下咽,然而这瓶国产白兰地度数虽然高达40度,口感却格外清爽。开瓶后可以闻到葡萄酒特有的醇类和有机酸混合而成的味道,入口后会首先品尝到单宁多酚的苦味,随后混合着未蒸馏完全的单糖的甜味,酒体流过喉咙,乙醇的辛辣随之涌进鼻腔,然而却不似中国白酒那样猛烈,这种恰到好处的温柔让人回想起高中时代前桌女生的巴掌和脸庞接触后那种微微涨红的感觉,然后酒体穿肠过肚,一股暖流升腾而起,寒夜里升腾而起的火焰,窜到夜空里> 本文用于记录笔者品尝的各种酒类的历程,纯娱乐。

\\n

杜苏·阿斯蒂甜白低醇高泡葡萄酒

\\n

品种:白葡萄酒
\\n口味 :甜型、果香
\\n产地:意大利
\\n购买途径:京东直采
\\n价格:¥150
\\n品酒记录:诚如酒名所言,入杯时泡沫四起,状如雪碧,片刻后泡沫消去,酒体为无色透明,有少许气泡着壁(其实这个酒真的挺像雪碧的)。初入口时有充气汽水的刺激感,同时伴有类似于原浆啤酒的香味,咽下时同样有轻微刺激感,其实到目前为止与RIO差不太多,但是咽下后口腔残留香味很浓,有点像蜜饯的味道,甜得有点腻那种感觉。总体来说,比较适合作为饮料来喝。
\\n记录日期:2018-03-08

\\n
\\n

桃乐丝特选公牛血干红葡萄酒

\\n

品种:红葡萄酒
\\n口味:干型
\\n产地:西班牙
\\n购买途径:京东直采
\\n价格:¥189
\\n品酒记录:入杯的观感的确如血一样红,在灯光照耀下犹如宝石般闪耀,陶菊说能闻到香味,但是我只能闻到干红特有的刺鼻气味。第一口并无太多感觉,有些干,但并不涩,这一点要比涩如木屑的几十块的张裕解百纳好很多,入肚后一股温热涌上来,峙龙评价说像白酒,确实如此。西方人酿红酒时努力将糖分剔除,因为红酒是作为佐餐佳品,糖分过多会掩盖肉类的味道,很可惜中国人并没有这么讲究,喝酒只是为了一解忧愁,为了容易入口,才有下酒菜这一说,相对西方来说,中国的酒是主角,菜是配角,而西方人为了佐菜而喝酒,这点的确有些不同,这也造就了中国人喝红酒总是喜欢掺雪碧的“奇观”,不过转念一想,西方人喝茶还掺糖加奶呢!所以啊,干红还是得配雪碧才得以下咽,当然,有肉就另说啦!
\\n记录日期:2018-03-24

\\n
\\n

年华半甜葡萄酒

\\n

品种:白葡萄酒
\\n口味:半甜型
\\n产地:德国
\\n购买途径:永辉超市
\\n价格:¥59
\\n品酒记录:酒体金黄,比较清澈,果香要比桃红酒淡上不少,刚入口有少许苦味,片刻后苦味散去,圆润的果香开始充斥口腔。总体来说跟桃红葡萄酒喝起来差不多。
\\n记录日期:2018-04-21

\\n
\\n

甄爱五星白兰地

\\n

品种:葡萄蒸馏酒
\\n口味:N/A
\\n产地:青岛
\\n购买途径:永辉超市
\\n价格:¥48
\\n品酒记录:好久没有喝过酒了,最近失去了经济来源,手头属实紧张,趁助学金发下来,去永辉整了瓶国产白兰地,这是我第一次喝国外的高度酒,实不相瞒,国内的白酒和各种梅子酒以及清酒完全喝不来,酒精味令我难以下咽,然而这瓶国产白兰地度数虽然高达40度,口感却格外清爽。开瓶后可以闻到葡萄酒特有的醇类和有机酸混合而成的味道,入口后会首先品尝到单宁多酚的苦味,随后混合着未蒸馏完全的单糖的甜味,酒体流过喉咙,乙醇的辛辣随之涌进鼻腔,然而却不似中国白酒那样猛烈,这种恰到好处的温柔让人回想起高中时代前桌女生的巴掌和脸庞接触后那种微微涨红的感觉,然后酒体穿肠过肚,一股暖流涌来,寒夜里升腾而起的火焰,窜到夜空里攸尔消失不见。
\\n饮酒提示: 适合独饮,若用于朋友聚会,可与果味饮料一同调制饮用,目前试过的最佳选择有水溶C100、果粒橙、柠檬味雪碧。\\n记录日期:2019-10-11

\\n"},"14":{"id":14,"date":"2018/05/11","author":"Yidadaa","title":"另一个我","mdContent":"> 我知道你的痛苦,所以请你尊重我选择快乐的权力。\\r\\n\\r\\n每隔一段时间,我就感觉自己的脑中好像被什么东西堵住了一样,觉得自己活得很不透彻。\\r\\n\\r\\n具体点说,就是注意力与敏感度全面下降,时常陷入呆滞,时常情绪低落,时常喜怒无常,对任何事情都提不起兴趣,遑论正常学习与工作。尽管这些描述与“抑郁症”患者的表现如此相似,我也不同意给自己贴上“抑郁症”的标签,一是由于现在这个标签基本被广泛滥用,二是这样会给自己造成心理暗示,加重负面情绪。\\r\\n\\r\\n我将其称为另一个我,我的对立面:他。\\r\\n\\r\\n他沉静、偏执,并多愁善感。他日常表现高冷,一切常人津津乐道之事,统统勾不起他的半点兴趣,这令他十分格格不入。他脾气古怪,在任何时候都可能会陷入呆滞,对外界刺激响应度降低,同时社交欲望大幅下降。他不喜欢做任何有意义的事情,他甚至否定意义本身,有时候他认为,生命是宇宙间最可笑的笑话,因为一切终归热寂,一切都将毫无意义,包括意义本身。所以,他还很中二。\\r\\n\\r\\n他的到来悄无声息,可能是在正午,也可能是在半夜。我不喜欢他,他在时我什么事也做不了,但我对他的到来束手无策。并且,他何时回来,会待到何时,我统统无从得知。\\r\\n\\r\\n所以这一次我采取的对策,是躺在床上什么都不做。这让我想起了实习的日子,这段日子之所以令我无法忘怀,是因为彼时他几乎时时刻刻和我形影不离,我躺在床上的时候,仿佛回到了那段日子里。我躺在床上,什么也不做,什么也做不了,没法写代码,没法做毕设,没法与喜欢的妹子聊天,没法与朋友开黑打游戏,真是糟糕透了。\\r\\n\\r\\n我想和他聊聊,他到底是谁,在想些什么,想让我做些什么,但这似乎不可能,我与他几乎无法交流。在某种程度上,我就是他,他就是我,当我是我时,他就不是我,当他是我时,我就不是我。我们无法同时存在,也无从谈起平等交流。\\r\\n\\r\\n因此,我趁我还是我时,写下这篇文章,作为一个媒介,希望当他来时可以看到,并知道我的真实想法,也让我知道他的真实想法。","content":"
\\n

我知道你的痛苦,所以请你尊重我选择快乐的权力。

\\n
\\n

每隔一段时间,我就感觉自己的脑中好像被什么东西堵住了一样,觉得自己活得很不透彻。

\\n

具体点说,就是注意力与敏感度全面下降,时常陷入呆滞,时常情绪低落,时常喜怒无常,对任何事情都提不起兴趣,遑论正常学习与工作。尽管这些描述与“抑郁症”患者的表现如此相似,我也不同意给自己贴上“抑郁症”的标签,一是由于现在这个标签基本被广泛滥用,二是这样会给自己造成心理暗示,加重负面情绪。

\\n

我将其称为另一个我,我的对立面:他。

\\n

他沉静、偏执,并多愁善感。他日常表现高冷,一切常人津津乐道之事,统统勾不起他的半点兴趣,这令他十分格格不入。他脾气古怪,在任何时候都可能会陷入呆滞,对外界刺激响应度降低,同时社交欲望大幅下降。他不喜欢做任何有意义的事情,他甚至否定意义本身,有时候他认为,生命是宇宙间最可笑的笑话,因为一切终归热寂,一切都将毫无意义,包括意义本身。所以,他还很中二。

\\n

他的到来悄无声息,可能是在正午,也可能是在半夜。我不喜欢他,他在时我什么事也做不了,但我对他的到来束手无策。并且,他何时回来,会待到何时,我统统无从得知。

\\n

所以这一次我采取的对策,是躺在床上什么都不做。这让我想起了实习的日子,这段日子之所以令我无法忘怀,是因为彼时他几乎时时刻刻和我形影不离,我躺在床上的时候,仿佛回到了那段日子里。我躺在床上,什么也不做,什么也做不了,没法写代码,没法做毕设,没法与喜欢的妹子聊天,没法与朋友开黑打游戏,真是糟糕透了。

\\n

我想和他聊聊,他到底是谁,在想些什么,想让我做些什么,但这似乎不可能,我与他几乎无法交流。在某种程度上,我就是他,他就是我,当我是我时,他就不是我,当他是我时,我就不是我。我们无法同时存在,也无从谈起平等交流。

\\n

因此,我趁我还是我时,写下这篇文章,作为一个媒介,希望当他来时可以看到,并知道我的真实想法,也让我知道他的真实想法。

\\n"},"15":{"id":15,"date":"2018/08/07","author":"Yidadaa","title":"某算法竞赛初试题解 - 3K问题","mdContent":"### 题目\\r\\n给定包含N个正整数的**非递减**数组A,假设该数组以以下形式保存了某个正整数K的值,即:\\r\\n$$K = \\\\sum_{i=0}^N 2^{A[i]}$$\\r\\n\\r\\n例如给定$A=[1,4,5]$,则$K=2^1+2^4+2^5=50$。\\r\\n\\r\\n则给出一个算法,计算出$3K$的二进制表示中为`1`的比特个数。\\r\\n例如$3K=150=10010110_2$,程序返回值为`4`。\\r\\n\\r\\n**要求**:时间复杂度控制在$O(N)$;空间复杂度控制在$O(1)$。\\r\\n\\r\\n### 题解\\r\\n对于二进制的题,一般都要从二进制本质出发,从数组A的定义可以得到:\\r\\n\\r\\n$$3K=3\\\\sum^N_{i=0}2^{A[i]}=(2^0+2^1)\\\\sum^N_{i=0}2^{A[i]}=\\\\sum^N_{i=0}2^{A[i]}+\\\\sum^N_{i=0}2^{A[i+1]}$$\\r\\n\\r\\n然后我们对数组中的每一位都加上1,然后合并回到数组A中,得到新的数组B,那么问题就转化为计算数组B代表的K的二进制中比特`1`的个数,为了表述方便,我们下文依然用数组A来举例表述。\\r\\n\\r\\n让我们再次回到二进制的基本概念上来,对于一个二进制数:\\r\\n$$110_2=0\\\\times 2^0 + 1\\\\times 2^1 + 1\\\\times 2^2$$\\r\\n\\r\\n那么数组A所代表的数字就变成了:\\r\\n$$K=2^1+2^4+2^5=1\\\\times2^1+0\\\\times2^2+0\\\\times2^3+1\\\\times2^4+1\\\\times2^5=11001_2$$\\r\\n\\r\\n也就是说,数组A中的每一位数字`A[i]`,都代表了K的二进制中低`A[i]`位的比特位为`1`。\\r\\n\\r\\n于是问题就简化为,将数组B表示的每个比特位加起来,最后的结果中`1`的个数,就是我们想要的结果。\\r\\n\\r\\n代码实现如下:\\r\\n```python\\r\\ndef solution(A):\\r\\n bits = {} # 使用哈希表来存储每一个比特位,可以节省空间\\r\\n # 如果键值k存在于bits中,那么代表低k位为1\\r\\n for i in A:\\r\\n for j in range(i, i + 2):\\r\\n t = j\\r\\n while t in bits:\\r\\n del bits[t] // 逐比特执行加法\\r\\n t += 1\\r\\n bits[t] = 1\\r\\n # 最后输出bits中的键值个数即可\\r\\n return len(bits.keys())\\r\\n```\\r\\n\\r\\n### 题外话\\r\\n然而,这道题是我在比赛结束一个小时之后才解出来的,认清了自己是菜鸡的事实。","content":"

题目

\\n

给定包含N个正整数的非递减数组A,假设该数组以以下形式保存了某个正整数K的值,即:

\\n

K=i=0N2A[i]K = \\\\sum_{i=0}^N 2^{A[i]}\\nK=i=0N2A[i]

\\n

例如给定A=[1,4,5]A=[1,4,5]A=[1,4,5],则K=21+24+25=50K=2^1+2^4+2^5=50K=21+24+25=50

\\n

则给出一个算法,计算出3K3K3K的二进制表示中为1的比特个数。\\n例如3K=150=1001011023K=150=10010110_23K=150=100101102,程序返回值为4

\\n

要求:时间复杂度控制在O(N)O(N)O(N);空间复杂度控制在O(1)O(1)O(1)

\\n

题解

\\n

对于二进制的题,一般都要从二进制本质出发,从数组A的定义可以得到:

\\n

3K=3i=0N2A[i]=(20+21)i=0N2A[i]=i=0N2A[i]+i=0N2A[i+1]3K=3\\\\sum^N_{i=0}2^{A[i]}=(2^0+2^1)\\\\sum^N_{i=0}2^{A[i]}=\\\\sum^N_{i=0}2^{A[i]}+\\\\sum^N_{i=0}2^{A[i+1]}\\n3K=3i=0N2A[i]=(20+21)i=0N2A[i]=i=0N2A[i]+i=0N2A[i+1]

\\n

然后我们对数组中的每一位都加上1,然后合并回到数组A中,得到新的数组B,那么问题就转化为计算数组B代表的K的二进制中比特1的个数,为了表述方便,我们下文依然用数组A来举例表述。

\\n

让我们再次回到二进制的基本概念上来,对于一个二进制数:

\\n

1102=0×20+1×21+1×22110_2=0\\\\times 2^0 + 1\\\\times 2^1 + 1\\\\times 2^2\\n1102=0×20+1×21+1×22

\\n

那么数组A所代表的数字就变成了:

\\n

K=21+24+25=1×21+0×22+0×23+1×24+1×25=110012K=2^1+2^4+2^5=1\\\\times2^1+0\\\\times2^2+0\\\\times2^3+1\\\\times2^4+1\\\\times2^5=11001_2\\nK=21+24+25=1×21+0×22+0×23+1×24+1×25=110012

\\n

也就是说,数组A中的每一位数字A[i],都代表了K的二进制中低A[i]位的比特位为1

\\n

于是问题就简化为,将数组B表示的每个比特位加起来,最后的结果中1的个数,就是我们想要的结果。

\\n

代码实现如下:

\\n
def solution(A):\\n    bits = {} # 使用哈希表来存储每一个比特位,可以节省空间\\n    # 如果键值k存在于bits中,那么代表低k位为1\\n    for i in A:\\n        for j in range(i, i + 2):\\n            t = j\\n            while t in bits:\\n                del bits[t] // 逐比特执行加法\\n                t += 1\\n            bits[t] = 1\\n    # 最后输出bits中的键值个数即可\\n    return len(bits.keys())\\n
\\n

题外话

\\n

然而,这道题是我在比赛结束一个小时之后才解出来的,认清了自己是菜鸡的事实。

\\n"},"16":{"id":16,"date":"2018/08/09","author":"Yidadaa","title":"LintCode 困难题赏 - 103.寻找带环链表入口","mdContent":"### 题目\\r\\n给定一个链表,如果链表中存在环,则返回到链表中环的起始节点,如果没有环,返回null。\\r\\n\\r\\n**要求**:不使用额外空间。 \\r\\n**例子**:带环链表`1->5->3->4->6->(index-2)`,返回值为`index-2`对应的节点,即值为`5`的那个节点。\\r\\n\\r\\n### 题解\\r\\n这道题可以说是链表题目中的经典题目了。解这道题之前,还有道题(LintCode No.102)是判断一个链表是否有环,使用了快慢指针的方法,具体思路是使用两根指针以不同的速度遍历链表,快指针一次走两个节点,慢指针一次走一个节点,如果两根指针中途相遇了,那么这个链表就是有环的。\\r\\n\\r\\n本题的思路依然是使用快慢指针的方法,但前提要先利用快慢指针的特性,找出快慢指针走过的路程、环入口、相遇点之间的数学关系。\\r\\n\\r\\n设起点到入口走了`x`步,入口到两根指针第一次相遇处走了`y`步,环的长度为`c`,则快慢指针第一次相遇时,快指针走的距离是慢指针的两倍,即`x+Nc+y=2(x+y)`(其中N为自然数),变换一下得到`x+y=Nc`,于是我们就可以得到入口的索引`x=Nc-y`,这个式子意味着,如果我们用另外两根指针去遍历该链表,其中一根从起点开始走,另外一根从之前快慢指针相遇的地方开始走,并且两根指针每次都只走一步,那么当第一根指针走了`x`步到达环入口时,恰好等于另一根指针从相遇点出发然后绕环N圈后到达环入口,即两根指针在环的入口处相遇。\\r\\n\\r\\n![default](https://user-images.githubusercontent.com/16968934/43889515-f8fdcf44-9bf6-11e8-83a0-53b50e8bb635.jpg)\\r\\n\\r\\n\\r\\n### 代码\\r\\n```python\\r\\n\\"\\"\\"\\r\\nDefinition of ListNode\\r\\nclass ListNode(object):\\r\\n\\r\\n def __init__(self, val, next=None):\\r\\n self.val = val\\r\\n self.next = next\\r\\n\\"\\"\\"\\r\\n\\r\\n\\r\\nclass Solution:\\r\\n \\"\\"\\"\\r\\n @param: head: The first node of linked list.\\r\\n @return: The node where the cycle begins. if there is no cycle, return null\\r\\n \\"\\"\\"\\r\\n def detectCycle(self, head):\\r\\n slow = head\\r\\n fast = head\\r\\n meet = None\\r\\n while fast and fast.next:\\r\\n slow = slow.next\\r\\n fast = fast.next.next\\r\\n if slow == fast:\\r\\n meet = fast\\r\\n break\\r\\n if not fast or not fast.next:\\r\\n return None\\r\\n slow = head\\r\\n while slow != meet:\\r\\n slow = slow.next\\r\\n meet = meet.next\\r\\n return slow\\r\\n```","content":"

题目

\\n

给定一个链表,如果链表中存在环,则返回到链表中环的起始节点,如果没有环,返回null。

\\n

要求:不使用额外空间。
\\n例子:带环链表1->5->3->4->6->(index-2),返回值为index-2对应的节点,即值为5的那个节点。

\\n

题解

\\n

这道题可以说是链表题目中的经典题目了。解这道题之前,还有道题(LintCode No.102)是判断一个链表是否有环,使用了快慢指针的方法,具体思路是使用两根指针以不同的速度遍历链表,快指针一次走两个节点,慢指针一次走一个节点,如果两根指针中途相遇了,那么这个链表就是有环的。

\\n

本题的思路依然是使用快慢指针的方法,但前提要先利用快慢指针的特性,找出快慢指针走过的路程、环入口、相遇点之间的数学关系。

\\n

设起点到入口走了x步,入口到两根指针第一次相遇处走了y步,环的长度为c,则快慢指针第一次相遇时,快指针走的距离是慢指针的两倍,即x+Nc+y=2(x+y)(其中N为自然数),变换一下得到x+y=Nc,于是我们就可以得到入口的索引x=Nc-y,这个式子意味着,如果我们用另外两根指针去遍历该链表,其中一根从起点开始走,另外一根从之前快慢指针相遇的地方开始走,并且两根指针每次都只走一步,那么当第一根指针走了x步到达环入口时,恰好等于另一根指针从相遇点出发然后绕环N圈后到达环入口,即两根指针在环的入口处相遇。

\\n

\\"default\\"

\\n

代码

\\n
"""\\nDefinition of ListNode\\nclass ListNode(object):\\n\\n    def __init__(self, val, next=None):\\n        self.val = val\\n        self.next = next\\n"""\\n\\n\\nclass Solution:\\n    """\\n    @param: head: The first node of linked list.\\n    @return: The node where the cycle begins. if there is no cycle, return null\\n    """\\n    def detectCycle(self, head):\\n        slow = head\\n        fast = head\\n        meet = None\\n        while fast and fast.next:\\n            slow = slow.next\\n            fast = fast.next.next\\n            if slow == fast:\\n                meet = fast\\n                break\\n        if not fast or not fast.next:\\n            return None\\n        slow = head\\n        while slow != meet:\\n            slow = slow.next\\n            meet = meet.next\\n        return slow\\n
\\n"},"17":{"id":17,"date":"2018/12/20","author":"Yidadaa","title":"2018,山与水的分界线","mdContent":"> **No Fear In My Heart(节选) - 朴树** \\r\\n你也曾经追问,然后沉默 \\r\\n渐渐习惯谎言,并以此为荣 \\r\\n因为没有草原,就忘了你是马 \\r\\n你卑微的人生,从不曾犯错的,无聊的人生 \\r\\n\\r\\n### 前言\\r\\n2018 年发生了很多注定写进历史书的大事,霍金、金庸、李咏这些耳熟能详的名人逝世的消息,无不昭示着某段旧时代的淡去。对于我而言,2018 年发生的大事莫过于毕业,本科最后的日子里与同学重温了青春最后的激情,毕业晚会上一曲合唱过后,生活泛起的最后一朵浪花消失在海面上,以后的日子仿佛便会一直沉浸在鱼入大海的逍遥自在和平淡无奇中,此时回味起那时的念头,觉得这预言怕是应验了。\\r\\n\\r\\n本文拖更了大半年,文章的标题在 18 年 12 月份就已经拟好,那时我踌躇满志,心里满怀期待,而时至今日动笔,这标题让我感慨万千。\\r\\n\\r\\n*注:本文全长 9861 字,读完预计耗时 10 分钟。*\\r\\n\\r\\n### 摘要\\r\\n简短地回顾 2018,前半年毕业在即,工作重心基本在毕业设计和毕业旅行的筹划上,全年更加深入地参与到小白健康的开发工作中去,主导了两个重要页面的协调和开发;而后毕业季来袭,匆匆赶完毕业答辩后,和大晗哥他们筹备了毕业晚会,虽然排面不大,但大家的热情都很高涨,很有热血的感觉。\\r\\n\\r\\n毕业晚会后,和党员、峙龙、陶菊以及狗哥去川西自驾,好好玩了一周,算是为本科时光划上了一个句号。下半年研究生生活开始,除了陶菊去华为工作之外,几个好友都重新回到课堂,拿起课本学习的时光无比充实,但焦虑也无处不在,大家都纷纷发了论文,而自己仍在工程和科研之间摇摆不定,这难题始终难以解决。\\r\\n\\r\\n2018 年至少是满怀希望的一年,我怀着对她的期望和热爱生活着,尽管没有取得什么璀璨的成绩,但总体上来说自己充满干劲,年中暑假我冒昧地邀请她去看五月天的演唱会,不出所料地遭到了拒绝,没有做好前期调研和预热的空想主义果然还是行不通啊,以后要好好吸取这个宝贵的教训才好。\\r\\n\\r\\n身体状况在暑假的时候迎来了最坏的时期,8 月份颈椎疼痛不已,甚至只能躺在床上写代码,幸好随后使用了各种手段,勉强让颈椎恢复了工作,一直到年末都没有遇到什么大问题,健康问题依然不能忽视。\\r\\n\\r\\n本文依然会延续去年的设定,从财务、编程、学习、品格、感情、健康这几个方面对 2018 做出总结,并对未来做出展望。\\r\\n\\r\\n\\r\\n### 财务\\r\\n\\r\\n我从去年的四月份开始,养成了记账的习惯,所以 2018 年全年的收支明细都一清二楚,去年的年终总结我很详细地列出了收支明细,现在觉得有隐私泄露的风险,而且说实话相对于已经工作的同学来说,这个数值也没有什么参考价值,所以今年就不给出详细数据了。\\r\\n\\r\\n先说收入方面,相对于 2017 年,2018 年的收入基本翻了一番,因为大四下学期时间比较充裕,我一直都在为毕业旅行攒钱,所以就花了很多时间和精力在小白健康的开发上,从记账软件中也可以看出全年有60%的收入都来自于小白健康的远程兼职,占比上与去年持平,但金额上翻了大概一倍,另外有17%的收入来自研究生的奖助学金,相当于把交的学费又给还回来了,这个其实没什么好说的,除此之外,也会零星地接一些小项目,剩下的收入基本都产生于此。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63689961-017ce280-c83e-11e9-92b1-a7711523d85b.png)\\r\\n\\r\\n然后说一下支出,当习惯了做外包恰烂钱之后,兜里有了点积蓄,但我又没有存钱的习惯,所以花钱一直大手大脚,2018 年有 35% 的钱都花在了数码设备上,比餐饮支出(23.8%)还要多,期间买了一台 matebook x 笔记本、一台索尼的 a6300 微单以及一个定焦镜头,手机从小米 6X 到红米 note5 再一路换到小米 8,也产生了不少花销,年底购入任天堂 switch 游戏机以及若干游戏,然后其他体脂秤、显示器拍立得之类的小玩意儿不一而足,买来基本属于吃灰状态,都算作是玩具。\\r\\n\\r\\n之后是电影话剧以及旅游等娱乐支出,年初的时候受陶菊邀请,和狗哥一起在云南自驾游了一番,玩得很爽;暑假时候五月天的演唱会门票花了些钱,然后毕业旅行以及暑假过渡期租了两个月的房子。在学校的时候在电影上花了很多钱,2018 年上映的 7 分以上的电影基本都去电影院贡献了票房。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63689498-b7dfc800-c83c-11e9-87ef-b9621ea455f2.png)\\r\\n\\r\\n现在想一想,用宝贵的大学时光去做外包挣小钱其实是一件得不偿失的事情,如果能把这些时间精力用来做比赛或者搞科研,其收益肯定要比做外包更高,而且出去实习工资要比自己接活更稳定,还能找工作时作为加分项。**如果有大学生在读这篇文章,我强烈建议你把时间投入到比赛 / 科研 / 学习 / 实习上去,学生时代挣的这点小钱跟学到的知识比实在是微不足道。**\\r\\n\\r\\n**2018年目标完成情况:**\\r\\n- [x] 攒够自己和妹妹毕业旅行的钱;\\r\\n- [x] 保证自给自足。\\r\\n\\r\\n其实第一项只完成了前半部分,妹妹第一年高考因成绩不理想而复读了,而且我也没能攒够两个人出去玩的钱,没有兑现承诺,内心有些愧疚。\\r\\n\\r\\n**对于2019的展望:**\\r\\n- [ ] 自给自足到出去实习。\\r\\n\\r\\n我认为没必要在学校里攒很多钱,如果能找到实习,一直到工作都不必担心钱的问题,所以 2019 最要紧的就是收缩自己的物欲,控制支出。\\r\\n\\r\\n\\r\\n### 编程\\r\\n![image](https://user-images.githubusercontent.com/16968934/63692006-4ce5bf80-c843-11e9-8d5c-a18034947773.png)\\r\\n\\r\\n我是 Github 的忠实用户,所有的代码都会托管到这个网站上,所以 Github 的频率图可以在很大程度上代表编程频率,从提交 Commit 的频率上来看,相比去年提升了 130%,与去年同期相比有了长足的进步,这是一个很好的迹象,证明我对编程的热爱有增无减,并且我确实认为这种热爱会一直保持下去。\\r\\n\\r\\n但值得一提的是,这里面有相当一部分的提交都是为了开发小白健康,也就是为了远程兼职恰饭,纯粹的自己的项目提交应该只占了 50% 左右。\\r\\n\\r\\n稍微盘点一下 2018 年写过的个人项目吧,先说说勉强算是成品的,第一个是能在浏览器里玩NES游戏的模拟器,可以在[此处体验](http://blog.simplenaive.cn/Web-NES/dist/index.html)。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63693445-af8c8a80-c846-11e9-876d-5be465952727.png)\\r\\n\\r\\n然后是自己写的博客框架,完全依赖 Github 的 Issue 功能,一键部署,响应式设计,兼容 PC / 手机 / 平板,不出意料的话,你看到的这篇文章就是通过这个项目展现的,可以在此处体验:[Yidadaa的个人博客](https://blog.simplenaive.cn/)。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63693772-802a4d80-c847-11e9-8861-e15cdd1b618e.png)\\r\\n\\r\\n再之就是和一个 18 级的学弟一起搞的小程序,相当于把 2017 年挖的坑给填了一下,顺便带学弟体验一下软件开发的流程,由于学校不允许这类小程序出现,所以没有正式上线,属于自用阶段,开源地址:[成电Life小程序](https://github.com/UESTC-Miniapp/UESTC-Miniapp-FrontEnd)。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63694091-3d1caa00-c848-11e9-8651-2afa4681bddb.png)\\r\\n\\r\\n然后罗列几个挖了坑没填上的项目,首先是分布式系统的课设,一个 P2P 的聊天程序,使用 Flutter 开发,由于在界面上花了太多功夫,导致没有按时完成,这门课最后也被我放弃了,项目自然也不了了之:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63694849-edd77900-c849-11e9-8d4a-ae3126cb2c09.png)\\r\\n\\r\\n然后是一个简易投票软件以及时间记录程序,前者本来是在毕业季的时候发放问卷用的,结果时间太紧没有完成,搞了界面就没有继续再做了;后者是我挖的另外一个惊天巨坑,还有另外两个相关的项目在计划中,以后会慢慢填上。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63695075-71916580-c84a-11e9-95a4-f8c832f52812.png)\\r\\n\\r\\n以上就是 2018 年的个人项目汇总,突然发现还挺多的,2018 过得还挺充实的嘛。\\r\\n\\r\\n**2018 目标完成情况**\\r\\n- [x] Coding 平均频度不低于 2 / 天,每周 Coding 记录不少于 5 天;\\r\\n- [ ] 全年频度向 1000 进发;\\r\\n- [x] 4月份之前将教务系统插件完善放出,组织新生力量投入开发;\\r\\n- [ ] lintCode 题库两遍以上。\\r\\n\\r\\n第二个目标没有完成,有点意外,可能是因为中间放假的时候太久没写代码了,希望2019能达成这个目标;第四个目标就纯粹是自己太懒了,三天打鱼两天筛网,2019 年应该拿出充足的时间进行算法训练,方便以后找工作。\\r\\n\\r\\n**对 2019 的展望**\\r\\n- [ ] 平均频度不小于 2 次/天,全年频度向 1000 次进发;\\r\\n- [ ] 完成至少 4 个个人项目;\\r\\n- [ ] 算法训练平均时间不少于 2h / 周。\\r\\n\\r\\n\\r\\n### 学习\\r\\n2018 年的成绩总体来说还算可以,表现要比本科平均水平强些,上半年的毕业设计期末总评拿到了 92 分,主要是工作量到位了,其实并没有多少自己的东西,对比同期的大佬同学,自己的工作实在是不够看。\\r\\n\\r\\n下半年研究生入学,几门学位课学的还算可以,研究生阶段的课程确实要比本科更偏计算机理论一些,收获比较大的几门课当数有限自动机、算法设计与分析以及高级计算机系统结构。高系这门课我自认为学的很透彻了,然而最后期末考试还是扑街了,回想起来应该还是有个知识点没有记牢,导致丢了很多分,但这门课依然十分重要,弥补了本科阶段知识的空白,把编译器到硬件这段之间的空白填补上了。\\r\\n\\r\\n然后就不得不说一说科研能力,2018 年结束了,自己的科研产出依然是零,说到底还是自己不上心,身边的同学都能稳稳地坐在实验室搬砖,而我总是自视甚高,不肯听导师的安排,导致科研迟迟没有进展,这实在是急需改进的地方,我希望剩下的两年能沉得住气,把基础的东西沉淀下来,这样以后才能走得更远。\\r\\n\\r\\n**2018 目标完成情况**\\r\\n- [ ] 系统地锻炼科研能力;\\r\\n- [ ] 目标是产出一篇较高水平的论文;\\r\\n- [ ] 保证每月的英语锻炼,包括词汇量以及文献阅读等。\\r\\n\\r\\n写到这里感觉自己脸上火辣辣得疼,浪潮退去之后,原来自己才是那个没穿裤衩裸泳的人……不过胜不骄,败不馁,打起精神来总能走出一片路来,继续向前走吧。\\r\\n\\r\\n**2019 的展望**\\r\\n- [ ] 在至少两场竞赛中夺得名次;\\r\\n- [ ] 完成至少一篇论文的写作。\\r\\n\\r\\n从今以后,论文和比赛就是我的命。\\r\\n\\r\\n\\r\\n### 品格\\r\\n对于自己性格中的优点和缺点,自己在去年的总结中已经有所提及,我认为 2018 年,我的品格上面没有出现特别大的问题,经历了毕业以后,自己越来越珍惜身边的朋友,他们身上的闪光点就是引导我前进的梯度,三人行必有我师,此话情真意切。\\r\\n\\r\\n但缺点仍不容忽视。我认为自己目前严肃有余而认真不足,表面上是在一本正经的做事,但只要稍稍仔细观察下,就能看出我总是做足了表面功夫,看见什么有趣的东西都想去试一试,然后却四处发力,拳头全打在了空气上。人类社会发展至今,已经不再需要亚里士多德或者达芬奇这样的博学者,而是更需要在某个细分领域专精的博士,更何况现在自己还只是个小硕,如果不能在某个方向立稳脚跟,我想这个研究生必定会读的很失败。\\r\\n\\r\\n**2019 的展望**\\r\\n- [ ] Focus on one thing at any time.\\r\\n\\r\\n### 情感\\r\\n个人情感大体上可以分成亲情、友情和爱情。去年这段是留空的,我当时觉得把这么私密的、敏感的情感公开出来,心理上会感到羞耻从而难以启齿,这也跟自己接受的教育有关,总认为沉静、含蓄和内敛才是好的。\\r\\n\\r\\n但我现在觉得,适当地显露自己的情绪也是好的,人始终是群居动物,本性里渴望交流,渴望寻求认同,所以必然会有分享欲,人类的喜乐或许并不相通,但身边的人多多能感受到一二分自己的真实心声,向身边的人袒露心扉,并不失为一种坦荡,所以今年我会花很大的篇幅来写一写这些事情。\\r\\n\\r\\n从家庭关系上来看,我这个儿子当得还算可以,到目前为止,我的表现已经大大超出了我父母的期待,虽然放到其他社会阶层去看,我只能算是勉强达到了及格线,但在农民段位里,我父母绝对能在养儿子这一项任务上拿到 SS 的评价了。\\r\\n\\r\\n然而,农民家庭出个知识分子,有喜也有忧。农村人没有特别高的追求,不会像城市里一些控制欲很强的父母那样要求子女一直往上走,他们抱着一种很朴实的心态,觉得读书学到了知识,够养家糊口娶媳妇就好,尤其是我母亲这样的农村妇女,能抱上孙子孙女已经算是人生一大乐事了。所以我这样读了大学的人,在家族里显得格格不入,这种格格不入从小便显山露水,我父亲一度指着鼻子骂我,说我从小受了亲戚邻居的恩惠,长大了却连漂亮话都说不囫囵,简直白眼狼一样,这时母亲往往还会附上我的若干堂兄弟在酒桌上挥斥方遒的英勇事例,我只能哑口无言,退避三舍。\\r\\n\\r\\n所以在亲情上,我表现得有些冷漠,如果有人能在时间尺度上纵览我的过去,他应该并不会对这种冷漠感到惊讶,他会打趣道:嗐,这浑小子小时候读说明书都能读得津津有味,您就别指望这死理性派长大后能八面玲珑巧舌如簧啦!\\r\\n\\r\\n这种死理性派带来的不良体验,不仅亲人们能感受出来,我的朋友们也能感受到。我是一个技术狂热者,认为编程可以解决生活中遇到的任何问题,甚至在跟喜欢的妹子聊天时都在想着能不能从聊天记录中挖掘出什么有用的信息来,后来的确也建了一个某人的信息库,但显然这种用直男思维解决情感问题的行为,跟用键盘控制挖掘机做菜一样荒诞。\\r\\n\\r\\n我能感受到朋友们对我的评价,他们或许会说我敲代码很熟练,这也会一点,那也会一点,但绝不会说我多么善解人意,或者会与人相处地很舒服,但现在总归要比刚上大学时强了很多,那时我似乎总是在别人的底线上走来走去,室友们能包容我到现在还没有动手,就已经非常值得在心底奏出一段土耳其进行曲了。\\r\\n\\r\\n我是个不解风情的人,而且懦弱,加上瘦弱的体型,看上去没有什么男子气概。这些都是客观存在的特点,并在一次深夜会酒时从异性那里得到了验证。不得不说,我很羡慕那些情商高异性缘也好的人,他们总是能在正确的时间说出合适的话,活跃气氛的同时收获一片好感,我就做不来,而且越怕越抵触,越抵触越得不到锻炼,那大脑里的一些神经元就永远沉睡。\\r\\n\\r\\n这种特质当然来源于我品格里死理性派的那部分,凡事都讲究个道理和缘由,哪怕是不讲道理的事情也试图去讲讲道理,自然讨不到什么好处。我的感情经历十分贫乏,没有什么人教过我怎么跟异性相处,而且这也不是什么自学就能解决的事情,若抱着自学的目的和很多女生接触,整个人就会陷进蛛网里,想想就觉得难以接受。\\r\\n\\r\\n既然决定了写这一部分,就力求搞清楚问题所在,然后对症下药,所以我就索性多写些过去的事情。\\r\\n\\r\\n我真正的情感萌芽生长于高考完的暑假,和女孩约定一起去青岛看海,那是一段刻骨铭心的经历,但不知怎么我就搞砸了,其实这也没什么,没有人不加练习就能在考试中取得好成绩,和女生相处也应该是这样,如果能摆正心态,事情总能不至于无法挽回,但那时我脑子里与异性打交道的回路似乎还没有就绪,觉得搞砸了就是搞砸了,然后就一砸到底。\\r\\n\\r\\n当时我的眼里没有海全是她,相机里一千多张照片里有九百张她的游客照,含她率高达90%,如果拿这些照片做训练集训个二分类器出来,绝对称得上是个极端不均衡样本问题。\\r\\n\\r\\n欢乐的时光很是短暂,在青岛玩了两三天就坐上了返程的火车,那时我们是真正的穷酸书生,只能坐二十多小时的硬座回家,夜深时,她靠在我的肩膀上睡着了,车窗外辽阔的华北平原灯火阑珊,而我的心里却是锣鼓喧天鞭炮齐鸣,头顶似乎正有一千颗黑洞盘旋,花上好一阵才能平静下来,我觉得时机到了,是时候升华一下这炽热的革命友谊了,然后便吻了上去。\\r\\n\\r\\n后来我看过很多场恋爱电影,也读过很多爱情故事,但都没能再有过那一秒钟的感受,只有在某个燃烧着绚丽彩云的傍晚,从不知名角落吹来的风偷偷拂过脸颊,我才能偶尔捕捉到那醉人的柔软感觉。\\r\\n\\r\\n但就算极尽辞藻之奢华,也不能令时光倒流,也不能抹去看到她头也不回地下车之后发来的那条短信后的失落感与罪恶感,那条短信简明扼要地说“没想到你是这样的人”,“怎样的人?”,这个问题我想了很多年,我一度将其解读为一种判决,她似乎把我与那些平日里不务正业的地痞流氓划为了一类,而这与我自身的认知产生了激烈的冲突,尽管我小时候顽劣不堪,但从初中开始我就努力地成为一个正直的人。\\r\\n\\r\\n而且我对她的热爱从高二那个有着刺眼阳光的午后便缓缓燃起,从拍下那张她在阳光下吃盒饭的照片时便已颇具规模,而现在再去解读那个瞬间,几乎可以百分之百地将其定义为心动的感觉,而且这种感觉在随后两年如野火般蔓延:刻意养成记错题本的习惯然后借给她,把她拉到围观我做好的班级画报的人群前然后尽量装作平常地说“这是我做的”,在她做物理题的时候偷偷地拍下她认真做题的样子,她和班里男生打篮球的时候我在隔壁场里变成一条酸菜鱼……我把对她的热爱小心翼翼地藏在这些小事后面,并把这些回忆在心中精心雕琢成她的样子,但那天过后,她直接用一条短信在这座雕像后刻上了“大清乾隆年间制”,这让我在做那些充满小心思的小事时,就跟初中时跟在女生后面挤眉弄眼的小混混一样惹人厌烦,然后那些炽烈的热爱瞬间就变为烈火吞噬了所有信心,连之后再去找她辩解的勇气也没能拿出来。\\r\\n\\r\\n以现在的视角去分析那件往事,意义并不大,她之后几年很快结识了新的男孩,男友也换了好几任,这场风波对她影响似乎并不大,我觉得也没有必要再去问她那条短信真正的意图,毕竟过去了好几年,许多事情都应该翻篇了,而当我意识到这一点的时候,时间已经是 2018 年,我决定重振旗鼓告别单身。文章开头说,2018 年我满怀期待,就是这个原因。\\r\\n\\r\\n[ 此处1076字已做加密处理 ]\\r\\n\\r\\n### 健康\\r\\n活着并不是意见容易的事情,尤其是健康地活着。这句话在去年的年终总结中就已经写过,但现在还应该添一句注解,不仅要身体健康,还要心理健康。\\r\\n\\r\\n由于身体状况在年中迎来了断崖式下降,我现在不得不比以前任何时候都更注意自己的健康状况。三月份、八月份和九月份都保持了一定的户外运动量,每周的两三天都要出去跑上两公里,此外颈椎部分的舒缓动作也研究了很多,基本已经能让不堪重负的颈椎更好受一些。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63859433-ed6de800-c9d9-11e9-930d-1eee4d42678e.png)\\r\\n\\r\\n然后便是睡眠情况,上图的数据来源自小米手环,我做了一张图来展示睡眠趋势。其实从睡眠参数上来看,2018 年的睡眠还是蛮正常的,深水比例 20%,平均每天睡觉 8 个小时,简直就是写在百科的睡眠参数,但是入睡时间和起床时间就有点偏离大众了,我可能更偏向于夜猫子型的作息。而且从上图中还能看到一个有趣的现象,那就是冬天的深睡比例和时长都明显要比春夏更长,可能寒冷的天气更加适合人类睡眠吧。\\r\\n\\r\\n然后就是体重,2018 年体重很稳定地保持在 57.7kg 附近,每次称体重都会提示我低于 85% 的同体型人士,看来以后还是要多吃多练,早日达到 65kg 的正常体重。\\r\\n\\r\\n**2018 目标完成情况**\\r\\n- [ ] 坚持跑步的好习惯\\r\\n- [ ] 修正作息至 00:00 - 09:00\\r\\n- [ ] 体重向 65kg 出发\\r\\n\\r\\n轻轻地叹上一口气,希望胃口能早点好起来,这个目标继续留到 2019 年。\\r\\n\\r\\n### 展望2019\\r\\n去年的展望中说到,我是一个非常庸俗的人,今年也不例外,但是随着三观的逐渐完善,很多事情的意义都在发生变化,比如学生时代,金钱对我来说真的那么重要吗?现在觉得未必,读书的时候,我想要追求的东西还是在形而上的层面,我希望 2019 年能把自己的一切都量化起来,只有量化成数据,自己才能更加清醒的活着。\\r\\n\\r\\n所以 2019 最大的愿望,就是**把自己的生活数字化**,然后在拥有基准线的基础上不断进步。这是一个美好的愿望,我希望它能在 2019 结束时变成现实。\\r\\n\\r\\n当然并不是所有事情都能量化,比如情感,它游离在我们身边的空气中,既不能抓住它,又不能任它离开,我能驾驭它吗?可能时间会给我答案。\\r\\n\\r\\n2019 年没有什么大不了的,认真地活着吧。","content":"
\\n

No Fear In My Heart(节选) - 朴树
\\n你也曾经追问,然后沉默
\\n渐渐习惯谎言,并以此为荣
\\n因为没有草原,就忘了你是马
\\n你卑微的人生,从不曾犯错的,无聊的人生

\\n
\\n

前言

\\n

2018 年发生了很多注定写进历史书的大事,霍金、金庸、李咏这些耳熟能详的名人逝世的消息,无不昭示着某段旧时代的淡去。对于我而言,2018 年发生的大事莫过于毕业,本科最后的日子里与同学重温了青春最后的激情,毕业晚会上一曲合唱过后,生活泛起的最后一朵浪花消失在海面上,以后的日子仿佛便会一直沉浸在鱼入大海的逍遥自在和平淡无奇中,此时回味起那时的念头,觉得这预言怕是应验了。

\\n

本文拖更了大半年,文章的标题在 18 年 12 月份就已经拟好,那时我踌躇满志,心里满怀期待,而时至今日动笔,这标题让我感慨万千。

\\n

注:本文全长 9861 字,读完预计耗时 10 分钟。

\\n

摘要

\\n

简短地回顾 2018,前半年毕业在即,工作重心基本在毕业设计和毕业旅行的筹划上,全年更加深入地参与到小白健康的开发工作中去,主导了两个重要页面的协调和开发;而后毕业季来袭,匆匆赶完毕业答辩后,和大晗哥他们筹备了毕业晚会,虽然排面不大,但大家的热情都很高涨,很有热血的感觉。

\\n

毕业晚会后,和党员、峙龙、陶菊以及狗哥去川西自驾,好好玩了一周,算是为本科时光划上了一个句号。下半年研究生生活开始,除了陶菊去华为工作之外,几个好友都重新回到课堂,拿起课本学习的时光无比充实,但焦虑也无处不在,大家都纷纷发了论文,而自己仍在工程和科研之间摇摆不定,这难题始终难以解决。

\\n

2018 年至少是满怀希望的一年,我怀着对她的期望和热爱生活着,尽管没有取得什么璀璨的成绩,但总体上来说自己充满干劲,年中暑假我冒昧地邀请她去看五月天的演唱会,不出所料地遭到了拒绝,没有做好前期调研和预热的空想主义果然还是行不通啊,以后要好好吸取这个宝贵的教训才好。

\\n

身体状况在暑假的时候迎来了最坏的时期,8 月份颈椎疼痛不已,甚至只能躺在床上写代码,幸好随后使用了各种手段,勉强让颈椎恢复了工作,一直到年末都没有遇到什么大问题,健康问题依然不能忽视。

\\n

本文依然会延续去年的设定,从财务、编程、学习、品格、感情、健康这几个方面对 2018 做出总结,并对未来做出展望。

\\n

财务

\\n

我从去年的四月份开始,养成了记账的习惯,所以 2018 年全年的收支明细都一清二楚,去年的年终总结我很详细地列出了收支明细,现在觉得有隐私泄露的风险,而且说实话相对于已经工作的同学来说,这个数值也没有什么参考价值,所以今年就不给出详细数据了。

\\n

先说收入方面,相对于 2017 年,2018 年的收入基本翻了一番,因为大四下学期时间比较充裕,我一直都在为毕业旅行攒钱,所以就花了很多时间和精力在小白健康的开发上,从记账软件中也可以看出全年有60%的收入都来自于小白健康的远程兼职,占比上与去年持平,但金额上翻了大概一倍,另外有17%的收入来自研究生的奖助学金,相当于把交的学费又给还回来了,这个其实没什么好说的,除此之外,也会零星地接一些小项目,剩下的收入基本都产生于此。

\\n

\\"image\\"

\\n

然后说一下支出,当习惯了做外包恰烂钱之后,兜里有了点积蓄,但我又没有存钱的习惯,所以花钱一直大手大脚,2018 年有 35% 的钱都花在了数码设备上,比餐饮支出(23.8%)还要多,期间买了一台 matebook x 笔记本、一台索尼的 a6300 微单以及一个定焦镜头,手机从小米 6X 到红米 note5 再一路换到小米 8,也产生了不少花销,年底购入任天堂 switch 游戏机以及若干游戏,然后其他体脂秤、显示器拍立得之类的小玩意儿不一而足,买来基本属于吃灰状态,都算作是玩具。

\\n

之后是电影话剧以及旅游等娱乐支出,年初的时候受陶菊邀请,和狗哥一起在云南自驾游了一番,玩得很爽;暑假时候五月天的演唱会门票花了些钱,然后毕业旅行以及暑假过渡期租了两个月的房子。在学校的时候在电影上花了很多钱,2018 年上映的 7 分以上的电影基本都去电影院贡献了票房。

\\n

\\"image\\"

\\n

现在想一想,用宝贵的大学时光去做外包挣小钱其实是一件得不偿失的事情,如果能把这些时间精力用来做比赛或者搞科研,其收益肯定要比做外包更高,而且出去实习工资要比自己接活更稳定,还能找工作时作为加分项。如果有大学生在读这篇文章,我强烈建议你把时间投入到比赛 / 科研 / 学习 / 实习上去,学生时代挣的这点小钱跟学到的知识比实在是微不足道。

\\n

2018年目标完成情况:

\\n
    \\n
  • \\n
  • \\n
\\n

其实第一项只完成了前半部分,妹妹第一年高考因成绩不理想而复读了,而且我也没能攒够两个人出去玩的钱,没有兑现承诺,内心有些愧疚。

\\n

对于2019的展望:

\\n
    \\n
  • \\n
\\n

我认为没必要在学校里攒很多钱,如果能找到实习,一直到工作都不必担心钱的问题,所以 2019 最要紧的就是收缩自己的物欲,控制支出。

\\n

编程

\\n

\\"image\\"

\\n

我是 Github 的忠实用户,所有的代码都会托管到这个网站上,所以 Github 的频率图可以在很大程度上代表编程频率,从提交 Commit 的频率上来看,相比去年提升了 130%,与去年同期相比有了长足的进步,这是一个很好的迹象,证明我对编程的热爱有增无减,并且我确实认为这种热爱会一直保持下去。

\\n

但值得一提的是,这里面有相当一部分的提交都是为了开发小白健康,也就是为了远程兼职恰饭,纯粹的自己的项目提交应该只占了 50% 左右。

\\n

稍微盘点一下 2018 年写过的个人项目吧,先说说勉强算是成品的,第一个是能在浏览器里玩NES游戏的模拟器,可以在此处体验

\\n

\\"image\\"

\\n

然后是自己写的博客框架,完全依赖 Github 的 Issue 功能,一键部署,响应式设计,兼容 PC / 手机 / 平板,不出意料的话,你看到的这篇文章就是通过这个项目展现的,可以在此处体验:Yidadaa的个人博客

\\n

\\"image\\"

\\n

再之就是和一个 18 级的学弟一起搞的小程序,相当于把 2017 年挖的坑给填了一下,顺便带学弟体验一下软件开发的流程,由于学校不允许这类小程序出现,所以没有正式上线,属于自用阶段,开源地址:成电Life小程序

\\n

\\"image\\"

\\n

然后罗列几个挖了坑没填上的项目,首先是分布式系统的课设,一个 P2P 的聊天程序,使用 Flutter 开发,由于在界面上花了太多功夫,导致没有按时完成,这门课最后也被我放弃了,项目自然也不了了之:

\\n

\\"image\\"

\\n

然后是一个简易投票软件以及时间记录程序,前者本来是在毕业季的时候发放问卷用的,结果时间太紧没有完成,搞了界面就没有继续再做了;后者是我挖的另外一个惊天巨坑,还有另外两个相关的项目在计划中,以后会慢慢填上。

\\n

\\"image\\"

\\n

以上就是 2018 年的个人项目汇总,突然发现还挺多的,2018 过得还挺充实的嘛。

\\n

2018 目标完成情况

\\n
    \\n
  • \\n
  • \\n
  • \\n
  • \\n
\\n

第二个目标没有完成,有点意外,可能是因为中间放假的时候太久没写代码了,希望2019能达成这个目标;第四个目标就纯粹是自己太懒了,三天打鱼两天筛网,2019 年应该拿出充足的时间进行算法训练,方便以后找工作。

\\n

对 2019 的展望

\\n
    \\n
  • \\n
  • \\n
  • \\n
\\n

学习

\\n

2018 年的成绩总体来说还算可以,表现要比本科平均水平强些,上半年的毕业设计期末总评拿到了 92 分,主要是工作量到位了,其实并没有多少自己的东西,对比同期的大佬同学,自己的工作实在是不够看。

\\n

下半年研究生入学,几门学位课学的还算可以,研究生阶段的课程确实要比本科更偏计算机理论一些,收获比较大的几门课当数有限自动机、算法设计与分析以及高级计算机系统结构。高系这门课我自认为学的很透彻了,然而最后期末考试还是扑街了,回想起来应该还是有个知识点没有记牢,导致丢了很多分,但这门课依然十分重要,弥补了本科阶段知识的空白,把编译器到硬件这段之间的空白填补上了。

\\n

然后就不得不说一说科研能力,2018 年结束了,自己的科研产出依然是零,说到底还是自己不上心,身边的同学都能稳稳地坐在实验室搬砖,而我总是自视甚高,不肯听导师的安排,导致科研迟迟没有进展,这实在是急需改进的地方,我希望剩下的两年能沉得住气,把基础的东西沉淀下来,这样以后才能走得更远。

\\n

2018 目标完成情况

\\n
    \\n
  • \\n
  • \\n
  • \\n
\\n

写到这里感觉自己脸上火辣辣得疼,浪潮退去之后,原来自己才是那个没穿裤衩裸泳的人……不过胜不骄,败不馁,打起精神来总能走出一片路来,继续向前走吧。

\\n

2019 的展望

\\n
    \\n
  • \\n
  • \\n
\\n

从今以后,论文和比赛就是我的命。

\\n

品格

\\n

对于自己性格中的优点和缺点,自己在去年的总结中已经有所提及,我认为 2018 年,我的品格上面没有出现特别大的问题,经历了毕业以后,自己越来越珍惜身边的朋友,他们身上的闪光点就是引导我前进的梯度,三人行必有我师,此话情真意切。

\\n

但缺点仍不容忽视。我认为自己目前严肃有余而认真不足,表面上是在一本正经的做事,但只要稍稍仔细观察下,就能看出我总是做足了表面功夫,看见什么有趣的东西都想去试一试,然后却四处发力,拳头全打在了空气上。人类社会发展至今,已经不再需要亚里士多德或者达芬奇这样的博学者,而是更需要在某个细分领域专精的博士,更何况现在自己还只是个小硕,如果不能在某个方向立稳脚跟,我想这个研究生必定会读的很失败。

\\n

2019 的展望

\\n
    \\n
  • \\n
\\n

情感

\\n

个人情感大体上可以分成亲情、友情和爱情。去年这段是留空的,我当时觉得把这么私密的、敏感的情感公开出来,心理上会感到羞耻从而难以启齿,这也跟自己接受的教育有关,总认为沉静、含蓄和内敛才是好的。

\\n

但我现在觉得,适当地显露自己的情绪也是好的,人始终是群居动物,本性里渴望交流,渴望寻求认同,所以必然会有分享欲,人类的喜乐或许并不相通,但身边的人多多能感受到一二分自己的真实心声,向身边的人袒露心扉,并不失为一种坦荡,所以今年我会花很大的篇幅来写一写这些事情。

\\n

从家庭关系上来看,我这个儿子当得还算可以,到目前为止,我的表现已经大大超出了我父母的期待,虽然放到其他社会阶层去看,我只能算是勉强达到了及格线,但在农民段位里,我父母绝对能在养儿子这一项任务上拿到 SS 的评价了。

\\n

然而,农民家庭出个知识分子,有喜也有忧。农村人没有特别高的追求,不会像城市里一些控制欲很强的父母那样要求子女一直往上走,他们抱着一种很朴实的心态,觉得读书学到了知识,够养家糊口娶媳妇就好,尤其是我母亲这样的农村妇女,能抱上孙子孙女已经算是人生一大乐事了。所以我这样读了大学的人,在家族里显得格格不入,这种格格不入从小便显山露水,我父亲一度指着鼻子骂我,说我从小受了亲戚邻居的恩惠,长大了却连漂亮话都说不囫囵,简直白眼狼一样,这时母亲往往还会附上我的若干堂兄弟在酒桌上挥斥方遒的英勇事例,我只能哑口无言,退避三舍。

\\n

所以在亲情上,我表现得有些冷漠,如果有人能在时间尺度上纵览我的过去,他应该并不会对这种冷漠感到惊讶,他会打趣道:嗐,这浑小子小时候读说明书都能读得津津有味,您就别指望这死理性派长大后能八面玲珑巧舌如簧啦!

\\n

这种死理性派带来的不良体验,不仅亲人们能感受出来,我的朋友们也能感受到。我是一个技术狂热者,认为编程可以解决生活中遇到的任何问题,甚至在跟喜欢的妹子聊天时都在想着能不能从聊天记录中挖掘出什么有用的信息来,后来的确也建了一个某人的信息库,但显然这种用直男思维解决情感问题的行为,跟用键盘控制挖掘机做菜一样荒诞。

\\n

我能感受到朋友们对我的评价,他们或许会说我敲代码很熟练,这也会一点,那也会一点,但绝不会说我多么善解人意,或者会与人相处地很舒服,但现在总归要比刚上大学时强了很多,那时我似乎总是在别人的底线上走来走去,室友们能包容我到现在还没有动手,就已经非常值得在心底奏出一段土耳其进行曲了。

\\n

我是个不解风情的人,而且懦弱,加上瘦弱的体型,看上去没有什么男子气概。这些都是客观存在的特点,并在一次深夜会酒时从异性那里得到了验证。不得不说,我很羡慕那些情商高异性缘也好的人,他们总是能在正确的时间说出合适的话,活跃气氛的同时收获一片好感,我就做不来,而且越怕越抵触,越抵触越得不到锻炼,那大脑里的一些神经元就永远沉睡。

\\n

这种特质当然来源于我品格里死理性派的那部分,凡事都讲究个道理和缘由,哪怕是不讲道理的事情也试图去讲讲道理,自然讨不到什么好处。我的感情经历十分贫乏,没有什么人教过我怎么跟异性相处,而且这也不是什么自学就能解决的事情,若抱着自学的目的和很多女生接触,整个人就会陷进蛛网里,想想就觉得难以接受。

\\n

既然决定了写这一部分,就力求搞清楚问题所在,然后对症下药,所以我就索性多写些过去的事情。

\\n

我真正的情感萌芽生长于高考完的暑假,和女孩约定一起去青岛看海,那是一段刻骨铭心的经历,但不知怎么我就搞砸了,其实这也没什么,没有人不加练习就能在考试中取得好成绩,和女生相处也应该是这样,如果能摆正心态,事情总能不至于无法挽回,但那时我脑子里与异性打交道的回路似乎还没有就绪,觉得搞砸了就是搞砸了,然后就一砸到底。

\\n

当时我的眼里没有海全是她,相机里一千多张照片里有九百张她的游客照,含她率高达90%,如果拿这些照片做训练集训个二分类器出来,绝对称得上是个极端不均衡样本问题。

\\n

欢乐的时光很是短暂,在青岛玩了两三天就坐上了返程的火车,那时我们是真正的穷酸书生,只能坐二十多小时的硬座回家,夜深时,她靠在我的肩膀上睡着了,车窗外辽阔的华北平原灯火阑珊,而我的心里却是锣鼓喧天鞭炮齐鸣,头顶似乎正有一千颗黑洞盘旋,花上好一阵才能平静下来,我觉得时机到了,是时候升华一下这炽热的革命友谊了,然后便吻了上去。

\\n

后来我看过很多场恋爱电影,也读过很多爱情故事,但都没能再有过那一秒钟的感受,只有在某个燃烧着绚丽彩云的傍晚,从不知名角落吹来的风偷偷拂过脸颊,我才能偶尔捕捉到那醉人的柔软感觉。

\\n

但就算极尽辞藻之奢华,也不能令时光倒流,也不能抹去看到她头也不回地下车之后发来的那条短信后的失落感与罪恶感,那条短信简明扼要地说“没想到你是这样的人”,“怎样的人?”,这个问题我想了很多年,我一度将其解读为一种判决,她似乎把我与那些平日里不务正业的地痞流氓划为了一类,而这与我自身的认知产生了激烈的冲突,尽管我小时候顽劣不堪,但从初中开始我就努力地成为一个正直的人。

\\n

而且我对她的热爱从高二那个有着刺眼阳光的午后便缓缓燃起,从拍下那张她在阳光下吃盒饭的照片时便已颇具规模,而现在再去解读那个瞬间,几乎可以百分之百地将其定义为心动的感觉,而且这种感觉在随后两年如野火般蔓延:刻意养成记错题本的习惯然后借给她,把她拉到围观我做好的班级画报的人群前然后尽量装作平常地说“这是我做的”,在她做物理题的时候偷偷地拍下她认真做题的样子,她和班里男生打篮球的时候我在隔壁场里变成一条酸菜鱼……我把对她的热爱小心翼翼地藏在这些小事后面,并把这些回忆在心中精心雕琢成她的样子,但那天过后,她直接用一条短信在这座雕像后刻上了“大清乾隆年间制”,这让我在做那些充满小心思的小事时,就跟初中时跟在女生后面挤眉弄眼的小混混一样惹人厌烦,然后那些炽烈的热爱瞬间就变为烈火吞噬了所有信心,连之后再去找她辩解的勇气也没能拿出来。

\\n

以现在的视角去分析那件往事,意义并不大,她之后几年很快结识了新的男孩,男友也换了好几任,这场风波对她影响似乎并不大,我觉得也没有必要再去问她那条短信真正的意图,毕竟过去了好几年,许多事情都应该翻篇了,而当我意识到这一点的时候,时间已经是 2018 年,我决定重振旗鼓告别单身。文章开头说,2018 年我满怀期待,就是这个原因。

\\n

[ 此处1076字已做加密处理 ]

\\n

健康

\\n

活着并不是意见容易的事情,尤其是健康地活着。这句话在去年的年终总结中就已经写过,但现在还应该添一句注解,不仅要身体健康,还要心理健康。

\\n

由于身体状况在年中迎来了断崖式下降,我现在不得不比以前任何时候都更注意自己的健康状况。三月份、八月份和九月份都保持了一定的户外运动量,每周的两三天都要出去跑上两公里,此外颈椎部分的舒缓动作也研究了很多,基本已经能让不堪重负的颈椎更好受一些。

\\n

\\"image\\"

\\n

然后便是睡眠情况,上图的数据来源自小米手环,我做了一张图来展示睡眠趋势。其实从睡眠参数上来看,2018 年的睡眠还是蛮正常的,深水比例 20%,平均每天睡觉 8 个小时,简直就是写在百科的睡眠参数,但是入睡时间和起床时间就有点偏离大众了,我可能更偏向于夜猫子型的作息。而且从上图中还能看到一个有趣的现象,那就是冬天的深睡比例和时长都明显要比春夏更长,可能寒冷的天气更加适合人类睡眠吧。

\\n

然后就是体重,2018 年体重很稳定地保持在 57.7kg 附近,每次称体重都会提示我低于 85% 的同体型人士,看来以后还是要多吃多练,早日达到 65kg 的正常体重。

\\n

2018 目标完成情况

\\n
    \\n
  • \\n
  • \\n
  • \\n
\\n

轻轻地叹上一口气,希望胃口能早点好起来,这个目标继续留到 2019 年。

\\n

展望2019

\\n

去年的展望中说到,我是一个非常庸俗的人,今年也不例外,但是随着三观的逐渐完善,很多事情的意义都在发生变化,比如学生时代,金钱对我来说真的那么重要吗?现在觉得未必,读书的时候,我想要追求的东西还是在形而上的层面,我希望 2019 年能把自己的一切都量化起来,只有量化成数据,自己才能更加清醒的活着。

\\n

所以 2019 最大的愿望,就是把自己的生活数字化,然后在拥有基准线的基础上不断进步。这是一个美好的愿望,我希望它能在 2019 结束时变成现实。

\\n

当然并不是所有事情都能量化,比如情感,它游离在我们身边的空气中,既不能抓住它,又不能任它离开,我能驾驭它吗?可能时间会给我答案。

\\n

2019 年没有什么大不了的,认真地活着吧。

\\n"},"20":{"id":20,"date":"2019/06/24","author":"Yidadaa","title":"LeetCode困难题赏 - 887.扔鸡蛋","mdContent":"### 题目\\r\\n假设有$K$个鸡蛋和$N$层楼,每个蛋的性质完全相同,而且如果某个蛋已经碎了,就没法再次使用。假如存在楼层$F, F\\\\in[0, n]$,且鸡蛋从任何高于$F$的楼层扔下都会碎掉,但从低于或等于$F$的楼层扔下则不会碎。\\r\\n\\r\\n每次移动,都可以使用一个鸡蛋从某个楼层扔下,如果你想准确地测得$F$的值,那么在最坏的情况下,最少需要移动几次?\\r\\n\\r\\n原题链接:[Leetcode 887](https://leetcode-cn.com/problems/super-egg-drop/)。\\r\\n\\r\\n### 题解\\r\\n刚看到这道题时,很容易一头雾水,不知道题目在说些什么,因为扔鸡蛋的结果我们是无法在做题时实时获得的,比如我们在第$x$层楼扔第$i$个蛋时,是不知道这个蛋是否会碎,而且对于给定楼层总数和鸡蛋总数,我们知道存在楼层$F$会令鸡蛋碎掉,但是我们却并不知道这个数值到底是几。\\r\\n\\r\\n那么这道题到底在说些什么呢?经过分析可以发现,这道题之所以难理解,是因为题目将真正考察的地方隐藏起来了,题目的核心就在于理解最后一句话:**在最坏的情况下,最少移动几次**。这句话意味着,我们在扔鸡蛋时是可以采取多种策略的,每种策略都对应一种最坏情况,比如:\\r\\n1. 采用策略$a$:从第一层逐层向上扔,那么最坏情况是$min(K, N, F)$;\\r\\n2. 采用策略$b$:从顶楼逐层向下扔,那么最坏情况是$min(K, N, F)$,即与第一种策略类似;\\r\\n3. 采用最优策略$\\\\hat{f}$,我们现在可能不知道这个策略具体是什么,但是可以确定移动次数是$x=f(K, N, F)$。\\r\\n\\r\\n针对上面列出的几种情况,不难发现,移动次数$x$是与$K, N, F$有关的函数,即$f(K, N, F)$,那么函数$f$就可以代表我们选择的策略,由于具体的策略我们是不知道的,所以将策略函数$f$也看作一个变量,那么最终我们要求得的$x$表达式可以写作:\\r\\n$$x=g(K, N, F, f)$$\\r\\n到了这一步,我们才算是真正的理解题意了,我们需要找到一个算法$g$,在策略空间$f\\\\in\\\\Psi$中找到最优策略$\\\\hat{f}$,并且针对这种策略$\\\\hat{f}$,找到最坏情况$F\\\\in\\\\[0,N]$对应的$x$。\\r\\n\\r\\n绕了这么久,可以发现,我们遍历所有可能的策略,并且在每个策略中,都假设楼层$F$总是出现在使当前策略$f$移动次数最大的楼层处,题目并没有给出如何遍历楼层,也没有给出$F$的值,这两个值都需要我们在算法$g$中自行遍历。\\r\\n\\r\\n对于这个问题,一个自然的思路就是使用动态规划的思想来处理。我们使用状态表$dp[K][N]$来表示我们拥有$K$个鸡蛋和$N$层楼时的最小移动次数,那么考虑初始情况,拥有$i\\\\in[0,K]$个蛋,$j\\\\in[0,N]$层楼时:\\r\\n1. 若$i=0 or j=0$,毋庸置疑,不需要进行测试,因为没有鸡蛋和楼层,$dp[0][1]=dp[1][0]=dp[0][0]=0$;\\r\\n2. 若$i=1, j\\\\in[1,N]$,此时我们只有一个蛋,那么只能使用前面提到的策略$a$来试探,那么最坏情况就是$F=j$,即蛋碎的楼层在最大楼层处,那么我们最少尝试次数就是$dp[1][j]=j, j\\\\in[1,N]$;\\r\\n3. 若$i\\\\in(1,K], j=1$,我们有不止一个鸡蛋,但是只有一层楼,那么毫无疑问,只需要测试一次就行了,得到$dp[i][1]=1, i\\\\in(1, K)$;\\r\\n4. 若$i\\\\in(1,K], j\\\\in(1,N)$,即有不止一个鸡蛋,且不止一层楼,那么我们就需要使用最优策略$\\\\hat{f}$来确定最小移动次数,\\r\\n\\r\\n写成代码形式:\\r\\n```python\\r\\ndef superEggDrop(self, K: int, N: int) -> int:\\r\\n dp = [[0] * (K + 1) for i in range(N + 1)]\\r\\n for i in range(1, N + 1): dp[i][1] = i\\r\\n for i in range(1, K + 1): dp[1][i] = 1\\r\\n\\r\\n for i in range(2, N + 1):\\r\\n for j in range(2, K + 1):\\r\\n dp[i][j] = dp[i][j - 1]\\r\\n for k in range(1, i + 1):\\r\\n dp[i][j] = min(dp[i][j], 1 + max(dp[k - 1][j - 1], dp[i - k][j]))\\r\\n\\r\\n return dp[N][K]\\r\\n```\\r\\n**算法复杂度:**$O(KN^2)$\\r\\n\\r\\n> 查看带有$\\\\LaTeX$公式渲染的博客内容:[https://blog.simplenaive.cn/#/post/20](https://blog.simplenaive.cn/#/post/20)","content":"

题目

\\n

假设有KKK个鸡蛋和NNN层楼,每个蛋的性质完全相同,而且如果某个蛋已经碎了,就没法再次使用。假如存在楼层F,F[0,n]F, F\\\\in[0, n]F,F[0,n],且鸡蛋从任何高于FFF的楼层扔下都会碎掉,但从低于或等于FFF的楼层扔下则不会碎。

\\n

每次移动,都可以使用一个鸡蛋从某个楼层扔下,如果你想准确地测得FFF的值,那么在最坏的情况下,最少需要移动几次?

\\n

原题链接:Leetcode 887

\\n

题解

\\n

刚看到这道题时,很容易一头雾水,不知道题目在说些什么,因为扔鸡蛋的结果我们是无法在做题时实时获得的,比如我们在第xxx层楼扔第iii个蛋时,是不知道这个蛋是否会碎,而且对于给定楼层总数和鸡蛋总数,我们知道存在楼层FFF会令鸡蛋碎掉,但是我们却并不知道这个数值到底是几。

\\n

那么这道题到底在说些什么呢?经过分析可以发现,这道题之所以难理解,是因为题目将真正考察的地方隐藏起来了,题目的核心就在于理解最后一句话:在最坏的情况下,最少移动几次。这句话意味着,我们在扔鸡蛋时是可以采取多种策略的,每种策略都对应一种最坏情况,比如:

\\n
    \\n
  1. 采用策略aaa:从第一层逐层向上扔,那么最坏情况是min(K,N,F)min(K, N, F)min(K,N,F)
  2. \\n
  3. 采用策略bbb:从顶楼逐层向下扔,那么最坏情况是min(K,N,F)min(K, N, F)min(K,N,F),即与第一种策略类似;
  4. \\n
  5. 采用最优策略f^\\\\hat{f}f^,我们现在可能不知道这个策略具体是什么,但是可以确定移动次数是x=f(K,N,F)x=f(K, N, F)x=f(K,N,F)
  6. \\n
\\n

针对上面列出的几种情况,不难发现,移动次数xxx是与K,N,FK, N, FK,N,F有关的函数,即f(K,N,F)f(K, N, F)f(K,N,F),那么函数fff就可以代表我们选择的策略,由于具体的策略我们是不知道的,所以将策略函数fff也看作一个变量,那么最终我们要求得的xxx表达式可以写作:

\\n

x=g(K,N,F,f)x=g(K, N, F, f)\\nx=g(K,N,F,f)

\\n

到了这一步,我们才算是真正的理解题意了,我们需要找到一个算法ggg,在策略空间fΨf\\\\in\\\\PsifΨ中找到最优策略f^\\\\hat{f}f^,并且针对这种策略f^\\\\hat{f}f^,找到最坏情况F\\\\in\\\\[0,N]对应的xxx

\\n

绕了这么久,可以发现,我们遍历所有可能的策略,并且在每个策略中,都假设楼层FFF总是出现在使当前策略fff移动次数最大的楼层处,题目并没有给出如何遍历楼层,也没有给出FFF的值,这两个值都需要我们在算法ggg中自行遍历。

\\n

对于这个问题,一个自然的思路就是使用动态规划的思想来处理。我们使用状态表dp[K][N]dp[K][N]dp[K][N]来表示我们拥有KKK个鸡蛋和NNN层楼时的最小移动次数,那么考虑初始情况,拥有i[0,K]i\\\\in[0,K]i[0,K]个蛋,j[0,N]j\\\\in[0,N]j[0,N]层楼时:

\\n
    \\n
  1. i=0orj=0i=0 or j=0i=0orj=0,毋庸置疑,不需要进行测试,因为没有鸡蛋和楼层,dp[0][1]=dp[1][0]=dp[0][0]=0dp[0][1]=dp[1][0]=dp[0][0]=0dp[0][1]=dp[1][0]=dp[0][0]=0
  2. \\n
  3. i=1,j[1,N]i=1, j\\\\in[1,N]i=1,j[1,N],此时我们只有一个蛋,那么只能使用前面提到的策略aaa来试探,那么最坏情况就是F=jF=jF=j,即蛋碎的楼层在最大楼层处,那么我们最少尝试次数就是dp[1][j]=j,j[1,N]dp[1][j]=j, j\\\\in[1,N]dp[1][j]=j,j[1,N]
  4. \\n
  5. i(1,K],j=1i\\\\in(1,K], j=1i(1,K],j=1,我们有不止一个鸡蛋,但是只有一层楼,那么毫无疑问,只需要测试一次就行了,得到dp[i][1]=1,i(1,K)dp[i][1]=1, i\\\\in(1, K)dp[i][1]=1,i(1,K)
  6. \\n
  7. i(1,K],j(1,N)i\\\\in(1,K], j\\\\in(1,N)i(1,K],j(1,N),即有不止一个鸡蛋,且不止一层楼,那么我们就需要使用最优策略f^\\\\hat{f}f^来确定最小移动次数,
  8. \\n
\\n

写成代码形式:

\\n
def superEggDrop(self, K: int, N: int) -> int:\\n    dp = [[0] * (K + 1) for i in range(N + 1)]\\n    for i in range(1, N + 1): dp[i][1] = i\\n    for i in range(1, K + 1): dp[1][i] = 1\\n\\n    for i in range(2, N + 1):\\n        for j in range(2, K + 1):\\n            dp[i][j] = dp[i][j - 1]\\n            for k in range(1, i + 1):\\n                dp[i][j] = min(dp[i][j], 1 + max(dp[k - 1][j - 1], dp[i - k][j]))\\n\\n    return dp[N][K]\\n
\\n

算法复杂度:O(KN2)O(KN^2)O(KN2)

\\n
\\n

查看带有LaTeX\\\\LaTeXLATEX公式渲染的博客内容:https://blog.simplenaive.cn/#/post/20

\\n
\\n"},"21":{"id":21,"date":"2019/06/29","author":"Yidadaa","title":"LeetCode困难题赏 - 600.不含连续1的非负整数","mdContent":"> 本文试从文法角度给出此题的解题思路。\\r\\n\\r\\n### 题目\\r\\n给定一个正整数 n,找出小于或等于 n 的非负整数中,其二进制表示不包含 连续的1的个数,其中$1\\\\leq n \\\\leq 10^9$。\\r\\n\\r\\n**示例:**\\r\\n```\\r\\n输入: 5\\r\\n输出: 5\\r\\n解释: \\r\\n下面是带有相应二进制表示的非负整数<= 5:\\r\\n0 : 0\\r\\n1 : 1\\r\\n2 : 10\\r\\n3 : 11\\r\\n4 : 100\\r\\n5 : 101\\r\\n其中,只有整数3违反规则(有两个连续的1),其他5个满足规则。\\r\\n```\\r\\n来源:[600. 不含连续1的非负整数](https://leetcode-cn.com/problems/non-negative-integers-without-consecutive-ones/)\\r\\n\\r\\n### 解析\\r\\n对于这种涉及到二进制字符串的题目,一般都会采用动态规划的思想来解决,观察本题所规定的二进制字符串,发现可以由以下文法$[1]$给出:\\r\\n\\r\\n$$S\\\\rightarrow 10S | 0S | 0 | 1 | \\\\epsilon$$\\r\\n\\r\\n那么根据该文法,我们自然而然地想到构造一个基于字符串长度的状态表$dp$,其中$dp[k]$表示长度为$k$的不含连续1的二进制字符串的个数,然后根据文法写出状态表的初始值:\\r\\n\\r\\n```python\\r\\ndp[0] = 1 # 由 S -> ε 语句给出,表示长度为0的字符串只有一个,即空字符串\\r\\ndp[1] = 2 # 由 S -> 0 | 1 语句给出,表示长度为1的字符串有两个,即0和1\\r\\n```\\r\\n\\r\\n然后根据文法语句$S\\\\rightarrow 10S | 0S$可知,任何长度为$k$的符合条件的字符串,都是由以下两种规则得到的:\\r\\n1. 在长度为$k-1$的字符串左侧添加$0$;\\r\\n2. 在长度为$k-2$的字符串左侧添加$10$。\\r\\n\\r\\n从而得到状态表的递推式:`dp[k] = dp[k - 1] + dp[k - 2]`,可以发现,该递推式就是斐波那契的生成式。\\r\\n\\r\\n到此为止,我们只能得到指定长度的二进制字符串的个数,题目中还有个限定条件是$\\\\leq n$,我们分析一个十进制转化为二进制字符串后的构成:\\r\\n$$5 \\\\rightarrow 101, 16 \\\\rightarrow 1000$$\\r\\n即首个字符肯定为1,也就是文法$[1]$中给出的形如$10S$的字符串,我们直接把十进制上界$n$转化为二进制后,可能会得到两种形式的字符串:\\r\\n1. $10S_{suffix}$,即第二位为0;\\r\\n2. $11S_{suffix}$,即第二位为1。\\r\\n\\r\\n对于第二种形式的正整数$n$,我们只需要计算小于$n$的形如$10S$的字符串对应的解即可,设求解程序为$f(S)$,则有如下的分解方式:\\r\\n$$f(S)=dp[L(g(S))]+f(g(S)_{suffix})$$\\r\\n其中$g(S)$表示不大于$S$的形如$10S$的字符串,$L(S)$表示字符串$S$的长度。\\r\\n\\r\\n### 代码\\r\\n[TODO]\\r\\n\\r\\n> 查看带有$\\\\LaTeX$公式渲染的博客内容:[https://blog.simplenaive.cn/#/post/21](https://blog.simplenaive.cn/#/post/21)","content":"
\\n

本文试从文法角度给出此题的解题思路。

\\n
\\n

题目

\\n

给定一个正整数 n,找出小于或等于 n 的非负整数中,其二进制表示不包含 连续的1的个数,其中1n1091\\\\leq n \\\\leq 10^91n109

\\n

示例:

\\n
输入: 5\\n输出: 5\\n解释: \\n下面是带有相应二进制表示的非负整数<= 5:\\n0 : 0\\n1 : 1\\n2 : 10\\n3 : 11\\n4 : 100\\n5 : 101\\n其中,只有整数3违反规则(有两个连续的1),其他5个满足规则。\\n
\\n

来源:600. 不含连续1的非负整数

\\n

解析

\\n

对于这种涉及到二进制字符串的题目,一般都会采用动态规划的思想来解决,观察本题所规定的二进制字符串,发现可以由以下文法[1][1][1]给出:

\\n

S10S0S01ϵS\\\\rightarrow 10S | 0S | 0 | 1 | \\\\epsilon\\nS10S0S01ϵ

\\n

那么根据该文法,我们自然而然地想到构造一个基于字符串长度的状态表dpdpdp,其中dp[k]dp[k]dp[k]表示长度为kkk的不含连续1的二进制字符串的个数,然后根据文法写出状态表的初始值:

\\n
dp[0] = 1 # 由 S -> ε 语句给出,表示长度为0的字符串只有一个,即空字符串\\ndp[1] = 2 # 由 S -> 0 | 1 语句给出,表示长度为1的字符串有两个,即0和1\\n
\\n

然后根据文法语句S10S0SS\\\\rightarrow 10S | 0SS10S0S可知,任何长度为kkk的符合条件的字符串,都是由以下两种规则得到的:

\\n
    \\n
  1. 在长度为k1k-1k1的字符串左侧添加000
  2. \\n
  3. 在长度为k2k-2k2的字符串左侧添加101010
  4. \\n
\\n

从而得到状态表的递推式:dp[k] = dp[k - 1] + dp[k - 2],可以发现,该递推式就是斐波那契的生成式。

\\n

到此为止,我们只能得到指定长度的二进制字符串的个数,题目中还有个限定条件是n\\\\leq nn,我们分析一个十进制转化为二进制字符串后的构成:

\\n

5101,1610005 \\\\rightarrow 101, 16 \\\\rightarrow 1000\\n5101,161000

\\n

即首个字符肯定为1,也就是文法[1][1][1]中给出的形如10S10S10S的字符串,我们直接把十进制上界nnn转化为二进制后,可能会得到两种形式的字符串:

\\n
    \\n
  1. 10Ssuffix10S_{suffix}10Ssuffix,即第二位为0;
  2. \\n
  3. 11Ssuffix11S_{suffix}11Ssuffix,即第二位为1。
  4. \\n
\\n

对于第二种形式的正整数nnn,我们只需要计算小于nnn的形如10S10S10S的字符串对应的解即可,设求解程序为f(S)f(S)f(S),则有如下的分解方式:

\\n

f(S)=dp[L(g(S))]+f(g(S)suffix)f(S)=dp[L(g(S))]+f(g(S)_{suffix})\\nf(S)=dp[L(g(S))]+f(g(S)suffix)

\\n

其中g(S)g(S)g(S)表示不大于SSS的形如10S10S10S的字符串,L(S)L(S)L(S)表示字符串SSS的长度。

\\n

代码

\\n

[TODO]

\\n
\\n

查看带有LaTeX\\\\LaTeXLATEX公式渲染的博客内容:https://blog.simplenaive.cn/#/post/21

\\n
\\n"},"22":{"id":22,"date":"2020/01/02","author":"Yidadaa","title":"2019,天际线","mdContent":"> **没有理想的人不伤心(节选)- 新裤子**\\r\\n> 你曾热爱的那个人\\r\\n> 这一生也不会再见面\\r\\n> 你等在这文化的废墟上\\r\\n> 已没人觉得你狂野\\r\\n> 那些让人敬仰的神殿\\r\\n> 只在无知的人心中灵验\\r\\n> 我住在属于我的猪圈\\r\\n> 这一夜无眠\\r\\n\\r\\n### 前言\\r\\n今年的总结不想再扯什么宏大叙事,因为我发现了人人都有的一种幻觉,对于同一件事情,自己得到的感触仿佛总要比别人的更强烈些,自己的人格仿佛就因此比别人显得更加独特,而不幸的是,这些独特的灵魂们最后往往都成了过江之鲫。\\r\\n\\r\\n所以笔者深知本文难逃窠臼,不过过去一年,有幸从身边人身上听到了一些关于自己的真实评价,这些只言片语对我而言,可谓是于无声处听惊雷,像是醉酒的人挨了当头一棒,偶尔清醒看到自己的满身狼藉,心中羞愧是有的,但是更多的还是感激,我为有这些说真话的亲人和朋友而感到庆幸。\\r\\n\\r\\n### 摘要\\r\\n简短地回顾 2019,前半年和室友参加了一个不怎么光彩的比赛,看到了许多成年人的利益纠纷,不由得感慨人活着实在太累。五月份参加了华为的软挑赛,中间和室友帮某大学的实验室讲了一些机器学习和深度学习的基础课程,恰了些烂钱。\\r\\n\\r\\n之后的半年几乎都在实验室度过,每天踩着滑板从宿舍到主楼通勤,上午九点钟到,下午六点钟走,晚上就在南门体育馆门前的大停车场练滑板。而且那时尤其喜欢吃朝阳的煲仔饭,午饭晚饭天天吃,后来到深圳吃了正宗的煲仔饭后才发现,食堂那个煲仔饭顶多叫盖浇饭 Pro,不过综合来看,还是盖浇饭 Pro 要更好吃一些(呲牙)。\\r\\n\\r\\n暑期结束后,室友和同学纷纷出去实习,我也有些心痒,所以在十一月初通过峙龙的推荐到腾讯的机器人实验室实习,开启了第二段实习经历。\\r\\n\\r\\n本文延续年终总结系列的传统设定,从各个方面盘点一下 2019 年的生活,这次会额外增加一些章节,并削减数据分析部分的内容,因为很多数据太过私人,没有展示出来的必要。\\r\\n\\r\\n### 消费\\r\\n2019 年全年都比较拮据,由于没有再四处接私活来产生稳定收入,只能靠不定期的比赛奖金和额外补贴来度日。\\r\\n\\r\\n2019 全年,我在 Switch 和 Steam 上买了接近两千块的游戏,这个支出既在意料之中,又令我觉得有点意外,因为自己称不上是个游戏狂热者,只是相对于几个朋友来说更喜欢玩单机游戏,像开源掌机这些小玩意儿,也从不吝惜钱财购买。很多游戏给我带来了很多欢乐,Steam 上的魔能、人类一败涂地、方块战斗剧场等,Switch 上的塞尔达传说、九张羊皮纸等,有些游戏和朋友一起玩欢乐得一塌糊涂,而有些游戏则是自己不知不觉就度过了几百个小时的孤独时光。\\r\\n\\r\\n游戏是第九艺术,我在 B 站上关注了很多做独立游戏的 UP 主,深知每款游戏都需要音乐、美术、剧情和程序的无间配合,才能与玩家见面,所以游戏算是消费品中的集大成者,兼有艺术美学和技术美学,这也是我对它们痴迷的原因,有时候购买一款游戏,可能仅仅是觉得开发者们为之付出的努力和激情值得赞赏。当然在大多数情况下,自己只是巴甫洛夫的那条狗罢了,期待从玩游戏这个曾经带来了很多欢乐的动作中再次获得快乐。\\r\\n\\r\\n 其次便是吃喝玩乐。\\r\\n\\r\\n### 编程\\r\\n[TODO]\\r\\n\\r\\n### 学习\\r\\n[TODO]\\r\\n\\r\\n### 品格\\r\\n[TODO]\\r\\n\\r\\n### 三观\\r\\n[TODO]\\r\\n\\r\\n### 健康\\r\\n[TODO]\\r\\n\\r\\n### 展望 2020\\r\\n[TODO]","content":"
\\n

没有理想的人不伤心(节选)- 新裤子\\n你曾热爱的那个人\\n这一生也不会再见面\\n你等在这文化的废墟上\\n已没人觉得你狂野\\n那些让人敬仰的神殿\\n只在无知的人心中灵验\\n我住在属于我的猪圈\\n这一夜无眠

\\n
\\n

前言

\\n

今年的总结不想再扯什么宏大叙事,因为我发现了人人都有的一种幻觉,对于同一件事情,自己得到的感触仿佛总要比别人的更强烈些,自己的人格仿佛就因此比别人显得更加独特,而不幸的是,这些独特的灵魂们最后往往都成了过江之鲫。

\\n

所以笔者深知本文难逃窠臼,不过过去一年,有幸从身边人身上听到了一些关于自己的真实评价,这些只言片语对我而言,可谓是于无声处听惊雷,像是醉酒的人挨了当头一棒,偶尔清醒看到自己的满身狼藉,心中羞愧是有的,但是更多的还是感激,我为有这些说真话的亲人和朋友而感到庆幸。

\\n

摘要

\\n

简短地回顾 2019,前半年和室友参加了一个不怎么光彩的比赛,看到了许多成年人的利益纠纷,不由得感慨人活着实在太累。五月份参加了华为的软挑赛,中间和室友帮某大学的实验室讲了一些机器学习和深度学习的基础课程,恰了些烂钱。

\\n

之后的半年几乎都在实验室度过,每天踩着滑板从宿舍到主楼通勤,上午九点钟到,下午六点钟走,晚上就在南门体育馆门前的大停车场练滑板。而且那时尤其喜欢吃朝阳的煲仔饭,午饭晚饭天天吃,后来到深圳吃了正宗的煲仔饭后才发现,食堂那个煲仔饭顶多叫盖浇饭 Pro,不过综合来看,还是盖浇饭 Pro 要更好吃一些(呲牙)。

\\n

暑期结束后,室友和同学纷纷出去实习,我也有些心痒,所以在十一月初通过峙龙的推荐到腾讯的机器人实验室实习,开启了第二段实习经历。

\\n

本文延续年终总结系列的传统设定,从各个方面盘点一下 2019 年的生活,这次会额外增加一些章节,并削减数据分析部分的内容,因为很多数据太过私人,没有展示出来的必要。

\\n

消费

\\n

2019 年全年都比较拮据,由于没有再四处接私活来产生稳定收入,只能靠不定期的比赛奖金和额外补贴来度日。

\\n

2019 全年,我在 Switch 和 Steam 上买了接近两千块的游戏,这个支出既在意料之中,又令我觉得有点意外,因为自己称不上是个游戏狂热者,只是相对于几个朋友来说更喜欢玩单机游戏,像开源掌机这些小玩意儿,也从不吝惜钱财购买。很多游戏给我带来了很多欢乐,Steam 上的魔能、人类一败涂地、方块战斗剧场等,Switch 上的塞尔达传说、九张羊皮纸等,有些游戏和朋友一起玩欢乐得一塌糊涂,而有些游戏则是自己不知不觉就度过了几百个小时的孤独时光。

\\n

游戏是第九艺术,我在 B 站上关注了很多做独立游戏的 UP 主,深知每款游戏都需要音乐、美术、剧情和程序的无间配合,才能与玩家见面,所以游戏算是消费品中的集大成者,兼有艺术美学和技术美学,这也是我对它们痴迷的原因,有时候购买一款游戏,可能仅仅是觉得开发者们为之付出的努力和激情值得赞赏。当然在大多数情况下,自己只是巴甫洛夫的那条狗罢了,期待从玩游戏这个曾经带来了很多欢乐的动作中再次获得快乐。

\\n

其次便是吃喝玩乐。

\\n

编程

\\n

[TODO]

\\n

学习

\\n

[TODO]

\\n

品格

\\n

[TODO]

\\n

三观

\\n

[TODO]

\\n

健康

\\n

[TODO]

\\n

展望 2020

\\n

[TODO]

\\n"},"25":{"id":25,"date":"2020/02/12","author":"Yidadaa","title":"LeetCode 趣题赏析 - 448. 找到数组中消失的数字","mdContent":"> 这是一道简单题,但是题目中的附加条件使得这道题别具趣味性。\\r\\n\\r\\n## 题目\\r\\n给定一个范围在 `1 ≤ a[i] ≤ n` ( `n` = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。\\r\\n\\r\\n找到所有在 `[1, n]` 范围之间没有出现在数组中的数字。\\r\\n\\r\\n您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。\\r\\n\\r\\n示例:\\r\\n```\\r\\n输入:\\r\\n[4,3,2,7,8,2,3,1]\\r\\n\\r\\n输出:\\r\\n[5,6]\\r\\n```\\r\\n来源:[力扣(LeetCode)](https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array)\\r\\n\\r\\n## 解读\\r\\n从题意出发,数组中重复元素会挤占消失元素的位置,我们举个比较简单的例子:\\r\\n```python\\r\\n[1, 2, 3, 4, 5, 1, 7]\\r\\n```\\r\\n可以看到元素 `1` 挤占了元素 `6` 的空间,我们可以很轻松地通过判断 `nums[i] == i` 来找到答案。\\r\\n\\r\\n这意味着,如果我们能对输入数组进行某种变换,使得数组中的元素满足某种特定地排列方式,那么我们只需要找出不等于索引值的元素所在的索引,就找到了消失的数字。\\r\\n\\r\\n举个稍微复杂点的例子,比如示例输入:\\r\\n```python\\r\\n[4, 3, 2, 7, 8, 2, 3, 1]\\r\\n```\\r\\n为了方便理解,我们对其排个序:\\r\\n```python\\r\\n[1, 2, 2, 3, 3, 4, 7, 8]\\r\\n```\\r\\n手动变换为我们想要的数组形式:\\r\\n```python\\r\\n[1, 2, 3, 4, 2, 3, 7, 8]\\r\\n```\\r\\n可以看到,索引 `5` 和 `6` 处(为方便表述,本文索引是从 `1` 开始)的元素满足了 `nums[i] != i`,所以 `[5, 6]` 就是我们要的答案。\\r\\n\\r\\n那么该如何进行这种变换呢?可以看到题中特地指出 `n == len(nums)`,这提示我们可以用元素值和索引值之间的映射关系来解决问题,过程如下所示,其中 `^` 表示当前索引:\\r\\n```python\\r\\n# 原始数组\\r\\n[4, 3, 2, 7, 8, 2, 3, 1]\\r\\n ^\\r\\n```\\r\\n先判断当前索引指向的数字是否满足 `nums[i] == i` 以及 `nums[nums[i]] == nums[i]`:\\r\\n- 如果已经满足则跳向下一个索引;\\r\\n- 如果不满足,则进行交换,即 `swap(nums[i], nums[nums[i]])`。\\r\\n\\r\\n逐次进行,我们得到以下步骤:\\r\\n```python\\r\\n[7, 3, 2, 4, 8, 2, 3, 1]\\r\\n ^\\r\\n[3, 3, 2, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[2, 3, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 1, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n```\\r\\n可以看到,数组逐渐变得有序,而且这种变换只需要 $O(N)$ 的时间复杂度,并且无需额外空间。\\r\\n\\r\\n## 代码\\r\\n```python\\r\\nclass Solution:\\r\\n def findDisappearedNumbers(self, nums: List[int]) -> List[int]:\\r\\n nums, i = [x - 1 for x in nums], 0 # 预处理\\r\\n while i < len(nums):\\r\\n if nums[i] == i and nums[nums[i]] == nums[i]\\r\\n tmp, nums[i] = nums[i], nums[nums[i]]\\r\\n nums[tmp] = tmp # 执行交换\\r\\n else: i += 1\\r\\n ret = [] # 找出消失的数字\\r\\n for i in range(len(nums)):\\r\\n if i != nums[i]: ret.append(i + 1)\\r\\n return ret\\r\\n```","content":"
\\n

这是一道简单题,但是题目中的附加条件使得这道题别具趣味性。

\\n
\\n

题目

\\n

给定一个范围在 1 ≤ a[i] ≤ nn = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。

\\n

找到所有在 [1, n] 范围之间没有出现在数组中的数字。

\\n

您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。

\\n

示例:

\\n
输入:\\n[4,3,2,7,8,2,3,1]\\n\\n输出:\\n[5,6]\\n
\\n

来源:力扣(LeetCode)

\\n

解读

\\n

从题意出发,数组中重复元素会挤占消失元素的位置,我们举个比较简单的例子:

\\n
[1, 2, 3, 4, 5, 1, 7]\\n
\\n

可以看到元素 1 挤占了元素 6 的空间,我们可以很轻松地通过判断 nums[i] == i 来找到答案。

\\n

这意味着,如果我们能对输入数组进行某种变换,使得数组中的元素满足某种特定地排列方式,那么我们只需要找出不等于索引值的元素所在的索引,就找到了消失的数字。

\\n

举个稍微复杂点的例子,比如示例输入:

\\n
[4, 3, 2, 7, 8, 2, 3, 1]\\n
\\n

为了方便理解,我们对其排个序:

\\n
[1, 2, 2, 3, 3, 4, 7, 8]\\n
\\n

手动变换为我们想要的数组形式:

\\n
[1, 2, 3, 4, 2, 3, 7, 8]\\n
\\n

可以看到,索引 56 处(为方便表述,本文索引是从 1 开始)的元素满足了 nums[i] != i,所以 [5, 6] 就是我们要的答案。

\\n

那么该如何进行这种变换呢?可以看到题中特地指出 n == len(nums),这提示我们可以用元素值和索引值之间的映射关系来解决问题,过程如下所示,其中 ^ 表示当前索引:

\\n
# 原始数组\\n[4, 3, 2, 7, 8, 2, 3, 1]\\n ^\\n
\\n

先判断当前索引指向的数字是否满足 nums[i] == i 以及 nums[nums[i]] == nums[i]

\\n
    \\n
  • 如果已经满足则跳向下一个索引;
  • \\n
  • 如果不满足,则进行交换,即 swap(nums[i], nums[nums[i]])
  • \\n
\\n

逐次进行,我们得到以下步骤:

\\n
[7, 3, 2, 4, 8, 2, 3, 1]\\n ^\\n[3, 3, 2, 4, 8, 2, 7, 1]\\n ^\\n[2, 3, 3, 4, 8, 2, 7, 1]\\n ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n    ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n       ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n          ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n             ^\\n[3, 2, 3, 4, 1, 2, 7, 8]\\n             ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n             ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n                ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n                   ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n                      ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n
\\n

可以看到,数组逐渐变得有序,而且这种变换只需要 O(N)O(N)O(N) 的时间复杂度,并且无需额外空间。

\\n

代码

\\n
class Solution:\\n    def findDisappearedNumbers(self, nums: List[int]) -> List[int]:\\n        nums, i = [x - 1 for x in nums], 0 # 预处理\\n        while i < len(nums):\\n            if nums[i] == i and nums[nums[i]] == nums[i]\\n                tmp, nums[i] = nums[i], nums[nums[i]]\\n                nums[tmp] = tmp # 执行交换\\n            else: i += 1\\n        ret = [] # 找出消失的数字\\n        for i in range(len(nums)):\\n            if i != nums[i]: ret.append(i + 1)\\n        return ret\\n
\\n"},"26":{"id":26,"date":"2020/02/27","author":"Yidadaa","title":"花式遍历二叉树","mdContent":"> 众所周知,递归是解决问题的良药,但是却不利于装逼,今天教你如何在最简单的问题上装逼。\\r\\n\\r\\n一般来说,基本所有的二叉树的题目都需要遍历树的每一个节点来找到解,在本科阶段学习数据结构时,所有的教材都会教给我们如何使用递归来执行各种遍历,诚然,递归形式的遍历不可谓不简洁:\\r\\n```python\\r\\nclass Node():\\r\\n def __init__(self, val, left, right):\\r\\n self.val = val\\r\\n self.left = left\\r\\n self.right = right\\r\\n\\r\\ndef traverse(node):\\r\\n if not node: return\\r\\n for next_node in [node, node.left, node.right]:\\r\\n if next_node == node: print(node.val)\\r\\n else: traverse(next_node)\\r\\n```\\r\\n我们只需要简单地调整 `[node, node.left, node.right]` 的顺序,就可以轻松地实现前序遍历、中序遍历和后序遍历,但是如果我们想要实现层次遍历,递归就无能为力了,而且由于递归本身自带的爆栈特性,在处理大规模数据时非常容易由于空间不足而报错[2]。\\r\\n\\r\\n那么有没有一种有效的遍历方法,只需要稍加变形就能实现以上四种遍历呢?这个时候就要请出树和图搜索中最常用的非递归遍历方式了,只需使用一个栈或者队列来辅助,就可以轻松实现各种遍历方式。\\r\\n\\r\\n我们先用一个简单的深度优先遍历(Depth First Search, DFS)算法作为例子,在前面定义好的二叉树结构的基础上,使用以下代码进行深度优先遍历:\\r\\n```python\\r\\ndef dfs(node):\\r\\n if not node: return\\r\\n print(node.val)\\r\\n for next_node in [node.left, node.right]:\\r\\n dfs(next_node)\\r\\n```\\r\\n这是一个小学生都会写的递归版本 DFS,显然无法满足我们的装逼需求,我们略施小计将其改造成非递归形式:\\r\\n```python\\r\\ndef dfs(node):\\r\\n stack = [node]\\r\\n while len(stack) > 0:\\r\\n node = stack.pop()\\r\\n if not node: continue\\r\\n print(node.val)\\r\\n for child in [node.left, node.right]:\\r\\n stack.append(child)\\r\\n```\\r\\n可以看到,代码虽然变长了,但是其解决思路却十分优雅,如果你碰巧没有在《编译原理》课上逃课、摸鱼或者睡觉,你应该能一眼看出这里的栈(stack)其实就是在模拟编译器(对于 Python 来说是解释器)在运行递归函数时的行为,回想一下之前的递归代码,每次调用自身函数时,都相当于往栈的末尾压入一个新的函数调用点,而很多编译器或者解释器(比如 Python 或者 Javascript)都没有开启尾递归优化[1],从而引入了栈溢出的风险,所以在实际写代码的时候,非递归形式的代码往往能在性能和效率上更胜一筹,这也是除了能用来装逼之外的更重要的优点。\\r\\n\\r\\n说了半天,让我们切入到文章的主题,如何对二叉树进行非递归形式的遍历呢?刚刚的 DFS 代码虽然很靓仔,但是有很多时候我要进行前中后序遍历或者层次遍历,该怎么写呢?其实只需要稍加变形即可。\\r\\n\\r\\n我们再次略施小计,把 DFS 变成广度优先遍历(Breadth First Search, BFS),而且只需要改动一处即可:\\r\\n```python\\r\\ndef dfs(node):\\r\\n queue= [node]\\r\\n while len(queue) > 0:\\r\\n node = queue.pop(0)\\r\\n if not node: continue\\r\\n print(node.val)\\r\\n for child in [node.left, node.right]:\\r\\n queue.append(child)\\r\\n```\\r\\n可以看到,我们只将栈改成了队列(queue),对于 Python 来说,只需要把 `pop` 函数的参数设置为 `0` 即可,为什么把栈改成队列就能将 DFS 改成 BFS 呢?回想一下栈和队列的特性:\\r\\n1. 栈的特性是先进后出,即所有元素只能从栈的尾部进入,尾部弹出;\\r\\n2. 队列的特性是先进先出,即所有元素从队列的尾部进入,头部弹出。\\r\\n\\r\\n基于队列的这种先进先出的特性,我们的函数在遍历第 `i + 1` 层节点时,其第 `i` 层的节点总能保证是已经被遍历过的,从而实现了广度优先遍历,而**二叉树的层次遍历其实就是广度优先遍历**。\\r\\n\\r\\n最后我们回到开头的引例,把上面的非递归形式的代码改造一下,从而完成二叉树的前中后序遍历,以二叉树的前序遍历为例子:\\r\\n```python\\r\\ndef pre_order(node):\\r\\n stack = [node]\\r\\n while len(stack) > 0:\\r\\n node = stack.pop()\\r\\n if not node: continue\\r\\n print(node.val)\\r\\n for child in [node.right, node.left]:\\r\\n stack.append(child)\\r\\n```\\r\\n可以看到,相对于原始的 DFS 代码,我们只调整了左右子树的入栈顺序,先将右子树入栈,再将左子树入栈,这样保证了出栈时左子树总是先于右子树出栈,从而保证了左子树一定会在右子树之前被遍历到,从而满足了前序遍历的“根左右”的遍历顺序。\\r\\n\\r\\n然后再看中序遍历,思考一下中序遍历的要求:每个节点只能在其左子树遍历完成后遍历,然后再遍历其右节点,即“左根右”的顺序,也就是说,我们要控制根节点被遍历的时机,但是基于栈的方式又不可避免地要先遍历根节点才能访问到其左右孩子,那么怎么才能判定根节点被访问的时机呢?其实我们只需要略施小计,让访问过的节点“二进宫”即可,即给每个节点多设置一个状态:\\r\\n```python\\r\\ndef pre_order(node):\\r\\n stack = [(node, False)]\\r\\n while len(stack) > 0:\\r\\n node, shuold_traverse = stack.pop()\\r\\n if not node: continue\\r\\n if should_traverse:\\r\\n print(node.val)\\r\\n continue\\r\\n for child in [node.right, node, node.left]:\\r\\n stack.append((child, child == node))\\r\\n```\\r\\n可以看到,我们用 `should_traverse` 来控制遍历的时机,每个节点并不会在第一时间被输出,而是赋予一个遍历状态,注意到二次入栈时,每个节点都会严格按照“右根左“的顺序入栈,这样出栈时顺序正好为”左根右“,从而保证了每个根节点只会在其左子树之后输出,满足了中序遍历的条件。\\r\\n\\r\\n有了前两个的铺垫,我们只需故技重施,就可以把中序遍历改造成后序遍历,即入栈时只需要按照”根右左“的顺序入栈:\\r\\n```python\\r\\ndef pre_order(node):\\r\\n stack = [(node, False)]\\r\\n while len(stack) > 0:\\r\\n node, shuold_traverse = stack.pop()\\r\\n if not node: continue\\r\\n if should_traverse:\\r\\n print(node.val)\\r\\n continue\\r\\n for child in [node, node.right, node.left]:\\r\\n stack.append((child, child == node))\\r\\n```\\r\\n这样根节点只会在其左右子树都遍历完成之后再输出,从而满足了后序遍历的条件。\\r\\n\\r\\n到此为止,花式遍历二叉树的方法就介绍完毕了,其实并没有什么高深的技巧,核心思想就是用队列或者栈来模拟递归函数的行为,为所有的遍历行为提供了一个统一的行为,从而更方便记忆,在面试的时候写出来也会有奇效。\\r\\n\\r\\n思考题:二叉树其实是一种有向无环图,可以想一下如何用非递归方法对一个图进行深度优先和广度优先遍历。\\r\\n\\r\\n### 附录\\r\\n1. [维基百科:尾调用](https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8)\\r\\n2. [维基百科:堆栈溢出](https://zh.wikipedia.org/wiki/%E5%A0%86%E7%96%8A%E6%BA%A2%E4%BD%8D)","content":"
\\n

众所周知,递归是解决问题的良药,但是却不利于装逼,今天教你如何在最简单的问题上装逼。

\\n
\\n

一般来说,基本所有的二叉树的题目都需要遍历树的每一个节点来找到解,在本科阶段学习数据结构时,所有的教材都会教给我们如何使用递归来执行各种遍历,诚然,递归形式的遍历不可谓不简洁:

\\n
class Node():\\n  def __init__(self, val, left, right):\\n    self.val = val\\n    self.left = left\\n    self.right = right\\n\\ndef traverse(node):\\n  if not node: return\\n  for next_node in [node, node.left, node.right]:\\n    if next_node == node: print(node.val)\\n    else: traverse(next_node)\\n
\\n

我们只需要简单地调整 [node, node.left, node.right] 的顺序,就可以轻松地实现前序遍历、中序遍历和后序遍历,但是如果我们想要实现层次遍历,递归就无能为力了,而且由于递归本身自带的爆栈特性,在处理大规模数据时非常容易由于空间不足而报错[2]。

\\n

那么有没有一种有效的遍历方法,只需要稍加变形就能实现以上四种遍历呢?这个时候就要请出树和图搜索中最常用的非递归遍历方式了,只需使用一个栈或者队列来辅助,就可以轻松实现各种遍历方式。

\\n

我们先用一个简单的深度优先遍历(Depth First Search, DFS)算法作为例子,在前面定义好的二叉树结构的基础上,使用以下代码进行深度优先遍历:

\\n
def dfs(node):\\n  if not node: return\\n  print(node.val)\\n  for next_node in [node.left, node.right]:\\n    dfs(next_node)\\n
\\n

这是一个小学生都会写的递归版本 DFS,显然无法满足我们的装逼需求,我们略施小计将其改造成非递归形式:

\\n
def dfs(node):\\n  stack = [node]\\n  while len(stack) > 0:\\n    node = stack.pop()\\n    if not node: continue\\n    print(node.val)\\n    for child in [node.left, node.right]:\\n      stack.append(child)\\n
\\n

可以看到,代码虽然变长了,但是其解决思路却十分优雅,如果你碰巧没有在《编译原理》课上逃课、摸鱼或者睡觉,你应该能一眼看出这里的栈(stack)其实就是在模拟编译器(对于 Python 来说是解释器)在运行递归函数时的行为,回想一下之前的递归代码,每次调用自身函数时,都相当于往栈的末尾压入一个新的函数调用点,而很多编译器或者解释器(比如 Python 或者 Javascript)都没有开启尾递归优化[1],从而引入了栈溢出的风险,所以在实际写代码的时候,非递归形式的代码往往能在性能和效率上更胜一筹,这也是除了能用来装逼之外的更重要的优点。

\\n

说了半天,让我们切入到文章的主题,如何对二叉树进行非递归形式的遍历呢?刚刚的 DFS 代码虽然很靓仔,但是有很多时候我要进行前中后序遍历或者层次遍历,该怎么写呢?其实只需要稍加变形即可。

\\n

我们再次略施小计,把 DFS 变成广度优先遍历(Breadth First Search, BFS),而且只需要改动一处即可:

\\n
def dfs(node):\\n  queue= [node]\\n  while len(queue) > 0:\\n    node = queue.pop(0)\\n    if not node: continue\\n    print(node.val)\\n    for child in [node.left, node.right]:\\n      queue.append(child)\\n
\\n

可以看到,我们只将栈改成了队列(queue),对于 Python 来说,只需要把 pop 函数的参数设置为 0 即可,为什么把栈改成队列就能将 DFS 改成 BFS 呢?回想一下栈和队列的特性:

\\n
    \\n
  1. 栈的特性是先进后出,即所有元素只能从栈的尾部进入,尾部弹出;
  2. \\n
  3. 队列的特性是先进先出,即所有元素从队列的尾部进入,头部弹出。
  4. \\n
\\n

基于队列的这种先进先出的特性,我们的函数在遍历第 i + 1 层节点时,其第 i 层的节点总能保证是已经被遍历过的,从而实现了广度优先遍历,而二叉树的层次遍历其实就是广度优先遍历

\\n

最后我们回到开头的引例,把上面的非递归形式的代码改造一下,从而完成二叉树的前中后序遍历,以二叉树的前序遍历为例子:

\\n
def pre_order(node):\\n  stack = [node]\\n  while len(stack) > 0:\\n    node = stack.pop()\\n    if not node: continue\\n    print(node.val)\\n    for child in [node.right, node.left]:\\n      stack.append(child)\\n
\\n

可以看到,相对于原始的 DFS 代码,我们只调整了左右子树的入栈顺序,先将右子树入栈,再将左子树入栈,这样保证了出栈时左子树总是先于右子树出栈,从而保证了左子树一定会在右子树之前被遍历到,从而满足了前序遍历的“根左右”的遍历顺序。

\\n

然后再看中序遍历,思考一下中序遍历的要求:每个节点只能在其左子树遍历完成后遍历,然后再遍历其右节点,即“左根右”的顺序,也就是说,我们要控制根节点被遍历的时机,但是基于栈的方式又不可避免地要先遍历根节点才能访问到其左右孩子,那么怎么才能判定根节点被访问的时机呢?其实我们只需要略施小计,让访问过的节点“二进宫”即可,即给每个节点多设置一个状态:

\\n
def pre_order(node):\\n  stack = [(node, False)]\\n  while len(stack) > 0:\\n    node, shuold_traverse = stack.pop()\\n    if not node: continue\\n    if should_traverse:\\n        print(node.val)\\n        continue\\n    for child in [node.right, node, node.left]:\\n      stack.append((child, child == node))\\n
\\n

可以看到,我们用 should_traverse 来控制遍历的时机,每个节点并不会在第一时间被输出,而是赋予一个遍历状态,注意到二次入栈时,每个节点都会严格按照“右根左“的顺序入栈,这样出栈时顺序正好为”左根右“,从而保证了每个根节点只会在其左子树之后输出,满足了中序遍历的条件。

\\n

有了前两个的铺垫,我们只需故技重施,就可以把中序遍历改造成后序遍历,即入栈时只需要按照”根右左“的顺序入栈:

\\n
def pre_order(node):\\n  stack = [(node, False)]\\n  while len(stack) > 0:\\n    node, shuold_traverse = stack.pop()\\n    if not node: continue\\n    if should_traverse:\\n        print(node.val)\\n        continue\\n    for child in [node, node.right, node.left]:\\n      stack.append((child, child == node))\\n
\\n

这样根节点只会在其左右子树都遍历完成之后再输出,从而满足了后序遍历的条件。

\\n

到此为止,花式遍历二叉树的方法就介绍完毕了,其实并没有什么高深的技巧,核心思想就是用队列或者栈来模拟递归函数的行为,为所有的遍历行为提供了一个统一的行为,从而更方便记忆,在面试的时候写出来也会有奇效。

\\n

思考题:二叉树其实是一种有向无环图,可以想一下如何用非递归方法对一个图进行深度优先和广度优先遍历。

\\n

附录

\\n
    \\n
  1. 维基百科:尾调用
  2. \\n
  3. 维基百科:堆栈溢出
  4. \\n
\\n"},"27":{"id":27,"date":"2020/02/28","author":"Yidadaa","title":"Gettysburg Address","mdContent":"> Abraham Lincoln, Gettysburg, Pennsylvania, 1863\\r\\n\\r\\nFour score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.\\r\\n\\r\\nNow we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.\\r\\n\\r\\nBut, in a larger sense, we can not dedicate, we can not consecrate, we can not hallow, this ground. The brave men, living and dead, who struggled here, have consecrated it, far above out poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living rather to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us, that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion, that we here highly resolve that these dead shall not have died in vain, that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.\\r\\n\\r\\nLink: [Video](https://www.bilibili.com/video/av45620254?from=search&seid=1967124183545885008)","content":"
\\n

Abraham Lincoln, Gettysburg, Pennsylvania, 1863

\\n
\\n

Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.

\\n

Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.

\\n

But, in a larger sense, we can not dedicate, we can not consecrate, we can not hallow, this ground. The brave men, living and dead, who struggled here, have consecrated it, far above out poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living rather to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us, that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion, that we here highly resolve that these dead shall not have died in vain, that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.

\\n

Link: Video

\\n"},"28":{"id":28,"date":"2020/03/18","author":"Yidadaa","title":"树和数组的千层套路","mdContent":"> 今天教你怎么在线性数组和二叉树间反复横跳。\\r\\n\\r\\n目录:\\r\\n- [x] 二叉堆\\r\\n- [ ] 树状数组\\r\\n- [x] 线段树\\r\\n\\r\\n在最简单的一类数据结构中,有两种最为基础:线性数组和树,它们在实际的应用中十分常用,比如线性数组用来存储结构化数据,树用来存储一些有层级归属关系的数据。一个最典型的使用树的场景就是浏览器中的 GUI 渲染策略,在你阅读本篇文章的时候,浏览器就已经从 HTML 文件中解析出当前界面上的所有元素,并按照从属关系依次渲染出来,你只需要按下键盘上的 `F12` 键,就可以看到页面中所有元素的从属关系。\\r\\n\\r\\n然而,一般意义上的树并不是高度结构化的数据,虽然一颗树的所有信息都可以由根节点遍历出来,但是在没有提前建立索引的情况下,你很难轻易地直接获取树中任意节点的数据。所以人们为了更高的随机索引速度,会更倾向于使用结构稳定的二叉树,尤其是完全二叉树,这是由于完全二叉树的左右子树高度差不超过 1,其每层的节点数量都是 $2^n$ ,所以完全二叉树可以很轻松地存储在线性数组里,然后通过 `child = tree[parent_index * 2]` 的方式去递推任意孩子节点的索引值。\\r\\n\\r\\n二叉树的这种特性使得树状结构可以很方便地存储在线性数组中,而本文就将阐述那些和线性数组关系紧密的树状数据结构们。\\r\\n\\r\\n## 二叉堆\\r\\n在使用二叉堆之前,你需要知道什么是堆,以及为什么要有堆。\\r\\n\\r\\n根据[维基百科](https://zh.wikipedia.org/wiki/%E5%A0%86%E7%A9%8D)的描述,堆是一种具有特殊顺序的树,也就是说,堆就是一种树,就像二叉搜索树那样,堆的节点间也有一些大小关系,最常用的堆是**最大堆**和**最小堆**,又称大(小)顶堆、大(小)根堆等,以最大堆为例,最大堆中的任意一个节点都比它的子节点的值更大,也就是说,堆的根节点的元素是整个堆的最大值,值得注意的是,这种大小约束只对父节点和子节点生效,而相同层级的节点不需要有大小关系上的约束。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/77818922-a1bb2280-7111-11ea-8eef-a4c8ab7e5783.png)\\r\\n\\r\\n除此之外,堆还必须是一颗完全二叉树,这可以保证对堆的各种操作的均摊复杂度最低。\\r\\n\\r\\n但是你可能会依然很疑惑,就像我在数据结构课上听到一个全新的数据结构的时候,心里会想:“wow, awesome, 然后呢?”,人们造出来拥有很多复杂结构的事物,并不是完全为了消磨时间,而是因为为了解决问题而不得不让这些工具变得这么复杂,本文中提到的三种数据结构也是如此,它们本身的特性使得它们在解决某些问题时异常好用。\\r\\n\\r\\n而堆的最常用例子就是优先队列,你可以看到堆的根节点始终是最大值或者最小值,这种最值可以以优先级的形式体现出来,而且这种极值顺序会在动态增减的过程中始终以 $O(log N)$ 的时间复杂度保持着,回想一下,如果你要对一个普通的线性数组取极大值,你就不得不付出 $O(N)$ 的时间复杂度。堆的特性可以大大降低很多需要动态维护优先级的算法的复杂度,比如原始的[迪杰斯特拉最短路径算法](https://zh.wikipedia.org/wiki/%E6%88%B4%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95)的时间复杂度是 $O(N^2)$,而使用堆优化后可以达到 $O(N logN)$。\\r\\n\\r\\n现在你知道了什么是堆,以及堆用来解决那些问题,下面就介绍一下堆的常用操作,本文所述的堆均代表二叉堆。\\r\\n\\r\\n二叉堆通常存储在一个数组中,而堆的插入(insert)、弹出(pop)、取堆顶(top)、删除(remove)操作均可以通过不断交换数组元素来完成,而这些操作均有两个最基础的操作组成:向上调整(up)和向下调整(down),顾名思义,向上调整就是从当前节点开始向根节点遍历,确认该路径上所有节点是否满足“子节点小于(大于)父节点”的顺序,如果不满足,则进行调整;而向下调整则是向叶子节点进行遍历,然后执行同样的操作,写成代码形式(以最大堆为例):\\r\\n\\r\\n```python\\r\\nclass Heap:\\r\\n def __init__(self):\\r\\n self.heap = []\\r\\n\\r\\n def swap(self, i, j):\\r\\n self.heap[i], self.heap[j] = self.heap[j], self.heap[i]\\r\\n\\r\\n def up(self, k):\\r\\n \'\'\'向上调整第 k 个节点\'\'\'\\r\\n while k > 0:\\r\\n parent_index = (k - 1) // 2\\r\\n # 如果子节点大于父节点,则进行调整\\r\\n if self.heap[k] > self.heap[parent_index]:\\r\\n self.swap(k, parent_index)\\r\\n else: break\\r\\n\\r\\n def down(self, k)\\r\\n \'\'\'向下调整第 k 个节点\'\'\'\\r\\n while k < len(self.heap):\\r\\n lchild, rchild = k * 2 + 1, k * 2 + 2\\r\\n # 两个子节点取较大节点\\r\\n child = lchild if self.heap[lchild] > self.heap[rchild] or rchild > len(self.heap) else rchild\\r\\n if child < len(self.heap) and self.heap[child] > self.heap[k]:\\r\\n self.swap(child, k)\\r\\n k = child\\r\\n else: break\\r\\n```\\r\\n\\r\\n可以看到,调整部分的代码的逻辑还是比较清晰的,只需要不断向上或者向下遍历,然后调整对应的值即可。有了两个基础操作,我们可以很方便地完成插入、弹出、取顶、删除等操作了,直接看代码:\\r\\n\\r\\n```python\\r\\nclass Heap:\\r\\n \'\'\'省略 up, down 部分的代码\'\'\'\\r\\n def insert(self, val):\\r\\n \'\'\'将 val 插入堆中\'\'\'\\r\\n self.heap.append(val) # 插到数组尾部\\r\\n self.up(len(self.heap) - 1) # 然后不断向上调整即可\\r\\n\\r\\n def top(self):\\r\\n \'\'\'获取堆顶值\'\'\'\\r\\n return None if len(self.heap) == 0 else self.heap[0] # 只需返回数组中的第一个元素即可\\r\\n\\r\\n def remove(self, k):\\r\\n \'\'\'移除第 k 个节点\'\'\'\\r\\n self.swap(k, len(self.heap) - 1) # 将第 k 个元素交换到尾部\\r\\n ret = self.heap.pop() # 将其从数组中删除\\r\\n self.up(k) # 确认是否需要向上调整\\r\\n self.down(k) # 确认是否需要向下调整\\r\\n return ret\\r\\n\\r\\n def pop(self):\\r\\n \'\'\'弹出堆顶\'\'\'\\r\\n return self.remove(0)\\r\\n```\\r\\n\\r\\n掌握了二叉堆,我们可以很轻松地解决 TOP-k 问题,以及所有需要使用到优先队列的问题,不过值得一提的是,二叉堆作为一种非常常用的数据结构,已经被内置到很多语言的官方库中,在实际使用或者写算法题时,可以直接调包使用,比如 Python3 的 `queue.PriorityQueue` 和 `heapq`,以及 C++ 的 `priority_queue` 等。\\r\\n\\r\\n## 线段树和树状数组\\r\\n线段树和树状数组非常像,可以说树状数组就是线段树的一种简化形式,在应对单点修改的区间问题时,树状数组更为简洁好用,但由于使用了 `low-bit` 技巧,相对来说并没有线段树容易理解,所以本文就先从线段树的原理讲起,再逐步扩展到树状数组。\\r\\n\\r\\n首先,我们用一个例题来作为切入概念:\\r\\n\\r\\n>来源:[303. 区域和检索 - 数组不可变](https://leetcode-cn.com/problems/range-sum-query-immutable/)\\r\\n> 给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。\\r\\n\\r\\n下面是一些示例,给定 `nums = [-2, 0, 3, -5, 2, -1]`,那么对任意的 `(i, j)` 进行求和:\\r\\n```\\r\\nsumRange(0, 2) -> 1\\r\\nsumRange(2, 5) -> -1\\r\\nsumRange(0, 5) -> -3\\r\\n```\\r\\n\\r\\n我们可以很容易地想到 $o(N)$ 的解法,直接对区间 $[i ... j]$ 的数进行求和即可,但是当查询次数很大时,很容易就会超时,而如果我们使用前缀和数组,就可以轻松地在 $o(1)$ 时间内完成任何查询操作,所谓的前缀和数组,就是把前 $i$ 位的数字加起来作为第 $i$ 位的元素值,即 $s[i] = \\\\sum_{j=0}^i a_i$,代码表示如下:\\r\\n\\r\\n```python\\r\\ndef solve(nums):\\r\\n s = [0] + nums\\r\\n for i in range(1, len(nums) + 1):\\r\\n s[i] += s[i - 1]\\r\\n```\\r\\n\\r\\n对于本例,可以算出前缀和数组,为了方便计算,我们往数组前部插入一个零,有了前缀数组,我们就可以使用 $sum(i, j) = s[j + 1] - s[i]$ 来计算任意区间的和了:\\r\\n```\\r\\ns = [0, -2, -2, 1, -4, -2, -3]\\r\\nsumRange(0, 2) = s[3] - s[0] = 1 - 0 = 1\\r\\nsumRange(2, 5) = s[6] - s[2] = -3 - (-2) = -1\\r\\n```\\r\\n\\r\\n可以看到,数组的区间信息可以压缩成更紧凑的形式。对于此例题中的静态数组,前缀和应对起来绰绰有余,但是如果在查询过程中数组的数据发生了变化,我们就不得不在每次变化的时候花上 $o(N)$ 的 时间开销去更新前缀数组,比如下面这道例题。\\r\\n\\r\\n>来源:[307. 区域和检索 - 数组可修改](https://leetcode-cn.com/problems/range-sum-query-mutable/)\\r\\n> 给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。\\r\\n> update(i, val) 函数可以通过将下标为 i 的数值更新为 val,从而对数列进行修改。\\r\\n\\r\\n\\r\\n前缀和的本质是维护区间信息,但在应对可变数组时的灵活性太差,所以我们使用更强大的线段树来解决这个问题。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/79983584-940f7780-84da-11ea-8882-a06db59b8056.png)\\r\\n\\r\\n上图表示了线段树的结构,线段树本质上是一个二叉树,它通过不断地把数组二分的方式来从根节点向叶子节点扩展,每个节点都维护了一个区间内的数组信息,这个数组信息可以是区间内的数组和以及最大最小值。\\r\\n\\r\\n```python\\r\\nclass Node:\\r\\n def __init__(self, l, r):\\r\\n self.l = l\\r\\n self.r = r\\r\\n self.lchild = None\\r\\n self.rchild = None\\r\\n self.val = 0\\r\\n\\r\\nclass NumArray:\\r\\n\\r\\n def __init__(self, nums: List[int]):\\r\\n self.nums = nums\\r\\n self.tree = self.build(0, len(nums) - 1)\\r\\n \\r\\n def build(self, l, r):\\r\\n if l > r: return None\\r\\n node = Node(l, r)\\r\\n if l == r:\\r\\n node.val = self.nums[l]\\r\\n return node\\r\\n m = (l + r) >> 1\\r\\n node.lchild = self.build(l, m)\\r\\n node.rchild = self.build(m + 1, r)\\r\\n node.val = node.lchild.val + node.rchild.val\\r\\n return node\\r\\n\\r\\n def update(self, i: int, val: int) -> None:\\r\\n d = val - self.nums[i]\\r\\n self.nums[i] = val\\r\\n q = [self.tree]\\r\\n while q:\\r\\n node = q.pop()\\r\\n if i >= node.l and i <= node.r: node.val += d\\r\\n else: continue\\r\\n for child in [node.lchild, node.rchild]:\\r\\n if child: q.append(child)\\r\\n\\r\\n def sumRange(self, i: int, j: int) -> int:\\r\\n ret = 0\\r\\n q = [self.tree]\\r\\n while q:\\r\\n node = q.pop()\\r\\n if not node: continue\\r\\n if node.l >= i and node.r <= j:\\r\\n ret += node.val\\r\\n elif node.l < j:\\r\\n q.append(node.lchild)\\r\\n elif node.r > i:\\r\\n q.append(node.rchild)\\r\\n return ret\\r\\n```\\r\\n\\r\\n[未完待续]","content":"
\\n

今天教你怎么在线性数组和二叉树间反复横跳。

\\n
\\n

目录:

\\n
    \\n
  • \\n
  • \\n
  • \\n
\\n

在最简单的一类数据结构中,有两种最为基础:线性数组和树,它们在实际的应用中十分常用,比如线性数组用来存储结构化数据,树用来存储一些有层级归属关系的数据。一个最典型的使用树的场景就是浏览器中的 GUI 渲染策略,在你阅读本篇文章的时候,浏览器就已经从 HTML 文件中解析出当前界面上的所有元素,并按照从属关系依次渲染出来,你只需要按下键盘上的 F12 键,就可以看到页面中所有元素的从属关系。

\\n

然而,一般意义上的树并不是高度结构化的数据,虽然一颗树的所有信息都可以由根节点遍历出来,但是在没有提前建立索引的情况下,你很难轻易地直接获取树中任意节点的数据。所以人们为了更高的随机索引速度,会更倾向于使用结构稳定的二叉树,尤其是完全二叉树,这是由于完全二叉树的左右子树高度差不超过 1,其每层的节点数量都是 2n2^n2n ,所以完全二叉树可以很轻松地存储在线性数组里,然后通过 child = tree[parent_index * 2] 的方式去递推任意孩子节点的索引值。

\\n

二叉树的这种特性使得树状结构可以很方便地存储在线性数组中,而本文就将阐述那些和线性数组关系紧密的树状数据结构们。

\\n

二叉堆

\\n

在使用二叉堆之前,你需要知道什么是堆,以及为什么要有堆。

\\n

根据维基百科的描述,堆是一种具有特殊顺序的树,也就是说,堆就是一种树,就像二叉搜索树那样,堆的节点间也有一些大小关系,最常用的堆是最大堆最小堆,又称大(小)顶堆、大(小)根堆等,以最大堆为例,最大堆中的任意一个节点都比它的子节点的值更大,也就是说,堆的根节点的元素是整个堆的最大值,值得注意的是,这种大小约束只对父节点和子节点生效,而相同层级的节点不需要有大小关系上的约束。

\\n

\\"image\\"

\\n

除此之外,堆还必须是一颗完全二叉树,这可以保证对堆的各种操作的均摊复杂度最低。

\\n

但是你可能会依然很疑惑,就像我在数据结构课上听到一个全新的数据结构的时候,心里会想:“wow, awesome, 然后呢?”,人们造出来拥有很多复杂结构的事物,并不是完全为了消磨时间,而是因为为了解决问题而不得不让这些工具变得这么复杂,本文中提到的三种数据结构也是如此,它们本身的特性使得它们在解决某些问题时异常好用。

\\n

而堆的最常用例子就是优先队列,你可以看到堆的根节点始终是最大值或者最小值,这种最值可以以优先级的形式体现出来,而且这种极值顺序会在动态增减的过程中始终以 O(logN)O(log N)O(logN) 的时间复杂度保持着,回想一下,如果你要对一个普通的线性数组取极大值,你就不得不付出 O(N)O(N)O(N) 的时间复杂度。堆的特性可以大大降低很多需要动态维护优先级的算法的复杂度,比如原始的迪杰斯特拉最短路径算法的时间复杂度是 O(N2)O(N^2)O(N2),而使用堆优化后可以达到 O(NlogN)O(N logN)O(NlogN)

\\n

现在你知道了什么是堆,以及堆用来解决那些问题,下面就介绍一下堆的常用操作,本文所述的堆均代表二叉堆。

\\n

二叉堆通常存储在一个数组中,而堆的插入(insert)、弹出(pop)、取堆顶(top)、删除(remove)操作均可以通过不断交换数组元素来完成,而这些操作均有两个最基础的操作组成:向上调整(up)和向下调整(down),顾名思义,向上调整就是从当前节点开始向根节点遍历,确认该路径上所有节点是否满足“子节点小于(大于)父节点”的顺序,如果不满足,则进行调整;而向下调整则是向叶子节点进行遍历,然后执行同样的操作,写成代码形式(以最大堆为例):

\\n
class Heap:\\n  def __init__(self):\\n    self.heap = []\\n\\n  def swap(self, i, j):\\n    self.heap[i], self.heap[j] = self.heap[j], self.heap[i]\\n\\n  def up(self, k):\\n    '''向上调整第 k 个节点'''\\n    while k > 0:\\n        parent_index = (k - 1) // 2\\n        # 如果子节点大于父节点,则进行调整\\n        if self.heap[k] > self.heap[parent_index]:\\n          self.swap(k, parent_index)\\n        else: break\\n\\n  def down(self, k)\\n    '''向下调整第 k 个节点'''\\n    while k < len(self.heap):\\n      lchild, rchild = k * 2 + 1, k * 2 + 2\\n      # 两个子节点取较大节点\\n      child = lchild if self.heap[lchild] > self.heap[rchild] or rchild > len(self.heap) else rchild\\n      if child < len(self.heap) and self.heap[child] > self.heap[k]:\\n        self.swap(child, k)\\n        k = child\\n      else: break\\n
\\n

可以看到,调整部分的代码的逻辑还是比较清晰的,只需要不断向上或者向下遍历,然后调整对应的值即可。有了两个基础操作,我们可以很方便地完成插入、弹出、取顶、删除等操作了,直接看代码:

\\n
class Heap:\\n  '''省略 up, down 部分的代码'''\\n  def insert(self, val):\\n    '''将 val 插入堆中'''\\n    self.heap.append(val) # 插到数组尾部\\n    self.up(len(self.heap) - 1) # 然后不断向上调整即可\\n\\n  def top(self):\\n    '''获取堆顶值'''\\n    return None if len(self.heap) == 0 else self.heap[0] # 只需返回数组中的第一个元素即可\\n\\n  def remove(self, k):\\n    '''移除第 k 个节点'''\\n    self.swap(k, len(self.heap) - 1) # 将第 k 个元素交换到尾部\\n    ret = self.heap.pop() # 将其从数组中删除\\n    self.up(k) # 确认是否需要向上调整\\n    self.down(k) # 确认是否需要向下调整\\n    return ret\\n\\n  def pop(self):\\n    '''弹出堆顶'''\\n    return self.remove(0)\\n
\\n

掌握了二叉堆,我们可以很轻松地解决 TOP-k 问题,以及所有需要使用到优先队列的问题,不过值得一提的是,二叉堆作为一种非常常用的数据结构,已经被内置到很多语言的官方库中,在实际使用或者写算法题时,可以直接调包使用,比如 Python3 的 queue.PriorityQueueheapq,以及 C++ 的 priority_queue 等。

\\n

线段树和树状数组

\\n

线段树和树状数组非常像,可以说树状数组就是线段树的一种简化形式,在应对单点修改的区间问题时,树状数组更为简洁好用,但由于使用了 low-bit 技巧,相对来说并没有线段树容易理解,所以本文就先从线段树的原理讲起,再逐步扩展到树状数组。

\\n

首先,我们用一个例题来作为切入概念:

\\n
\\n

来源:303. 区域和检索 - 数组不可变\\n给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。

\\n
\\n

下面是一些示例,给定 nums = [-2, 0, 3, -5, 2, -1],那么对任意的 (i, j) 进行求和:

\\n
sumRange(0, 2) -> 1\\nsumRange(2, 5) -> -1\\nsumRange(0, 5) -> -3\\n
\\n

我们可以很容易地想到 o(N)o(N)o(N) 的解法,直接对区间 [i...j][i ... j][i...j] 的数进行求和即可,但是当查询次数很大时,很容易就会超时,而如果我们使用前缀和数组,就可以轻松地在 o(1)o(1)o(1) 时间内完成任何查询操作,所谓的前缀和数组,就是把前 iii 位的数字加起来作为第 iii 位的元素值,即 s[i]=j=0iais[i] = \\\\sum_{j=0}^i a_is[i]=j=0iai,代码表示如下:

\\n
def solve(nums):\\n  s = [0] + nums\\n  for i in range(1, len(nums) + 1):\\n    s[i] += s[i - 1]\\n
\\n

对于本例,可以算出前缀和数组,为了方便计算,我们往数组前部插入一个零,有了前缀数组,我们就可以使用 sum(i,j)=s[j+1]s[i]sum(i, j) = s[j + 1] - s[i]sum(i,j)=s[j+1]s[i] 来计算任意区间的和了:

\\n
s = [0, -2, -2, 1, -4, -2, -3]\\nsumRange(0, 2) = s[3] - s[0] = 1 - 0 = 1\\nsumRange(2, 5) = s[6] - s[2] = -3 - (-2) = -1\\n
\\n

可以看到,数组的区间信息可以压缩成更紧凑的形式。对于此例题中的静态数组,前缀和应对起来绰绰有余,但是如果在查询过程中数组的数据发生了变化,我们就不得不在每次变化的时候花上 o(N)o(N)o(N) 的 时间开销去更新前缀数组,比如下面这道例题。

\\n
\\n

来源:307. 区域和检索 - 数组可修改\\n给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。\\nupdate(i, val) 函数可以通过将下标为 i 的数值更新为 val,从而对数列进行修改。

\\n
\\n

前缀和的本质是维护区间信息,但在应对可变数组时的灵活性太差,所以我们使用更强大的线段树来解决这个问题。

\\n

\\"image\\"

\\n

上图表示了线段树的结构,线段树本质上是一个二叉树,它通过不断地把数组二分的方式来从根节点向叶子节点扩展,每个节点都维护了一个区间内的数组信息,这个数组信息可以是区间内的数组和以及最大最小值。

\\n
class Node:\\n    def __init__(self, l, r):\\n        self.l = l\\n        self.r = r\\n        self.lchild = None\\n        self.rchild = None\\n        self.val = 0\\n\\nclass NumArray:\\n\\n    def __init__(self, nums: List[int]):\\n        self.nums = nums\\n        self.tree = self.build(0, len(nums) - 1)\\n \\n    def build(self, l, r):\\n        if l > r: return None\\n        node = Node(l, r)\\n        if l == r:\\n            node.val = self.nums[l]\\n            return node\\n        m = (l + r) >> 1\\n        node.lchild = self.build(l, m)\\n        node.rchild = self.build(m + 1, r)\\n        node.val = node.lchild.val + node.rchild.val\\n        return node\\n\\n    def update(self, i: int, val: int) -> None:\\n        d = val - self.nums[i]\\n        self.nums[i] = val\\n        q = [self.tree]\\n        while q:\\n            node = q.pop()\\n            if i >= node.l and i <= node.r: node.val += d\\n            else: continue\\n            for child in [node.lchild, node.rchild]:\\n                if child: q.append(child)\\n\\n    def sumRange(self, i: int, j: int) -> int:\\n        ret = 0\\n        q = [self.tree]\\n        while q:\\n            node = q.pop()\\n            if not node: continue\\n            if node.l >= i and node.r <= j:\\n                ret += node.val\\n            elif node.l < j:\\n                q.append(node.lchild)\\n            elif node.r > i:\\n                q.append(node.rchild)\\n        return ret\\n
\\n

[未完待续]

\\n"},"29":{"id":29,"date":"2020/05/24","author":"Yidadaa","title":"随机采样一致性与特征图匹配","mdContent":"Rocco[1] 等人在其弱监督语义级别图像匹配的工作中,将特征匹配与随机采样一致性算法(RANdom SAmple Consensus, RANSAC)联系在一起,提出了一个可微分的基于语义的评分损失函数,文中对于语义特征匹配和 RANSAC 算法的阐述令人耳目一新,遂作此文对相关概念追本溯源。\\r\\n\\r\\n## 随机采样一致性(RANSAC)\\r\\n真实世界的数据往往充满各种各样的噪声,如果想得到足够鲁棒的适用于所有场景的模型,就要尽量降低噪声的影响。由 Fischler[3] 等人提出的随机采样一致性算法,可以很好从含有大量离群点和噪音地数据中恢复出足够准确地模型参数。相对于在统计学领域中大量使用的 M 估计(M-estimators)和最小均方回归(Least Median Square Regression)等稳健估计方法(Robust Estimation),RANSAC 则主要应用于计算机视觉领域,而且从 Fichler[3] 的论文就可以看出,RANSAC 算法从诞生之初便和图像匹配存在不解之缘。\\r\\n\\r\\n简略来看,RANSAC 从所有数据中采样尽量少的观测点来估计模型参数,而不是像其他依赖于从尽可能多的数据中过滤掉离群点,RANSCAC 使用一组足够少的点来初始化算法,然后不断地使用迭代式方法来增大具有**数据一致性**的可信点规模。\\r\\n\\r\\n算法的流程可以简要地描述如下:\\r\\n1. 从数据样本 $S$ 中**随机**选取 $K$ 个点,得到假设子集 $S_K$,然后使用 $S_K$ 优化模型参数 $M(\\\\theta)$,得到初始模型;\\r\\n2. 对所有数据样本 $S$ 应用模型 $M(\\\\theta)$,然后根据每个数据样本的误差 $||M(\\\\theta, x_i) - y_i||$ 来所有数据样本划分为内群点(inliner points)和离群点(outliner points),其中误差大于超参数 $\\\\epsilon$ 的称为离群点 $S_{outliner}$,反正则为内群点 $S_{inliner}$;\\r\\n3. 如果内群点的占比 $R = \\\\frac{S_{inliner}}{S}$ 大于超参数阈值 $\\\\tau$,则使用所有的内群点 $S_{inliner}$ 来重新优化模型参数,并将得到的模型作为最终模型,终止迭代;\\r\\n4. 否则使用内群点 $S_{inliner}$ 优化模型,并重复执行步骤 2,直到条件 3 满足,或者达到迭代次数上限 $N$。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 1 RANSAC 迭代过程演示

\\r\\n\\r\\n\\r\\n\\r\\n可以看到 RANSAC 算法的性能由超参数 $N, K, \\\\epsilon, \\\\tau$ 来决定,通常来讲,迭代次数 $N$ 必须设置的足够大,才能使得至少有一次数据采样 $S_K$ 不包含任何离群点的概率 $p$ (一般来说 $p = 0.99$)满足我们的使用要求,以此来避免离群点的影响,我们可以通过简单地推导来得到迭代上限的近似估计。\\r\\n\\r\\n假设每次随机选择一个点,该点为内群点的概率为 $u$,则其为离群点的概率为 $v = 1 - u$,则在 $N$ 次迭代,每次迭代选取 $K$ 个点的情况下,有\\r\\n$$1 - p = (1 - u^K)^N$$\\r\\n从而估计出\\r\\n$$N = \\\\frac{log(1 - p)}{log(1 - (1 - v)^K)}$$\\r\\n\\r\\n举个粒子,假设数据总量 $1000$,有大概 $100$ 个离群点,则有 $u = 0.9, v = 0.1$,每次迭代选取 $K = 200$ 个点来估计参数,则迭代上限应该为 $N = log(1 - 0.99) \\\\div log(1 - 0.9^{200}) \\\\approx 21076$ 次。\\r\\n\\r\\n此外,RANSAC 算法还有很多变种,比如在估计离群点时应用极大似然估计的 MLESAC[4],每次迭代采样时使用权重采样的 IMPSAC[5]。\\r\\n\\r\\n## 图像特征匹配(Image Feature Matching)\\r\\n图像特征匹配是计算机视觉中的经典任务,无论是做图像检索、三维重建,还是做相机重定位或者 SLAM,都需要先从图像中提取出匹配好的特征点对。以两幅图像为例,图像特征匹配一般包含两个阶段[6]:\\r\\n\\r\\n1. 从图像对 $(I_s, I_t)$ 中分别提取特征点 $(F_s, F_t)$;\\r\\n2. 对两个特征点集进行匹配,得到匹配完毕的特征对 $M_{s \\\\rightarrow t}$。\\r\\n\\r\\n其中第一步提取的特征点,可以是经典的手动构造的特征描述子 SIFT、SURF 或者 ORB 等,也可以是由神经网络端到端地生成特征描述子,比如 LIFT[7]、DELF[8] 以及 D2-Net[9] 等。然后对得到的特征点进行特征匹配,常见的策略有 opencv 自带的 Brute-Force 暴力匹配和快速近似 k 近邻匹配[10]、朴素最近邻匹配、双向最近邻(或称 cycle consistent)等匹配方法。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 2 图像匹配的一般流程[11]

\\r\\n\\r\\n在进行特征点匹配时,为了保证匹配的鲁棒性,需要尽可能多得降低离群点以及错误匹配的影响,此时就要结合前文中提到的 RANSAC 算法过滤掉离群点。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n\\r\\n

图 3 使用 RANSAC 过滤离群点。A 和 B 中的红色点表示在另一幅图中不存在对应的特征点,蓝色点表示存在对应的特征点,但是没有得到完美匹配,黄色的点表示匹配成功。

\\r\\n\\r\\n考虑同一个场景下的图像匹配,如图 3 所示,两张图片在同一场景的不同视角进行拍摄,因此我们如果想要将两张图片对齐,就要求解出两个视角的单应矩阵 $H$,由于我们选用的特征描述子不可能总是完美地表示原始图片中的视觉特征,所以我们在使用汉明距离或者 $L_2$ 距离 $D$ 应用 k 紧邻或者暴力匹配算法时,总是会产生错误匹配,也就是图 3 中的蓝色点,那么此时在所有特征点对 $M_{s \\\\rightarrow t}$ 中,蓝色的匹配就是离群点,我们的目标便是使用 RANSAC 算法来降低这些离群点的影响[12]:\\r\\n1. 从 $M_{s \\\\rightarrow t}$ 中随机选取 $k$ 个特征点对 $M_k$;\\r\\n2. 使用 $M_k$ 求解单应矩阵 $H$;\\r\\n3. 对图像 $I_s$ 中的所有特征点 $F_s$ 应用单应矩阵 $H$,将其投影到图像 $I_s$ 的成像坐标系中,得到投影后的特征点 $\\\\hat F_s = Project(H, F_s)$;\\r\\n4. 根据预设定阈值 $\\\\epsilon$ 计算内群点,即满足 $D(\\\\hat{F_s}, M_{s \\\\rightarrow t}F_s) \\\\leq \\\\epsilon$ 的匹配;\\r\\n5. 如果内群点占比不满足条件,则重复上述步骤 2 - 4;否则使用当前的所有内群点对重新计算 $H$ 作为最终结果。\\r\\n\\r\\n结合了 RANSAC 的改进算法和各种改良后的特征描述子的特征匹配算法的性能已经足够良好[6],并且能够稳定地胜任 SLAM、三维重建和位姿估计等视觉任务。然而上述算法却无法直接应用到神经网络中,这是因为每次采样选点的过程都是不可微分的,这与梯度下降法的使用场景相悖。此外,经典的视觉特征描述子也很难描述出图像中物体的语义特征,因此,可微分的 RANSAC 算法应运而生,而随着深度学习的兴起,语义层面的特征匹配方法也层出不穷。\\r\\n\\r\\n## 语义特征匹配\\r\\n前文提到,在大多数计算机视觉任务中,我们只需要找到同一个场景中在不同视角拍摄的图片之间的响应或者匹配,就可以完成三维重建或者相机重定位等任务,然而有时候我们需要对不同场景的图像进行语义级别的匹配,比如当场景中存在动态物体时,传统匹配方法无法灵活处理动态部分的场景,这时候就需要我们使用语义级别的特征匹配方法。\\r\\n

\\r\\n \\r\\n

\\r\\n

图 4 语义级别的图像匹配[13]

\\r\\n\\r\\n为了解决传统特征描述子的描述能力受限的问题,人们首先考虑从深度学习学到的特征开始入手,使用各种各样的 CNN 学习到的特征描述子进行特征匹配[14],比如使用物体级别的框体匹配(learned at the object-proposal level)的 SC-NET [15][16],以及直接使用手工标注的特征响应的有监督的方法 [17]。\\r\\n\\r\\n首个弱监督的端到端的语义特征匹配方法是 Rocco 的这篇文章[1],作者声称从经典的 RANSAC 算法中得到启发,直接从特征相关矩阵中学习特征相似度较大的区域,从而学习到密集的特征响应信号,同时为了使得整个流程可以微分,使用了 Spatial Transformer Networks 中的图像变换模型来执行图像采样。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 5 Rocco[1] 提出的端到端的特征特征匹配流程

\\r\\n\\r\\n前文提到,图像匹配的流程一般包含两个步骤,首先是特征提取,其次是特征匹配。Rocco[1] 的 pipeline 使用一个参数共享的孪生 CNN 来提取原始图像的特征图,从而得到图像对的特征图 $(F_s, F_t)$,然后对于特征匹配部分,则使用一个图像变换参数估计模型 $G$ 来学习 $F_s \\\\rightarrow F_t$ 的仿射变换矩阵 $T$ 的参数 $g$,这也是整个流程的核心所在,那么如何得到图像变换参数估计模型 $G$ 的监督信号呢?我们还要先从特征图的相关矩阵(correlation map)说起,相关矩阵也是计算机视觉中注意力机制[19][20]的基础,对特征图 $(F_s, F_t)$ 计算相关矩阵:\\r\\n\\r\\n$$\\r\\nV_s = Flatten(F_s), V_t = Flatten(F_t); R^{h \\\\times w \\\\times c} \\\\rightarrow R^{hw \\\\times c}\\r\\n$$\\r\\n\\r\\n$$C = Softmax(V_sV_t^T); R^{hw \\\\times c} \\\\times R^{c \\\\times hw} \\\\rightarrow R^{hw \\\\times hw} $$\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 6 相关性矩阵的计算

\\r\\n\\r\\n直观上理解相关性矩阵的计算,就是用特征图中位于 $(i, j)$ 处的长度为 $C$ 的向量 $F_{(i, j)}$ 来描述位置 $(i, j)$ 处的特征,先将维度为 $h\\\\times w \\\\times c$ 的特征图重整(reshape)为维度为 $hw \\\\times c$ 的向量矩阵,然后使用向量乘法与 Softmax 来计算一个相似性度量,可以看到这个相似性度量与余弦距离在形式上较为接近,这样可以直观地将相关矩阵 $C$ 理解为描述两张特征图中任意两个位置的特征向量之间的相似性。\\r\\n\\r\\n可以发现,特征图 $F_s$ 与 $F_t$ 的相似性越高,相关矩阵 $C$ 的对角线上的相似性得分就越高,回顾图 1 中我们用直线拟合来介绍 RANSAC 的例子,可以发现这两个问题有着形式上的相似性,自然而然地,我们便可以使用相关矩阵 $C$ 对角线上的得分来作为两张特征图的相似性度量得分,而我们想要学习得到的图像变换模型 $G$ 的目标函数就可以描述为 $L_g = Score(C(F_s · G_\\\\theta (F_s, F_t), F_t))$。\\r\\n\\r\\n为了增加鲁棒性,不仅考虑 $C$ 的对角线上的得分,而且考虑对角线周边的区域得分,所以 Rocco[1] 来使用一个以对角线为中心的掩层(mask)来框选得分区域,就是图 5 中的 $m$。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 7 学习图像变换模型的过程就是 RANSAC 采样的过程

\\r\\n\\r\\n如果我们把相关矩阵 $C$ 中掩层区域外的值看作离群点,掩层内的值看错内群点,而图像变换参数估计模型 $G$ 估计出的参数则用来对 $F_s$ 进行采样,可以发现,优化目标函数 $L_g$ 的过程其实与前文中提到的 RANSAC 过程是一致的,我们使用掩层内的值不断地优化模型 $G$,就相当于 RANSAC 中使用内群点不断地优化模型参数,从而学习到更加鲁棒的模型。\\r\\n\\r\\n到此为止,图像变换矩阵 $G$ 便描述了特征图 $F_s$ 与 $F_t$ 之间的特征响应,而且得益于 CNN 的感受野,这种特征响应也描述了两张图像在语义层面上的相关性。\\r\\n\\r\\n## 扩展应用\\r\\n相较于直接使用 $L_1$ 或者 $L_2$ 等误差函数,前文提到的 $L_g$ 可以更好得描述特征图间得语义相关性,这使得其可以在图像匹配之外的场景中应用,比如 Wang[21] 就将其应用在挖掘视频帧间响应的任务中,以无监督的形式在多种视觉任务上得到了与监督学习匹敌的性能数据。其他诸如在动态场景的任务,如 SLAM、视频深度估计或者三维重建中也有较大的应用潜力。\\r\\n\\r\\n## 参考资料\\r\\n1. Rocco, Ignacio, Relja Arandjelović, and Josef Sivic. \\"End-to-end weakly-supervised semantic alignment.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2018.\\r\\n2. Derpanis, Konstantinos G. \\"Overview of the RANSAC Algorithm.\\" Image Rochester NY 4.1 (2010): 2-3.\\r\\n3. Fischler, Martin A., and Robert C. Bolles. \\"Random sample consensus: a paradigm for model fitting with applications to image analysis and automated cartography.\\" Communications of the ACM 24.6 (1981): 381-395.Communications of the ACM, 24(6):381–395, 1981.\\r\\n4. Torr, Philip HS, and Andrew Zisserman. \\"MLESAC: A new robust estimator with application to estimating image geometry.\\" Computer vision and image understanding 78.1 (2000): 138-156.\\r\\n5. Torr, Philip H. S., and Colin Davidson. \\"IMPSAC: Synthesis of importance sampling and random sample consensus.\\" IEEE Transactions on Pattern Analysis and Machine Intelligence 25.3 (2003): 354-364.\\r\\n6. Jin, Yuhe, et al. \\"Image Matching across Wide Baselines: From Paper to Practice.\\" arXiv preprint arXiv:2003.01587 (2020).\\r\\n7. Yi, Kwang Moo, et al. \\"Lift: Learned invariant feature transform.\\" European Conference on Computer Vision. Springer, Cham, 2016.\\r\\n8. Noh, Hyeonwoo, et al. \\"Large-scale image retrieval with attentive deep local features.\\" Proceedings of the IEEE international conference on computer vision. 2017.\\r\\n9. Dusmanu, Mihai, et al. \\"D2-Net: A Trainable CNN for Joint Description and Detection of Local Features.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.\\r\\n10. “Feature Matching.” OpenCV, opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_feature2d/py_matcher/py_matcher.html.\\r\\n11. http://cmp.felk.cvut.cz/~mishkdmy/slides/EECVC2019_Mishkin_image_matching.pdf\\r\\n12. https://people.cs.umass.edu/~elm/Teaching/ppt/370/370_10_RANSAC.pptx.pdf\\r\\n13. Chen, Yun-Chun, et al. \\"Deep semantic matching with foreground detection and cycle-consistency.\\" Asian Conference on Computer Vision. Springer, Cham, 2018.\\r\\n14. Ufer, Nikolai, and Bjorn Ommer. \\"Deep semantic feature matching.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.\\r\\n15. Han, Kai, et al. \\"Scnet: Learning semantic correspondence.\\" Proceedings of the IEEE International Conference on Computer Vision. 2017.\\r\\n16. Ufer, Nikolai, and Bjorn Ommer. \\"Deep semantic feature matching.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.\\r\\n17. Rocco, Ignacio, Relja Arandjelovic, and Josef Sivic. \\"Convolutional neural network architecture for geometric matching.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.\\r\\n18. Jaderberg, Max, Karen Simonyan, and Andrew Zisserman. \\"Spatial transformer networks.\\" Advances in neural information processing systems. 2015.\\r\\n19. Vaswani, Ashish, et al. \\"Attention is all you need.\\" Advances in neural information processing systems. 2017.\\r\\n20. Wang, Xiaolong, et al. \\"Non-local neural networks.\\" Proceedings of the IEEE conference on computer vision and pattern recognition. 2018.\\r\\n21. Wang, Xiaolong, Allan Jabri, and Alexei A. Efros. \\"Learning correspondence from the cycle-consistency of time.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.","content":"

Rocco[1] 等人在其弱监督语义级别图像匹配的工作中,将特征匹配与随机采样一致性算法(RANdom SAmple Consensus, RANSAC)联系在一起,提出了一个可微分的基于语义的评分损失函数,文中对于语义特征匹配和 RANSAC 算法的阐述令人耳目一新,遂作此文对相关概念追本溯源。

\\n

随机采样一致性(RANSAC)

\\n

真实世界的数据往往充满各种各样的噪声,如果想得到足够鲁棒的适用于所有场景的模型,就要尽量降低噪声的影响。由 Fischler[3] 等人提出的随机采样一致性算法,可以很好从含有大量离群点和噪音地数据中恢复出足够准确地模型参数。相对于在统计学领域中大量使用的 M 估计(M-estimators)和最小均方回归(Least Median Square Regression)等稳健估计方法(Robust Estimation),RANSAC 则主要应用于计算机视觉领域,而且从 Fichler[3] 的论文就可以看出,RANSAC 算法从诞生之初便和图像匹配存在不解之缘。

\\n

简略来看,RANSAC 从所有数据中采样尽量少的观测点来估计模型参数,而不是像其他依赖于从尽可能多的数据中过滤掉离群点,RANSCAC 使用一组足够少的点来初始化算法,然后不断地使用迭代式方法来增大具有数据一致性的可信点规模。

\\n

算法的流程可以简要地描述如下:

\\n
    \\n
  1. 从数据样本 SSS随机选取 KKK 个点,得到假设子集 SKS_KSK,然后使用 SKS_KSK 优化模型参数 M(θ)M(\\\\theta)M(θ),得到初始模型;
  2. \\n
  3. 对所有数据样本 SSS 应用模型 M(θ)M(\\\\theta)M(θ),然后根据每个数据样本的误差 M(θ,xi)yi||M(\\\\theta, x_i) - y_i||M(θ,xi)yi 来所有数据样本划分为内群点(inliner points)和离群点(outliner points),其中误差大于超参数 ϵ\\\\epsilonϵ 的称为离群点 SoutlinerS_{outliner}Soutliner,反正则为内群点 SinlinerS_{inliner}Sinliner
  4. \\n
  5. 如果内群点的占比 R=SinlinerSR = \\\\frac{S_{inliner}}{S}R=SSinliner 大于超参数阈值 τ\\\\tauτ,则使用所有的内群点 SinlinerS_{inliner}Sinliner 来重新优化模型参数,并将得到的模型作为最终模型,终止迭代;
  6. \\n
  7. 否则使用内群点 SinlinerS_{inliner}Sinliner 优化模型,并重复执行步骤 2,直到条件 3 满足,或者达到迭代次数上限 NNN
  8. \\n
\\n

\\n \\n

\\n

图 1 RANSAC 迭代过程演示

\\n

可以看到 RANSAC 算法的性能由超参数 N,K,ϵ,τN, K, \\\\epsilon, \\\\tauN,K,ϵ,τ 来决定,通常来讲,迭代次数 NNN 必须设置的足够大,才能使得至少有一次数据采样 SKS_KSK 不包含任何离群点的概率 ppp (一般来说 p=0.99p = 0.99p=0.99)满足我们的使用要求,以此来避免离群点的影响,我们可以通过简单地推导来得到迭代上限的近似估计。

\\n

假设每次随机选择一个点,该点为内群点的概率为 uuu,则其为离群点的概率为 v=1uv = 1 - uv=1u,则在 NNN 次迭代,每次迭代选取 KKK 个点的情况下,有

\\n

1p=(1uK)N1 - p = (1 - u^K)^N\\n1p=(1uK)N

\\n

从而估计出

\\n

N=log(1p)log(1(1v)K)N = \\\\frac{log(1 - p)}{log(1 - (1 - v)^K)}\\nN=log(1(1v)K)log(1p)

\\n

举个粒子,假设数据总量 100010001000,有大概 100100100 个离群点,则有 u=0.9,v=0.1u = 0.9, v = 0.1u=0.9,v=0.1,每次迭代选取 K=200K = 200K=200 个点来估计参数,则迭代上限应该为 N=log(10.99)÷log(10.9200)21076N = log(1 - 0.99) \\\\div log(1 - 0.9^{200}) \\\\approx 21076N=log(10.99)÷log(10.9200)21076 次。

\\n

此外,RANSAC 算法还有很多变种,比如在估计离群点时应用极大似然估计的 MLESAC[4],每次迭代采样时使用权重采样的 IMPSAC[5]。

\\n

图像特征匹配(Image Feature Matching)

\\n

图像特征匹配是计算机视觉中的经典任务,无论是做图像检索、三维重建,还是做相机重定位或者 SLAM,都需要先从图像中提取出匹配好的特征点对。以两幅图像为例,图像特征匹配一般包含两个阶段[6]:

\\n
    \\n
  1. 从图像对 (Is,It)(I_s, I_t)(Is,It) 中分别提取特征点 (Fs,Ft)(F_s, F_t)(Fs,Ft)
  2. \\n
  3. 对两个特征点集进行匹配,得到匹配完毕的特征对 MstM_{s \\\\rightarrow t}Mst
  4. \\n
\\n

其中第一步提取的特征点,可以是经典的手动构造的特征描述子 SIFT、SURF 或者 ORB 等,也可以是由神经网络端到端地生成特征描述子,比如 LIFT[7]、DELF[8] 以及 D2-Net[9] 等。然后对得到的特征点进行特征匹配,常见的策略有 opencv 自带的 Brute-Force 暴力匹配和快速近似 k 近邻匹配[10]、朴素最近邻匹配、双向最近邻(或称 cycle consistent)等匹配方法。

\\n

\\n \\n

\\n

图 2 图像匹配的一般流程[11]

\\n

在进行特征点匹配时,为了保证匹配的鲁棒性,需要尽可能多得降低离群点以及错误匹配的影响,此时就要结合前文中提到的 RANSAC 算法过滤掉离群点。

\\n

\\n \\n

\\n

图 3 使用 RANSAC 过滤离群点。A 和 B 中的红色点表示在另一幅图中不存在对应的特征点,蓝色点表示存在对应的特征点,但是没有得到完美匹配,黄色的点表示匹配成功。

\\n

考虑同一个场景下的图像匹配,如图 3 所示,两张图片在同一场景的不同视角进行拍摄,因此我们如果想要将两张图片对齐,就要求解出两个视角的单应矩阵 HHH,由于我们选用的特征描述子不可能总是完美地表示原始图片中的视觉特征,所以我们在使用汉明距离或者 L2L_2L2 距离 DDD 应用 k 紧邻或者暴力匹配算法时,总是会产生错误匹配,也就是图 3 中的蓝色点,那么此时在所有特征点对 MstM_{s \\\\rightarrow t}Mst 中,蓝色的匹配就是离群点,我们的目标便是使用 RANSAC 算法来降低这些离群点的影响[12]:

\\n
    \\n
  1. MstM_{s \\\\rightarrow t}Mst 中随机选取 kkk 个特征点对 MkM_kMk
  2. \\n
  3. 使用 MkM_kMk 求解单应矩阵 HHH
  4. \\n
  5. 对图像 IsI_sIs 中的所有特征点 FsF_sFs 应用单应矩阵 HHH,将其投影到图像 IsI_sIs 的成像坐标系中,得到投影后的特征点 F^s=Project(H,Fs)\\\\hat F_s = Project(H, F_s)F^s=Project(H,Fs)
  6. \\n
  7. 根据预设定阈值 ϵ\\\\epsilonϵ 计算内群点,即满足 D(Fs^,MstFs)ϵD(\\\\hat{F_s}, M_{s \\\\rightarrow t}F_s) \\\\leq \\\\epsilonD(Fs^,MstFs)ϵ 的匹配;
  8. \\n
  9. 如果内群点占比不满足条件,则重复上述步骤 2 - 4;否则使用当前的所有内群点对重新计算 HHH 作为最终结果。
  10. \\n
\\n

结合了 RANSAC 的改进算法和各种改良后的特征描述子的特征匹配算法的性能已经足够良好[6],并且能够稳定地胜任 SLAM、三维重建和位姿估计等视觉任务。然而上述算法却无法直接应用到神经网络中,这是因为每次采样选点的过程都是不可微分的,这与梯度下降法的使用场景相悖。此外,经典的视觉特征描述子也很难描述出图像中物体的语义特征,因此,可微分的 RANSAC 算法应运而生,而随着深度学习的兴起,语义层面的特征匹配方法也层出不穷。

\\n

语义特征匹配

\\n

前文提到,在大多数计算机视觉任务中,我们只需要找到同一个场景中在不同视角拍摄的图片之间的响应或者匹配,就可以完成三维重建或者相机重定位等任务,然而有时候我们需要对不同场景的图像进行语义级别的匹配,比如当场景中存在动态物体时,传统匹配方法无法灵活处理动态部分的场景,这时候就需要我们使用语义级别的特征匹配方法。

\\n

\\n \\n

\\n

图 4 语义级别的图像匹配[13]

\\n

为了解决传统特征描述子的描述能力受限的问题,人们首先考虑从深度学习学到的特征开始入手,使用各种各样的 CNN 学习到的特征描述子进行特征匹配[14],比如使用物体级别的框体匹配(learned at the object-proposal level)的 SC-NET [15][16],以及直接使用手工标注的特征响应的有监督的方法 [17]。

\\n

首个弱监督的端到端的语义特征匹配方法是 Rocco 的这篇文章[1],作者声称从经典的 RANSAC 算法中得到启发,直接从特征相关矩阵中学习特征相似度较大的区域,从而学习到密集的特征响应信号,同时为了使得整个流程可以微分,使用了 Spatial Transformer Networks 中的图像变换模型来执行图像采样。

\\n

\\n \\n

\\n

图 5 Rocco[1] 提出的端到端的特征特征匹配流程

\\n

前文提到,图像匹配的流程一般包含两个步骤,首先是特征提取,其次是特征匹配。Rocco[1] 的 pipeline 使用一个参数共享的孪生 CNN 来提取原始图像的特征图,从而得到图像对的特征图 (Fs,Ft)(F_s, F_t)(Fs,Ft),然后对于特征匹配部分,则使用一个图像变换参数估计模型 GGG 来学习 FsFtF_s \\\\rightarrow F_tFsFt 的仿射变换矩阵 TTT 的参数 ggg,这也是整个流程的核心所在,那么如何得到图像变换参数估计模型 GGG 的监督信号呢?我们还要先从特征图的相关矩阵(correlation map)说起,相关矩阵也是计算机视觉中注意力机制[19][20]的基础,对特征图 (Fs,Ft)(F_s, F_t)(Fs,Ft) 计算相关矩阵:

\\n

Vs=Flatten(Fs),Vt=Flatten(Ft);Rh×w×cRhw×cV_s = Flatten(F_s), V_t = Flatten(F_t); R^{h \\\\times w \\\\times c} \\\\rightarrow R^{hw \\\\times c}\\nVs=Flatten(Fs),Vt=Flatten(Ft);Rh×w×cRhw×c

\\n

C=Softmax(VsVtT);Rhw×c×Rc×hwRhw×hwC = Softmax(V_sV_t^T); R^{hw \\\\times c} \\\\times R^{c \\\\times hw} \\\\rightarrow R^{hw \\\\times hw} \\nC=Softmax(VsVtT);Rhw×c×Rc×hwRhw×hw

\\n

\\n \\n

\\n

图 6 相关性矩阵的计算

\\n

直观上理解相关性矩阵的计算,就是用特征图中位于 (i,j)(i, j)(i,j) 处的长度为 CCC 的向量 F(i,j)F_{(i, j)}F(i,j) 来描述位置 (i,j)(i, j)(i,j) 处的特征,先将维度为 h×w×ch\\\\times w \\\\times ch×w×c 的特征图重整(reshape)为维度为 hw×chw \\\\times chw×c 的向量矩阵,然后使用向量乘法与 Softmax 来计算一个相似性度量,可以看到这个相似性度量与余弦距离在形式上较为接近,这样可以直观地将相关矩阵 CCC 理解为描述两张特征图中任意两个位置的特征向量之间的相似性。

\\n

可以发现,特征图 FsF_sFsFtF_tFt 的相似性越高,相关矩阵 CCC 的对角线上的相似性得分就越高,回顾图 1 中我们用直线拟合来介绍 RANSAC 的例子,可以发现这两个问题有着形式上的相似性,自然而然地,我们便可以使用相关矩阵 CCC 对角线上的得分来作为两张特征图的相似性度量得分,而我们想要学习得到的图像变换模型 GGG 的目标函数就可以描述为 Lg=Score(C(FsGθ(Fs,Ft),Ft))L_g = Score(C(F_s · G_\\\\theta (F_s, F_t), F_t))Lg=Score(C(FsGθ(Fs,Ft),Ft))

\\n

为了增加鲁棒性,不仅考虑 CCC 的对角线上的得分,而且考虑对角线周边的区域得分,所以 Rocco[1] 来使用一个以对角线为中心的掩层(mask)来框选得分区域,就是图 5 中的 mmm

\\n

\\n \\n

\\n

图 7 学习图像变换模型的过程就是 RANSAC 采样的过程

\\n

如果我们把相关矩阵 CCC 中掩层区域外的值看作离群点,掩层内的值看错内群点,而图像变换参数估计模型 GGG 估计出的参数则用来对 FsF_sFs 进行采样,可以发现,优化目标函数 LgL_gLg 的过程其实与前文中提到的 RANSAC 过程是一致的,我们使用掩层内的值不断地优化模型 GGG,就相当于 RANSAC 中使用内群点不断地优化模型参数,从而学习到更加鲁棒的模型。

\\n

到此为止,图像变换矩阵 GGG 便描述了特征图 FsF_sFsFtF_tFt 之间的特征响应,而且得益于 CNN 的感受野,这种特征响应也描述了两张图像在语义层面上的相关性。

\\n

扩展应用

\\n

相较于直接使用 L1L_1L1 或者 L2L_2L2 等误差函数,前文提到的 LgL_gLg 可以更好得描述特征图间得语义相关性,这使得其可以在图像匹配之外的场景中应用,比如 Wang[21] 就将其应用在挖掘视频帧间响应的任务中,以无监督的形式在多种视觉任务上得到了与监督学习匹敌的性能数据。其他诸如在动态场景的任务,如 SLAM、视频深度估计或者三维重建中也有较大的应用潜力。

\\n

参考资料

\\n
    \\n
  1. Rocco, Ignacio, Relja Arandjelović, and Josef Sivic. "End-to-end weakly-supervised semantic alignment." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2018.
  2. \\n
  3. Derpanis, Konstantinos G. "Overview of the RANSAC Algorithm." Image Rochester NY 4.1 (2010): 2-3.
  4. \\n
  5. Fischler, Martin A., and Robert C. Bolles. "Random sample consensus: a paradigm for model fitting with applications to image analysis and automated cartography." Communications of the ACM 24.6 (1981): 381-395.Communications of the ACM, 24(6):381–395, 1981.
  6. \\n
  7. Torr, Philip HS, and Andrew Zisserman. "MLESAC: A new robust estimator with application to estimating image geometry." Computer vision and image understanding 78.1 (2000): 138-156.
  8. \\n
  9. Torr, Philip H. S., and Colin Davidson. "IMPSAC: Synthesis of importance sampling and random sample consensus." IEEE Transactions on Pattern Analysis and Machine Intelligence 25.3 (2003): 354-364.
  10. \\n
  11. Jin, Yuhe, et al. "Image Matching across Wide Baselines: From Paper to Practice." arXiv preprint arXiv:2003.01587 (2020).
  12. \\n
  13. Yi, Kwang Moo, et al. "Lift: Learned invariant feature transform." European Conference on Computer Vision. Springer, Cham, 2016.
  14. \\n
  15. Noh, Hyeonwoo, et al. "Large-scale image retrieval with attentive deep local features." Proceedings of the IEEE international conference on computer vision. 2017.
  16. \\n
  17. Dusmanu, Mihai, et al. "D2-Net: A Trainable CNN for Joint Description and Detection of Local Features." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.
  18. \\n
  19. “Feature Matching.” OpenCV, opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_feature2d/py_matcher/py_matcher.html.
  20. \\n
  21. http://cmp.felk.cvut.cz/~mishkdmy/slides/EECVC2019_Mishkin_image_matching.pdf
  22. \\n
  23. https://people.cs.umass.edu/~elm/Teaching/ppt/370/370_10_RANSAC.pptx.pdf
  24. \\n
  25. Chen, Yun-Chun, et al. "Deep semantic matching with foreground detection and cycle-consistency." Asian Conference on Computer Vision. Springer, Cham, 2018.
  26. \\n
  27. Ufer, Nikolai, and Bjorn Ommer. "Deep semantic feature matching." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.
  28. \\n
  29. Han, Kai, et al. "Scnet: Learning semantic correspondence." Proceedings of the IEEE International Conference on Computer Vision. 2017.
  30. \\n
  31. Ufer, Nikolai, and Bjorn Ommer. "Deep semantic feature matching." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.
  32. \\n
  33. Rocco, Ignacio, Relja Arandjelovic, and Josef Sivic. "Convolutional neural network architecture for geometric matching." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.
  34. \\n
  35. Jaderberg, Max, Karen Simonyan, and Andrew Zisserman. "Spatial transformer networks." Advances in neural information processing systems. 2015.
  36. \\n
  37. Vaswani, Ashish, et al. "Attention is all you need." Advances in neural information processing systems. 2017.
  38. \\n
  39. Wang, Xiaolong, et al. "Non-local neural networks." Proceedings of the IEEE conference on computer vision and pattern recognition. 2018.
  40. \\n
  41. Wang, Xiaolong, Allan Jabri, and Alexei A. Efros. "Learning correspondence from the cycle-consistency of time." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.
  42. \\n
\\n"},"30":{"id":30,"date":"2021/12/26","author":"Yidadaa","title":"Rust 新手错误和最差实践","mdContent":"> 原文:[Common Newbie Mistakes and Bad Practices in Rust: Bad Habits](https://adventures.michaelfbryan.com/posts/rust-best-practices/bad-habits/#using-sentinel-values) \\r\\n> 译者:[Yidadaa](https://github.com/Yidadaa)\\r\\n\\r\\n很多从其他语言过来的 Rust 新手都会不可避免地利用之前的编码经验来写 Rust,这无可厚非,毕竟确实没必要从头开始学习编程知识。但是,这些经验性知识,却极有可能导致你写出来很垃圾的 Rust 代码。\\r\\n\\r\\n## 别再用哨兵值了\\r\\n\\r\\n这可能是我最讨厌的一点。在大多数沿袭 C 语言设计的语言中(C,C#,Java 等),经常使用一个特殊值来表示某个函数执行失败的情况。比如,在 C# 中,用于在字符串中查找另一个字符串的索引位置的函数 `String.IndexOf(t)` 会在找不到 `t` 时返回 `-1`,从而写出这样的 C# 代码:\\r\\n\\r\\n```c#\\r\\nstring sentence = \\"The fox jumps over the dog\\";\\r\\n\\r\\nint index = sentence.IndexOf(\\"fox\\");\\r\\n\\r\\nif (index != -1)\\r\\n{\\r\\n string wordsAfterFox = sentence.SubString(index);\\r\\n Console.WriteLine(wordsAfterFox);\\r\\n}\\r\\n```\\r\\n\\r\\n在其他的语言中,这种用法更是数不胜数,类似的哨兵值还有空字符串 `\\"\\"` 或者 `null`、`None` 之类的空值。\\r\\n\\r\\n既然它这么常用,为什么还要说它很差劲呢?原因就是你极有可能会忘记处理哨兵值所代表的失败情况,然后导致整个程序直接崩溃。\\r\\n\\r\\nRust 则提供了很好的解决方案,那就是 `Option`。`Option` 从设计层面就杜绝了忘记考虑 `None` 时的情况,编译器会在编译时就进行强制检查,如果你忘了处理 `None`,编译器会马上告诉你。上面字符串例子的代码,在 Rust 中可以写成这样:\\r\\n\\r\\n```rust\\r\\nlet sentence = \\"The fox jumps over the dog\\";\\r\\nlet index = sentence.find(\\"fox\\");\\r\\n\\r\\n// let words_after_fox = &sentence[index..];\\r\\n// 如果你直接使用 index,会得到报错:Error: Can\'t index str with Option\\r\\n\\r\\nif let Some(fox) = index {\\r\\n let words_after_fox = &sentence[fox..];\\r\\n println!(\\"{}\\", words_after_fox);\\r\\n}\\r\\n```\\r\\n\\r\\n## 别再用匈牙利命名了\\r\\n\\r\\n上世纪 70 年代,程序员们逐渐开始在无类型或动态类型语言中使用[匈牙利命名法](https://en.wikipedia.org/wiki/Hungarian_notation),他们给变量名加上不同的前缀来表示变量的类型,比如 `bVisited` 表示布尔型的变量 `visited`,`strName` 表示字符串类型的变量 `name`。\\r\\n\\r\\n我们可以在 `Delphi` 语言中见到大量的例子,`T` 开头的表示类(class)或者类型(type),`F` 表示属性值(fields),`A` 表示参数(arguments),诸如此类。\\r\\n\\r\\n```delphi\\r\\ntype\\r\\n TKeyValue = class\\r\\n private\\r\\n FKey: integer;\\r\\n FValue: TObject;\\r\\n public\\r\\n property Key: integer read FKey write FKey;\\r\\n property Value: TObject read FValue write FValue;\\r\\n function Frobnicate(ASomeArg: string): string;\\r\\n end;\\r\\n```\\r\\n\\r\\nC# 中也有类似的使用习惯,比如用 `I` 开头表示一个接口(interface),所以 C# 程序员很可能会写出这种 Rust 代码:\\r\\n\\r\\n```rust\\r\\ntrait IClone {\\r\\n fn clone(&self) -> Self;\\r\\n}\\r\\n```\\r\\n\\r\\n你大可以直接扔掉前面的 `I`,因为 Rust 的语法已经保证了我们很难混淆 `trait` 和 `type`,不像 C# 很容易就分不清 `interface` 和 `class`(译者按:Typescript 中就是 `interface`、`type` 和 `class` 大混战了,狗头.jpg)。\\r\\n\\r\\n此外,你也没有必要在给一些工具函数或者中间变量命名时带上它的类型信息,比如下面的代码:\\r\\n\\r\\n```rust\\r\\nlet account_bytes: Vec = read_some_input();\\r\\nlet account_str = String::from_utf8(account_bytes)?;\\r\\nlet account: Account = account_str.parse()?;\\r\\n```\\r\\n\\r\\n既然 `String.from_utf8()` 已经明明白白地返回了一个字符串,为什么还要在命名时加上 `_str` 后缀呢?\\r\\n\\r\\n与其他语言不同,Rust 语言鼓励程序员在对变量进行一系列变换操作时,使用同名变量覆写掉不再使用的旧值,比如:\\r\\n\\r\\n```rust\\r\\nlet account: Vec = read_some_input();\\r\\nlet account = String::from_utf8(account)?;\\r\\nlet account: Account = account.parse()?;\\r\\n```\\r\\n\\r\\n使用相同的变量名可以很好地保证概念的一致性。\\r\\n\\r\\n有些语言会明令禁止[覆写变量](https://rules.sonarsource.com/cpp/RSPEC-1117),尤其像 Javascript 这种动态类型语言,因为频繁变化的类型,在缺少类型推断的情况下,尤其有可能会导致 bug 出现。\\r\\n\\r\\n## 你可能不需要这么多 `Rc>`\\r\\n\\r\\nOOP 编程实践常常会保存其他对象的引用,并在合适的时候调用他们的函数,这没啥不好的,依赖注入(Dependency Injection)是个蛮不错的实践,不过有别于大多数面向对象的语言,Rust 并没有垃圾内存回收机制(Garbage Collector),并且对共享可变性非常敏感。\\r\\n\\r\\n举个例子,我们正要实现一个打怪兽的游戏,玩家需要对怪物们造成足量伤害才算打败他们(我也不知道为什么要这么设定,可能是接受了什么委托?)。\\r\\n\\r\\n先创建一个 `Monster` 类,包含 `health` 生命值属性以及 `takeDamage()` 遭受伤害的方法,为了能知道怪物遭受了多少伤害,我们允许为 `Monster` 类注入一个回调函数,该回调函数可以接收每次遭受的伤害值。\\r\\n\\r\\n```ts\\r\\ntype OnReceivedDamage = (damageReceived: number) => void;\\r\\n\\r\\nclass Monster {\\r\\n health: number = 50;\\r\\n receivedDamage: OnReceivedDamage[] = [];\\r\\n\\r\\n takeDamage(amount: number) {\\r\\n amount = Math.min(this.health, amount);\\r\\n this.health -= amount;\\r\\n this.receivedDamage.forEach((cb) => cb(amount));\\r\\n }\\r\\n\\r\\n on(event: \\"damaged\\", callback: OnReceivedDamage): void {\\r\\n this.receivedDamage.push(callback);\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n然后设计一个伤害计数类 `DamageCounter`,可以累计怪物伤害值:\\r\\n\\r\\n```ts\\r\\nclass DamageCounter {\\r\\n damageInflicted: number = 0;\\r\\n\\r\\n reachedTargetDamage(): boolean {\\r\\n return this.damageInflicted > 100;\\r\\n }\\r\\n\\r\\n onDamageInflicted(amount: number) {\\r\\n this.damageInflicted += amount;\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n然后我们对怪物造成随机伤害,直到伤害计数达到上限。\\r\\n\\r\\n```ts\\r\\nconst counter = new DamageCounter();\\r\\n\\r\\nconst monsters = [\\r\\n new Monster(),\\r\\n new Monster(),\\r\\n new Monster(),\\r\\n new Monster(),\\r\\n new Monster(),\\r\\n];\\r\\nmonsters.forEach((m) =>\\r\\n m.on(\\"damaged\\", (amount) => counter.onDamageInflicted(amount))\\r\\n);\\r\\n\\r\\nwhile (!counter.reachedTargetDamage()) {\\r\\n // 随机选中怪物\\r\\n const index = Math.floor(Math.random() * monsters.length);\\r\\n const target = monsters[index];\\r\\n // 产生随机伤害\\r\\n const damage = Math.round(Math.random() * 50);\\r\\n target.takeDamage(damage);\\r\\n\\r\\n console.log(`Monster ${index} received ${damage} damage`);\\r\\n}\\r\\n```\\r\\n\\r\\n这里是[在线运行示例](https://www.typescriptlang.org/play?#code/C4TwDgpgBA8gdgJQgYwgSwG4QCYBECGAtvgObQC8UAFIQFxQCyA9nAM7AQBOANFNkaQhJUmHPTgBXQgCMuASijkAfFAxM02ANwAobcgA2+Vq0Yt2XKAG9tUW1AAWEfPuD3xU2Z0VQArAAYdOyhOFHQsPAEyenhhMJwCYjIAbQBdb1SdGztgfABrCATBKiImCThgdxl5Kyyg2xKy4G8GfFcAOkI0OCpXNFY2x2dXXgbyuUC6217+wZd7KABaSlHgTTrauum2kJFwwrI2gDMmTgBRfGR7KmRpRRUbnvs+kcJSsfGNgF9dIJYqCCw5XoACJ+IkcMDeMhnPppBdctFEKFRBFwXJ6GoNDVJlMnv0dnFUYI2mAJKwrtD9LD4R8gt9vnpDMYoPsIABhN4cLzWIJgwQASTgh30aGQHGwlU83gCPzsIQujmwABV8JwyMBWVR0VBpEwmPonHBsTiQsAJJwjVs+WRBcLReKoCoAIx+GV02W2Fis20isU4YqvRqS6o8nFWyIQH32nBQADUy0D5QmtnpumQZia6caFkocAgAHcWRGOdnOFrMum2E1XlWuCZKEk84XmLWy3JeE3TK2tR2C13zG3e82M1we1BOy2B1qUjoawP+sczgqaHcoIQ2n9QRHsJDqAB9F6chTKKBZ8pcDdwb1C33igNHuQfbT5p4G6gAQjPXO2TkuOBVaoQBqEZagooa2AA9BBUBgKKuRQPgwT4HA2BMIQa4jpwGyVuwUBdNgEAAB7NK09hHPoepli07ScMhqGEFqABUc5cv0BpwCQri0nYOFNDkgFNJQLF1kk+FETOGxQVArgQEa1rQGgTSIdIinYRmfARiRNFvNgVDUWRtEoWhWpQIxvh+NxUyquqbQ5PkmryU+QQ4fqEBtBRJBUAABpOXJQAAJJYYmEZ8wTIuEAWWPJoXydQwBMDk+gaeCeE3tGEqRV+F7yVGfrYJ8XkfJ8QA)。\\r\\n\\r\\n然后我们用 Rust 重写上述逻辑,`Monster` 结构体结构保持不变,使用 `Box` 来接收闭包,该闭包接受一个 `u32` 型参数。\\r\\n\\r\\n```rust\\r\\ntype OnReceivedDamage = Box;\\r\\n\\r\\nstruct Monster {\\r\\n health: u32,\\r\\n received_damage: Vec,\\r\\n}\\r\\n\\r\\nimpl Monster {\\r\\n fn take_damage(&mut self, amount: u32) {\\r\\n let damage_received = cmp::min(self.health, amount);\\r\\n self.health -= damage_received;\\r\\n for callback in &mut self.received_damage {\\r\\n callback(damage_received);\\r\\n }\\r\\n }\\r\\n\\r\\n fn add_listener(&mut self, listener: OnReceivedDamage) {\\r\\n self.received_damage.push(listener);\\r\\n }\\r\\n}\\r\\n\\r\\nimpl Default for Monster {\\r\\n fn default() -> Self {\\r\\n Monster { health: 100, received_damage: Vec::new() }\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n随后是 `DamageCounter` 类:\\r\\n\\r\\n```rust\\r\\n#[derive(Default)]\\r\\nstruct DamageCounter {\\r\\n damage_inflicted: u32,\\r\\n}\\r\\n\\r\\nimpl DamageCounter {\\r\\n fn reached_target_damage(&self) -> bool {\\r\\n self.damage_inflicted > 100\\r\\n }\\r\\n\\r\\n fn on_damage_received(&mut self, damage: u32) {\\r\\n self.damage_inflicted += damage;\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n然后开始打怪:\\r\\n\\r\\n```rust\\r\\nfn main() {\\r\\n let mut rng = rand::thread_rng();\\r\\n let mut counter = DamageCounter::default();\\r\\n let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\r\\n\\r\\n for monster in &mut monsters {\\r\\n monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n }\\r\\n\\r\\n while !counter.reached_target_damage() {\\r\\n let index = rng.gen_range(0..monsters.len());\\r\\n let target = &mut monsters[index];\\r\\n\\r\\n let damage = rng.gen_range(0..50);\\r\\n target.take_damage(damage);\\r\\n\\r\\n println!(\\"Monster {} received {} damage\\", index, damage);\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n同样地,Rust 代码也有一个[在线运行示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a8cc547728ef102bbd5dc6b9cafb0ff6)。\\r\\n\\r\\n然而,编译器狠狠地给我们报了四个错误,全都集中在 `monster.add_listener()` 这一行:\\r\\n\\r\\n```rust\\r\\nerror[E0596]: cannot borrow `counter` as mutable, as it is a captured variable in a `Fn` closure\\r\\n --\x3e src/main.rs:47:48\\r\\n |\\r\\n47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n | ^^^^^^^ cannot borrow as mutable\\r\\n\\r\\nerror[E0499]: cannot borrow `counter` as mutable more than once at a time\\r\\n --\x3e src/main.rs:47:39\\r\\n |\\r\\n47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n | ---------^^^^^^^^------------------------------------\\r\\n | | | |\\r\\n | | | borrows occur due to use of `counter` in closure\\r\\n | | `counter` was mutably borrowed here in the previous iteration of the loop\\r\\n | cast requires that `counter` is borrowed for `\'static`\\r\\n\\r\\nerror[E0597]: `counter` does not live long enough\\r\\n --\x3e src/main.rs:47:48\\r\\n |\\r\\n47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n | ------------------^^^^^^^----------------------------\\r\\n | | | |\\r\\n | | | borrowed value does not live long enough\\r\\n | | value captured here\\r\\n | cast requires that `counter` is borrowed for `\'static`\\r\\n...\\r\\n60 | }\\r\\n | - `counter` dropped here while still borrowed\\r\\n\\r\\nerror[E0502]: cannot borrow `counter` as immutable because it is also borrowed as mutable\\r\\n --\x3e src/main.rs:50:12\\r\\n |\\r\\n47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n | -----------------------------------------------------\\r\\n | | | |\\r\\n | | | first borrow occurs due to use of `counter` in closure\\r\\n | | mutable borrow occurs here\\r\\n | cast requires that `counter` is borrowed for `\'static`\\r\\n...\\r\\n50 | while !counter.reached_target_damage() {\\r\\n | ^^^^^^^ immutable borrow occurs here\\r\\n```\\r\\n\\r\\n看起来是一团乱麻,让我来翻译翻译,什么叫惊喜:\\r\\n\\r\\n- 闭包捕获了对 `counter` 的引用;\\r\\n- `counter.on_damage_received()` 方法接受 `&mut self`,所以闭包需要 `&mut` 可变引用。由于这个闭包在循环里,所以对同一个对象的可变引用 `&mut` 会执行多次,这是不被 Rust 允许的;\\r\\n- 我们传入的闭包没有任何生命周期标记,意味着它需要掌控所有包含变量的所有权,所以 `counter` 需要被移动到闭包内部,而在循环中,重复移动某值就会造成 `use of moved value` 错误;\\r\\n- 当 `counter` 被移动到闭包内后,我们又尝试在条件语句中使用它,显然也会报错。\\r\\n\\r\\n总之,情况不太妙。\\r\\n\\r\\n一个经典解决方法是把 `DamageCounter` 用引用计数指针裹起来,这样我们可以重复使用它了,此外由于我们需要使用 `&mut self`,所以我们需要使用 `RefCell` 来在运行时做借用检查(borrow checking),而不是在编译时。\\r\\n\\r\\n```diff\\r\\n fn main() {\\r\\n let mut rng = rand::thread_rng();\\r\\n- let mut counter = DamageCounter::default();\\r\\n+ let mut counter = Rc::new(RefCell::new(DamageCounter::default()));\\r\\n let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\r\\n\\r\\n for monster in &mut monsters {\\r\\n- monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n+ let counter = Rc::clone(&counter);\\r\\n+ monster.add_listener(Box::new(move |damage| {\\r\\n+ counter.borrow_mut().on_damage_received(damage)\\r\\n+ }));\\r\\n }\\r\\n\\r\\n- while !counter.reached_target_damage() {\\r\\n+ while !counter.borrow().reached_target_damage() {\\r\\n let index = rng.gen_range(0..monsters.len());\\r\\n let target = &mut monsters[index];\\r\\n ...\\r\\n }\\r\\n }\\r\\n```\\r\\n\\r\\n这里是[改动后的代码](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=7aca92432f337fa29de62999ea5709b8)。\\r\\n\\r\\n虽然现在代码可以正常运行了,但是整块代码会被 `RcVec>>` 之类的玩意儿搞得乌烟瘴气。而且当代码变得更复杂时,`RefCell` 也有可能会被可变借用多次。而如果在多线程中使用了 `Arc>>`,`RefCell` 引发 panic 之后,整个程序会死锁。\\r\\n\\r\\n所以,一个更好的解决方法是避免在结构体中存储持久化引用。我们对 `Monster::take_damage()` 稍加改造:\\r\\n\\r\\n```rust\\r\\nstruct Monster {\\r\\n health: u32,\\r\\n}\\r\\n\\r\\nimpl Monster {\\r\\n fn take_damage(&mut self, amount: u32, on_damage_received: impl FnOnce(u32)) {\\r\\n let damage_received = cmp::min(self.health, amount);\\r\\n self.health -= damage_received;\\r\\n on_damage_received(damage_received);\\r\\n }\\r\\n}\\r\\n\\r\\nimpl Default for Monster {\\r\\n fn default() -> Self { Monster { health: 100 } }\\r\\n}\\r\\n\\r\\n// 省略了 `DamageCounter` 的代码\\r\\n\\r\\nfn main() {\\r\\n let mut rng = rand::thread_rng();\\r\\n let mut counter = DamageCounter::default();\\r\\n let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\r\\n\\r\\n while !counter.reached_target_damage() {\\r\\n let index = rng.gen_range(0..monsters.len());\\r\\n let target = &mut monsters[index];\\r\\n\\r\\n let damage = rng.gen_range(0..50);\\r\\n target.take_damage(damage, |dmg| counter.on_damage_received(dmg));\\r\\n\\r\\n println!(\\"Monster {} received {} damage\\", index, damage);\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n这里是[在线示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=52b789a7616efe6c2e24b7e1949f7c03)。\\r\\n\\r\\n由于避免了存储回调函数,改造后的代码行数从 62 行下降到了 47 行。\\r\\n\\r\\n此外,我们也可以给 `take_damage()` 加个返回值,这样可以把伤害值放在返回值里,以备后用:\\r\\n\\r\\n```rust\\r\\nimpl Monster {\\r\\n fn take_damage(&mut self, amount: u32) -> AttackSummary {\\r\\n let damage_received = cmp::min(self.health, amount);\\r\\n self.health -= damage_received;\\r\\n AttackSummary { damage_received }\\r\\n }\\r\\n}\\r\\n\\r\\nstruct AttackSummary {\\r\\n damage_received: u32,\\r\\n}\\r\\n\\r\\n// 省略了 `DamageCounter` 的代码\\r\\n\\r\\nfn main() {\\r\\n let mut rng = rand::thread_rng();\\r\\n let mut counter = DamageCounter::default();\\r\\n let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\r\\n\\r\\n while !counter.reached_target_damage() {\\r\\n let index = rng.gen_range(0..monsters.len());\\r\\n let target = &mut monsters[index];\\r\\n\\r\\n let damage = rng.gen_range(0..50);\\r\\n let AttackSummary { damage_received } = target.take_damage(damage);\\r\\n counter.on_damage_received(damage_received);\\r\\n\\r\\n println!(\\"Monster {} received {} damage\\", index, damage);\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n当代码复杂度上升时,代码也不会变成一团糟,而且它看起来更“函数式”。\\r\\n\\r\\n## 错用整数类型\\r\\n\\r\\n另一个从 C 语言带来的坏毛病是错用整数类型,导致代码里到处都是 `usize` 的类型转换,尤其是在对数组做索引时。\\r\\n\\r\\nC 程序员在初学时就被各种教程教会了使用 `int` 类型来做索引和循环,当他们开始写 Rust 时,也自然而然地用 `Vec` 类型来做数组切片。但是阿 Rust 真的很严格,不让程序员使用 `usize` 以外的类型对数组、切片和 `Vec` 进行索引,这就不得不在索引的时候进行一次 `i32 as usize` 的类型转换。\\r\\n\\r\\nRust 这么做有诸多好处:\\r\\n\\r\\n- 无符号类型可以避免负数索引(译者按:Python 程序员请求出战);\\r\\n- `usize` 与普通指针的大小相同,指针运算不会造成隐式类型转换;\\r\\n- 内存操作函数 `std::mem::size_of()` 和 `std::mem::align_of()` 返回 `usize` 类型。\\r\\n\\r\\n所以,请尽量使用 `usize` 类型作为可能涉及索引操作的中间变量的首选类型。\\r\\n\\r\\n## 没人比我更懂 `unsafe`\\r\\n\\r\\n每次当我看到 C 程序员使用 `std::mem::transmute()` 函数或者裸指针来跳过编译器的借用检查时,我都会想起论坛中那条古老的 Rust 圣经:[Obstacles, by Daniel Keep](https://users.rust-lang.org/t/rust-koans/2408?u=michael-f-bryan)。\\r\\n\\r\\n建议你现在就去读一读,我可以等。(译者按:有空了给大伙翻译一下。)\\r\\n\\r\\n你可能已经身经百战见得多了,精通八种编程语言,所以毫无顾忌地破坏 Rust 精心构筑的规则:创建自引用的结构体、用 `unsafe` 创建全局变量。而且每一次,你都用同样的借口:“这是个单线程程序,所以 `static mut` 百分之一万没问题”、“这在 C 语言里跑得好好的”。\\r\\n\\r\\n`unsafe` 很微妙,你必须要对 Rust 的借用检查规则和内存模型有深刻的认识才行。我也不想像祥林嫂一样念叨:“未成年人请在编译器监督下编写 `unsafe` 多线程代码”,但如果你刚开始学这门语言,我衷心建议你耐心从编译器报错的痛苦中慢慢品味 Rust 的美妙。\\r\\n\\r\\n当你成为了 Rust 大师,你可以尽情玩弄 `unsafe` 代码,但在那之前,我还是想告诉你,`unsafe` 不是杀死编译器报错的板蓝根,也不是能让你自在书写 C 风味 Rust 代码的作弊码。\\r\\n\\r\\n## 不舍得用命名空间\\r\\n\\r\\nC 语言中的另一个常见实践是给函数增加所属的库名或者模块名作为前缀,比如 `rune_wasmer_runtime_load()` 就表示 `rune` 库的 `wasmer/runtime` 模块下的 `load()` 函数。Rust 提供了非常好用的命名空间机制,请尽情使用它,比如刚刚这个函数就可以写成 `rune::wasmer::Runtime::load()`。\\r\\n\\r\\n## 滥用切片索引\\r\\n\\r\\nC 语言离不开 `for (int i = 0; i < n; i ++)`,就像西方不能没有耶路撒冷。\\r\\n\\r\\n所以下面的 Rust 的代码也就不足为奇:\\r\\n\\r\\n```rust\\r\\nlet points: Vec = ...;\\r\\nlet differences = Vec::new();\\r\\n\\r\\nfor i in 1..points.len() [\\r\\n let current = points[i];\\r\\n let previous = points[i-1];\\r\\n differences.push(current - previous);\\r\\n]\\r\\n```\\r\\n\\r\\n就像呼吸一样自然。然而,就算是老司机也难免会中招下标越界 bug ,尤其当你想在循环里取前一个值时,你就得花心思去考虑 `i` 是否是从 1 开始的。\\r\\n\\r\\nRust 很担心你,所以拿出了迭代器,切片类型甚至还有 `windows()` 和 `array_windows()` 这种高级函数来获取相邻的元素对。上面的代码可以重写为:\\r\\n\\r\\n```rust\\r\\nlet points: Vec = ...;\\r\\nlet mut differences = Vec::new();\\r\\n\\r\\nfor [previous, current] in points.array_windows().copied() {\\r\\n differences.push(current - previous);\\r\\n}\\r\\n```\\r\\n\\r\\n甚至可以用链式调用来炫技:\\r\\n\\r\\n```rust\\r\\nlet differences: Vec<_> = points\\r\\n .array_windows()\\r\\n .copied()\\r\\n .map(|[previous, current]| current - previous)\\r\\n .collect();\\r\\n```\\r\\n\\r\\n有些人会主张使用了 `map()` 和 `collect` 版本的代码更加“函数式“,我则觉得仁者见仁,智者见智。\\r\\n\\r\\n不仅如此,迭代器的性能往往比朴素的 `for` 循环更好,你可以在[这里](https://users.rust-lang.org/t/we-all-know-iter-is-faster-than-loop-but-why/51486/7?u=michael-f-bryan)了解原因。\\r\\n\\r\\n## 滥用迭代器\\r\\n\\r\\n一旦你用迭代器用上瘾了,你极有可能跑向对立面:拿着迭代器这个锤子,看啥都像钉子。由 `map`,`filter` 和 `and_then()` 堆叠成的链式调用会让代码可读性下降,而且频繁使用闭包,会让数据类型变得不再直观。\\r\\n\\r\\n下面有个例子,演示了迭代器如何让你的代码变得更复杂,你可以读一读这段代码,并猜猜它是干啥的:\\r\\n\\r\\n```rust\\r\\npub fn functional_blur(input: &Matrix) -> Matrix {\\r\\n assert!(input.width >= 3);\\r\\n assert!(input.height >= 3);\\r\\n\\r\\n // 先保存首尾两行,方便后续使用\\r\\n let mut rows = input.rows();\\r\\n let first_row = rows.next().unwrap();\\r\\n let last_row = rows.next_back().unwrap();\\r\\n\\r\\n let top_row = input.rows();\\r\\n let middle_row = input.rows().skip(1);\\r\\n let bottom_row = input.rows().skip(2);\\r\\n\\r\\n let blurred_elements = top_row\\r\\n .zip(middle_row)\\r\\n .zip(bottom_row)\\r\\n .flat_map(|((top, middle), bottom)| blur_rows(top, middle, bottom));\\r\\n\\r\\n let elements: Vec = first_row\\r\\n .iter()\\r\\n .copied()\\r\\n .chain(blurred_elements)\\r\\n .chain(last_row.iter().copied())\\r\\n .collect();\\r\\n\\r\\n Matrix::new_row_major(elements, input.width, input.height)\\r\\n}\\r\\n\\r\\nfn blur_rows<\'a>(\\r\\n top_row: &\'a [f32],\\r\\n middle_row: &\'a [f32],\\r\\n bottom_row: &\'a [f32],\\r\\n) -> impl Iterator + \'a {\\r\\n // 保存头尾元素,以备后用\\r\\n let &first = middle_row.first().unwrap();\\r\\n let &last = middle_row.last().unwrap();\\r\\n\\r\\n // 获取上中下的 3x3 矩阵来做平均\\r\\n let top_window = top_row.windows(3);\\r\\n let middle_window = middle_row.windows(3);\\r\\n let bottom_window = bottom_row.windows(3);\\r\\n\\r\\n // 滑动窗口取均值,除了首尾元素\\r\\n let averages = top_window\\r\\n .zip(middle_window)\\r\\n .zip(bottom_window)\\r\\n .map(|((top, middle), bottom)| top.iter().chain(middle).chain(bottom).sum::() / 9.0);\\r\\n\\r\\n std::iter::once(first)\\r\\n .chain(averages)\\r\\n .chain(std::iter::once(last))\\r\\n}\\r\\n```\\r\\n\\r\\n[在线示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=da8fa6e55ca5a0de6005b13672688c14)。\\r\\n\\r\\n看起来好像并不难,做个均值滤波罢了,不过我这里有个更好的实现:\\r\\n\\r\\n```rust\\r\\npub fn imperative_blur(input: &Matrix) -> Matrix {\\r\\n assert!(input.width >= 3);\\r\\n assert!(input.height >= 3);\\r\\n\\r\\n // 直接从输入值拷贝返回值矩阵,这样就不需要考虑边界条件\\r\\n let mut output = input.clone();\\r\\n\\r\\n for y in 1..(input.height - 1) {\\r\\n for x in 1..(input.width - 1) {\\r\\n let mut pixel_value = 0.0;\\r\\n\\r\\n pixel_value += input[[x - 1, y - 1]];\\r\\n pixel_value += input[[x, y - 1]];\\r\\n pixel_value += input[[x + 1, y - 1]];\\r\\n\\r\\n pixel_value += input[[x - 1, y]];\\r\\n pixel_value += input[[x, y]];\\r\\n pixel_value += input[[x + 1, y]];\\r\\n\\r\\n pixel_value += input[[x - 1, y + 1]];\\r\\n pixel_value += input[[x, y + 1]];\\r\\n pixel_value += input[[x + 1, y + 1]];\\r\\n\\r\\n output[[x, y]] = pixel_value / 9.0;\\r\\n }\\r\\n }\\r\\n\\r\\n output\\r\\n}\\r\\n```\\r\\n\\r\\n[在线示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ed5a8cbe8cfab762c32466c551957810)。\\r\\n\\r\\n我想你的心里已经有答案了吧。\\r\\n\\r\\n## 不会用模式匹配\\r\\n\\r\\n让我们回到一开始的 `IndexOf()` 函数,我们用 `Option` 类型举了一个很好的例子,先看下原始代码:\\r\\n\\r\\n```c#\\r\\nint index = sentence.IndexOf(\\"fox\\");\\r\\n\\r\\nif (index != -1)\\r\\n{\\r\\n string wordsAfterFox = sentence.SubString(index);\\r\\n Console.WriteLine(wordsAfterFox);\\r\\n}\\r\\n```\\r\\n\\r\\n然后,你可能会看到这样的 Rust 代码:\\r\\n\\r\\n```rust\\r\\nlet opt: Option<_> = ...;\\r\\n\\r\\nif opt.is_some() {\\r\\n let value = opt.unwrap();\\r\\n ...\\r\\n}\\r\\n```\\r\\n\\r\\n或者这样的:\\r\\n\\r\\n```rust\\r\\nlet list: &[f32] = ...;\\r\\n\\r\\nif !list.is_empty() {\\r\\n let first = list[0];\\r\\n ...\\r\\n}\\r\\n```\\r\\n\\r\\n这些条件语句都在避免某些边界条件,不过就像之前说到的哨兵值一样,我们在重构的时候依然会极有可能引入 bug。\\r\\n\\r\\n而使用 Rust 的模式匹配,你可以保证当且仅当值有效时才会执行到对应的代码:\\r\\n\\r\\n```rust\\r\\nif let Some(value) = opt {\\r\\n ...\\r\\n}\\r\\n\\r\\nif let [first, ..] = list {\\r\\n ...\\r\\n}\\r\\n```\\r\\n\\r\\n相比于之前的代码,由于避免了 `opt.unwrap()` 和 `list[index]`,模式匹配可以有更好的性能(作者的一点忠告:不要在网上听风就是雨,如果你真的想知道真相,建议写个 Benchmark 验证下)。\\r\\n\\r\\n## 别再构造函数后初始化\\r\\n\\r\\n许多语言都会在构造对象后调用对应的初始化函数(`init()` 之类的),但这有悖于 Rust 的约定:让无效状态不可见。\\r\\n\\r\\n假设你在写个 NLP 程序,需要加载一个包含所有关键词的词表:\\r\\n\\r\\n```rust\\r\\nlet mut dict = Dictionary::new();\\r\\n// 读取文件并且把值存到哈希表或者列表里\\r\\ndict.load_from_file(\\"./words.txt\\")?;\\r\\n```\\r\\n\\r\\n然而,如果这么写了,意味着 `Dictionary` 类有两个状态:空的和满的。\\r\\n\\r\\n那么如果后续代码假设 `Dictionary` 有值,并且直接使用它,那当我们错误地对一个空状态的 `Dictionary` 进行索引时,就会造成 panic。\\r\\n\\r\\n在 Rust 中,最好在构造时就对结构体进行初始化,来避免结构体的空状态。\\r\\n\\r\\n```rust\\r\\nlet dict = Dictionary::from_file(\\"./words.txt\\")?;\\r\\n\\r\\nimpl Dictionary {\\r\\n fn from_file(filename: impl AsRef) -> Result {\\r\\n let text = std::fs::read_to_string(filename)?;\\r\\n let mut words = Vec::new();\\r\\n for line in text.lines() {\\r\\n words.push(line);\\r\\n }\\r\\n Ok(Dictionary { words })\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n`Dictionary::from_file()` 直接执行了初始化操作,并返回了初始化后的、立即可用的结构体,从而避免了上述问题。\\r\\n\\r\\n当然,遇到这种问题的频率因人而异,完全取决于你的编码经验和代码风格。\\r\\n\\r\\n一般来讲,函数式语言强调不可变性,所以函数式语言的使用者会天然地掌握这个经验。毕竟当你不能随便改变某个值时,你也不大可能创建一个初始化了一半的变量,然后再用什么其他值去填满它。\\r\\n\\r\\n但面向对象的语言就不太一样了,它可能更鼓励你先构造个空对象,然后再调用具体函数初始化它,毕竟对象引用很容易为 `null`,而且他们也不关心什么可变性之类的玩意儿……现在你知道为啥那些面向对象语言会经常由于 `NullPointerException` 崩溃了吧。\\r\\n\\r\\n## 保护性拷贝\\r\\n\\r\\n不可变对象的一个显而易见的优点时,你永远可以相信它不会发生变化,而放心地使用它的值。但某些语言,比如 Python 或者 Java,不可变性没有传递性。举个例子,`x` 是个不可变对象,`x.y` 却不一定是不可变的,除非显式地定义它的可变性。\\r\\n\\r\\n这意味着会出现下面的 Python 代码:\\r\\n\\r\\n```python\\r\\nclass ImmutablePerson:\\r\\n def __init__(self, name: str, age: int, addresses: List[str]):\\r\\n self._name = name\\r\\n self._age = age\\r\\n self._addresses = addresses\\r\\n\\r\\n # 只读属性\\r\\n @property\\r\\n def name(self): return self._name\\r\\n @property\\r\\n def age(self): return self._age\\r\\n @property\\r\\n def addresses(self): return self._addresses\\r\\n```\\r\\n\\r\\n后来其他人用了这个号称不可变的 `ImmutablePerson`,但是却不小心把它搞乱了:\\r\\n\\r\\n```python\\r\\ndef send_letters(message: str, addresses: List[str]):\\r\\n # 注意:发信 api 只接受大写字母,所以这里要预处理\\r\\n for i, address in enumerate(addresses):\\r\\n addresses[i] = addresses.upper()\\r\\n\\r\\n client = PostOfficeClient()\\r\\n client.send_bulk_mail(message, addresses)\\r\\n\\r\\n\\r\\nperson = ImmutablePerson(\\"Joe Bloggs\\", 42, [\\"123 Fake Street\\"])\\r\\n\\r\\nsend_letters(\\r\\n f\\"Dear {person.name}, I Nigerian prince. Please help me moving my monies.\\",\\r\\n person.addresses\\r\\n)\\r\\n\\r\\nprint(person.addresses) # [\\"123 FAKE STREET\\"]\\r\\n```\\r\\n\\r\\n我承认,这个例子确实有点刻意了,但是修改一个函数的传参却非常常见(译者按:尤其在某些深度学习项目里)。当你知道你自己定义的 `ImmutablePerson` 的 `addresses` 属性不可变时,不会有什么大问题,但是当你和别人协作,而且别人还不知道 `addresses` 不可变时,那就出大问题了。\\r\\n\\r\\n事情也不是无法挽回,解决这个问题的经典方法是,总是在获取属性值的时候,返回它的拷贝,而非它自己:\\r\\n\\r\\n```python\\r\\nclass ImmutablePerson:\\r\\n ...\\r\\n\\r\\n @property\\r\\n def addresses(self): return self._addresses.copy()\\r\\n```\\r\\n\\r\\n这样就可以保证别人在使用该对象的属性时,不会意外改变它的原始值。\\r\\n\\r\\n考虑到这篇文章的主题是 Rust,你可能已经猜到了造成这种问题的根本原因:别名与可变性。\\r\\n\\r\\n并且你也可能想到,这种情况不会发生在 Rust 中,生命周期机制以及“有且只能有一处可变引用”的机制,保证了程序员无法在取得变量的所有权情况下去改动它的值,也没法显式地使用 `std::sync::Mutex` 去改变某个共享引用值。\\r\\n\\r\\n> 备注:你可能见过别人用 `.clone()` 来处理借用检查器的报错,然后就大喊“你看,Rust 还不是强迫我们做了保护性拷贝措施?”。我想说的是,这种代码基本都是由于程序员不熟悉生命周期机制,或者代码设计有问题导致的。\\r\\n\\r\\n## 总结\\r\\n\\r\\n本文并不能覆盖所有的最差实践,有些是因为我没亲身经历过,有些则是由于没法给出精简的例子。\\r\\n\\r\\n衷心地感谢回复我在 Rust 论坛发布的[这个帖子](https://users.rust-lang.org/t/common-newbie-mistakes-or-bad-practices/64821/8)的各位同仁,尽管帖子的最后有点跑偏,各位 Rust 老鸟的论战还是让我受益颇深。\\r\\n","content":"
\\n

原文:Common Newbie Mistakes and Bad Practices in Rust: Bad Habits
\\n译者:Yidadaa

\\n
\\n

很多从其他语言过来的 Rust 新手都会不可避免地利用之前的编码经验来写 Rust,这无可厚非,毕竟确实没必要从头开始学习编程知识。但是,这些经验性知识,却极有可能导致你写出来很垃圾的 Rust 代码。

\\n

别再用哨兵值了

\\n

这可能是我最讨厌的一点。在大多数沿袭 C 语言设计的语言中(C,C#,Java 等),经常使用一个特殊值来表示某个函数执行失败的情况。比如,在 C# 中,用于在字符串中查找另一个字符串的索引位置的函数 String.IndexOf(t) 会在找不到 t 时返回 -1,从而写出这样的 C# 代码:

\\n
string sentence = "The fox jumps over the dog";\\n\\nint index = sentence.IndexOf("fox");\\n\\nif (index != -1)\\n{\\n  string wordsAfterFox = sentence.SubString(index);\\n  Console.WriteLine(wordsAfterFox);\\n}\\n
\\n

在其他的语言中,这种用法更是数不胜数,类似的哨兵值还有空字符串 "" 或者 nullNone 之类的空值。

\\n

既然它这么常用,为什么还要说它很差劲呢?原因就是你极有可能会忘记处理哨兵值所代表的失败情况,然后导致整个程序直接崩溃。

\\n

Rust 则提供了很好的解决方案,那就是 OptionOption 从设计层面就杜绝了忘记考虑 None 时的情况,编译器会在编译时就进行强制检查,如果你忘了处理 None,编译器会马上告诉你。上面字符串例子的代码,在 Rust 中可以写成这样:

\\n
let sentence = "The fox jumps over the dog";\\nlet index = sentence.find("fox");\\n\\n// let words_after_fox = &sentence[index..];\\n// 如果你直接使用 index,会得到报错:Error: Can't index str with Option<usize>\\n\\nif let Some(fox) = index {\\n  let words_after_fox = &sentence[fox..];\\n  println!("{}", words_after_fox);\\n}\\n
\\n

别再用匈牙利命名了

\\n

上世纪 70 年代,程序员们逐渐开始在无类型或动态类型语言中使用匈牙利命名法,他们给变量名加上不同的前缀来表示变量的类型,比如 bVisited 表示布尔型的变量 visitedstrName 表示字符串类型的变量 name

\\n

我们可以在 Delphi 语言中见到大量的例子,T 开头的表示类(class)或者类型(type),F 表示属性值(fields),A 表示参数(arguments),诸如此类。

\\n
type\\n TKeyValue = class\\n  private\\n    FKey: integer;\\n    FValue: TObject;\\n  public\\n    property Key: integer read FKey write FKey;\\n    property Value: TObject read FValue write FValue;\\n    function Frobnicate(ASomeArg: string): string;\\n  end;\\n
\\n

C# 中也有类似的使用习惯,比如用 I 开头表示一个接口(interface),所以 C# 程序员很可能会写出这种 Rust 代码:

\\n
trait IClone {\\n  fn clone(&self) -> Self;\\n}\\n
\\n

你大可以直接扔掉前面的 I,因为 Rust 的语法已经保证了我们很难混淆 traittype,不像 C# 很容易就分不清 interfaceclass(译者按:Typescript 中就是 interfacetypeclass 大混战了,狗头.jpg)。

\\n

此外,你也没有必要在给一些工具函数或者中间变量命名时带上它的类型信息,比如下面的代码:

\\n
let account_bytes: Vec<u8> = read_some_input();\\nlet account_str = String::from_utf8(account_bytes)?;\\nlet account: Account = account_str.parse()?;\\n
\\n

既然 String.from_utf8() 已经明明白白地返回了一个字符串,为什么还要在命名时加上 _str 后缀呢?

\\n

与其他语言不同,Rust 语言鼓励程序员在对变量进行一系列变换操作时,使用同名变量覆写掉不再使用的旧值,比如:

\\n
let account: Vec<u8> = read_some_input();\\nlet account = String::from_utf8(account)?;\\nlet account: Account = account.parse()?;\\n
\\n

使用相同的变量名可以很好地保证概念的一致性。

\\n

有些语言会明令禁止覆写变量,尤其像 Javascript 这种动态类型语言,因为频繁变化的类型,在缺少类型推断的情况下,尤其有可能会导致 bug 出现。

\\n

你可能不需要这么多 Rc<RefCell<T>>

\\n

OOP 编程实践常常会保存其他对象的引用,并在合适的时候调用他们的函数,这没啥不好的,依赖注入(Dependency Injection)是个蛮不错的实践,不过有别于大多数面向对象的语言,Rust 并没有垃圾内存回收机制(Garbage Collector),并且对共享可变性非常敏感。

\\n

举个例子,我们正要实现一个打怪兽的游戏,玩家需要对怪物们造成足量伤害才算打败他们(我也不知道为什么要这么设定,可能是接受了什么委托?)。

\\n

先创建一个 Monster 类,包含 health 生命值属性以及 takeDamage() 遭受伤害的方法,为了能知道怪物遭受了多少伤害,我们允许为 Monster 类注入一个回调函数,该回调函数可以接收每次遭受的伤害值。

\\n
type OnReceivedDamage = (damageReceived: number) => void;\\n\\nclass Monster {\\n  health: number = 50;\\n  receivedDamage: OnReceivedDamage[] = [];\\n\\n  takeDamage(amount: number) {\\n    amount = Math.min(this.health, amount);\\n    this.health -= amount;\\n    this.receivedDamage.forEach((cb) => cb(amount));\\n  }\\n\\n  on(event: "damaged", callback: OnReceivedDamage): void {\\n    this.receivedDamage.push(callback);\\n  }\\n}\\n
\\n

然后设计一个伤害计数类 DamageCounter,可以累计怪物伤害值:

\\n
class DamageCounter {\\n  damageInflicted: number = 0;\\n\\n  reachedTargetDamage(): boolean {\\n    return this.damageInflicted > 100;\\n  }\\n\\n  onDamageInflicted(amount: number) {\\n    this.damageInflicted += amount;\\n  }\\n}\\n
\\n

然后我们对怪物造成随机伤害,直到伤害计数达到上限。

\\n
const counter = new DamageCounter();\\n\\nconst monsters = [\\n  new Monster(),\\n  new Monster(),\\n  new Monster(),\\n  new Monster(),\\n  new Monster(),\\n];\\nmonsters.forEach((m) =>\\n  m.on("damaged", (amount) => counter.onDamageInflicted(amount))\\n);\\n\\nwhile (!counter.reachedTargetDamage()) {\\n  // 随机选中怪物\\n  const index = Math.floor(Math.random() * monsters.length);\\n  const target = monsters[index];\\n  // 产生随机伤害\\n  const damage = Math.round(Math.random() * 50);\\n  target.takeDamage(damage);\\n\\n  console.log(`Monster ${index} received ${damage} damage`);\\n}\\n
\\n

这里是在线运行示例

\\n

然后我们用 Rust 重写上述逻辑,Monster 结构体结构保持不变,使用 Box<dyn Fn(u32)> 来接收闭包,该闭包接受一个 u32 型参数。

\\n
type OnReceivedDamage = Box<dyn Fn(u32)>;\\n\\nstruct Monster {\\n    health: u32,\\n    received_damage: Vec<OnReceivedDamage>,\\n}\\n\\nimpl Monster {\\n    fn take_damage(&mut self, amount: u32) {\\n        let damage_received = cmp::min(self.health, amount);\\n        self.health -= damage_received;\\n        for callback in &mut self.received_damage {\\n            callback(damage_received);\\n        }\\n    }\\n\\n    fn add_listener(&mut self, listener: OnReceivedDamage) {\\n        self.received_damage.push(listener);\\n    }\\n}\\n\\nimpl Default for Monster {\\n    fn default() -> Self {\\n        Monster { health: 100, received_damage: Vec::new() }\\n    }\\n}\\n
\\n

随后是 DamageCounter 类:

\\n
#[derive(Default)]\\nstruct DamageCounter {\\n    damage_inflicted: u32,\\n}\\n\\nimpl DamageCounter {\\n    fn reached_target_damage(&self) -> bool {\\n        self.damage_inflicted > 100\\n    }\\n\\n    fn on_damage_received(&mut self, damage: u32) {\\n        self.damage_inflicted += damage;\\n    }\\n}\\n
\\n

然后开始打怪:

\\n
fn main() {\\n    let mut rng = rand::thread_rng();\\n    let mut counter = DamageCounter::default();\\n    let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\n\\n    for monster in &mut monsters {\\n        monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n    }\\n\\n    while !counter.reached_target_damage() {\\n        let index = rng.gen_range(0..monsters.len());\\n        let target = &mut monsters[index];\\n\\n        let damage = rng.gen_range(0..50);\\n        target.take_damage(damage);\\n\\n        println!("Monster {} received {} damage", index, damage);\\n    }\\n}\\n
\\n

同样地,Rust 代码也有一个在线运行示例

\\n

然而,编译器狠狠地给我们报了四个错误,全都集中在 monster.add_listener() 这一行:

\\n
error[E0596]: cannot borrow `counter` as mutable, as it is a captured variable in a `Fn` closure\\n  --> src/main.rs:47:48\\n   |\\n47 |         monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n   |                                                ^^^^^^^ cannot borrow as mutable\\n\\nerror[E0499]: cannot borrow `counter` as mutable more than once at a time\\n  --> src/main.rs:47:39\\n   |\\n47 |         monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n   |                              ---------^^^^^^^^------------------------------------\\n   |                              |        |        |\\n   |                              |        |        borrows occur due to use of `counter` in closure\\n   |                              |        `counter` was mutably borrowed here in the previous iteration of the loop\\n   |                              cast requires that `counter` is borrowed for `'static`\\n\\nerror[E0597]: `counter` does not live long enough\\n  --> src/main.rs:47:48\\n   |\\n47 |         monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n   |                              ------------------^^^^^^^----------------------------\\n   |                              |        |        |\\n   |                              |        |        borrowed value does not live long enough\\n   |                              |        value captured here\\n   |                              cast requires that `counter` is borrowed for `'static`\\n...\\n60 | }\\n   | - `counter` dropped here while still borrowed\\n\\nerror[E0502]: cannot borrow `counter` as immutable because it is also borrowed as mutable\\n  --> src/main.rs:50:12\\n   |\\n47 |         monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n   |                              -----------------------------------------------------\\n   |                              |        |        |\\n   |                              |        |        first borrow occurs due to use of `counter` in closure\\n   |                              |        mutable borrow occurs here\\n   |                              cast requires that `counter` is borrowed for `'static`\\n...\\n50 |     while !counter.reached_target_damage() {\\n   |            ^^^^^^^ immutable borrow occurs here\\n
\\n

看起来是一团乱麻,让我来翻译翻译,什么叫惊喜

\\n
    \\n
  • 闭包捕获了对 counter 的引用;
  • \\n
  • counter.on_damage_received() 方法接受 &mut self,所以闭包需要 &mut 可变引用。由于这个闭包在循环里,所以对同一个对象的可变引用 &mut 会执行多次,这是不被 Rust 允许的;
  • \\n
  • 我们传入的闭包没有任何生命周期标记,意味着它需要掌控所有包含变量的所有权,所以 counter 需要被移动到闭包内部,而在循环中,重复移动某值就会造成 use of moved value 错误;
  • \\n
  • counter 被移动到闭包内后,我们又尝试在条件语句中使用它,显然也会报错。
  • \\n
\\n

总之,情况不太妙。

\\n

一个经典解决方法是把 DamageCounter 用引用计数指针裹起来,这样我们可以重复使用它了,此外由于我们需要使用 &mut self,所以我们需要使用 RefCell 来在运行时做借用检查(borrow checking),而不是在编译时。

\\n
 fn main() {\\n     let mut rng = rand::thread_rng();\\n-    let mut counter = DamageCounter::default();\\n+    let mut counter = Rc::new(RefCell::new(DamageCounter::default()));\\n     let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\n\\n     for monster in &mut monsters {\\n-        monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n+        let counter = Rc::clone(&counter);\\n+        monster.add_listener(Box::new(move |damage| {\\n+            counter.borrow_mut().on_damage_received(damage)\\n+        }));\\n     }\\n\\n-    while !counter.reached_target_damage() {\\n+    while !counter.borrow().reached_target_damage() {\\n         let index = rng.gen_range(0..monsters.len());\\n         let target = &mut monsters[index];\\n         ...\\n     }\\n }\\n
\\n

这里是改动后的代码

\\n

虽然现在代码可以正常运行了,但是整块代码会被 Rc<RefCell>Vec<Foo>>> 之类的玩意儿搞得乌烟瘴气。而且当代码变得更复杂时,RefCell 也有可能会被可变借用多次。而如果在多线程中使用了 Arc<Mutex<Vec<Foo>>>RefCell 引发 panic 之后,整个程序会死锁。

\\n

所以,一个更好的解决方法是避免在结构体中存储持久化引用。我们对 Monster::take_damage() 稍加改造:

\\n
struct Monster {\\n    health: u32,\\n}\\n\\nimpl Monster {\\n    fn take_damage(&mut self, amount: u32, on_damage_received: impl FnOnce(u32)) {\\n        let damage_received = cmp::min(self.health, amount);\\n        self.health -= damage_received;\\n        on_damage_received(damage_received);\\n    }\\n}\\n\\nimpl Default for Monster {\\n  fn default() -> Self { Monster { health: 100 } }\\n}\\n\\n// 省略了 `DamageCounter` 的代码\\n\\nfn main() {\\n    let mut rng = rand::thread_rng();\\n    let mut counter = DamageCounter::default();\\n    let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\n\\n    while !counter.reached_target_damage() {\\n        let index = rng.gen_range(0..monsters.len());\\n        let target = &mut monsters[index];\\n\\n        let damage = rng.gen_range(0..50);\\n        target.take_damage(damage, |dmg| counter.on_damage_received(dmg));\\n\\n        println!("Monster {} received {} damage", index, damage);\\n    }\\n}\\n
\\n

这里是在线示例

\\n

由于避免了存储回调函数,改造后的代码行数从 62 行下降到了 47 行。

\\n

此外,我们也可以给 take_damage() 加个返回值,这样可以把伤害值放在返回值里,以备后用:

\\n
impl Monster {\\n    fn take_damage(&mut self, amount: u32) -> AttackSummary {\\n        let damage_received = cmp::min(self.health, amount);\\n        self.health -= damage_received;\\n        AttackSummary { damage_received }\\n    }\\n}\\n\\nstruct AttackSummary {\\n    damage_received: u32,\\n}\\n\\n// 省略了 `DamageCounter` 的代码\\n\\nfn main() {\\n    let mut rng = rand::thread_rng();\\n    let mut counter = DamageCounter::default();\\n    let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\n\\n    while !counter.reached_target_damage() {\\n        let index = rng.gen_range(0..monsters.len());\\n        let target = &mut monsters[index];\\n\\n        let damage = rng.gen_range(0..50);\\n        let AttackSummary { damage_received } = target.take_damage(damage);\\n        counter.on_damage_received(damage_received);\\n\\n        println!("Monster {} received {} damage", index, damage);\\n    }\\n}\\n
\\n

当代码复杂度上升时,代码也不会变成一团糟,而且它看起来更“函数式”。

\\n

错用整数类型

\\n

另一个从 C 语言带来的坏毛病是错用整数类型,导致代码里到处都是 usize 的类型转换,尤其是在对数组做索引时。

\\n

C 程序员在初学时就被各种教程教会了使用 int 类型来做索引和循环,当他们开始写 Rust 时,也自然而然地用 Vec<i32> 类型来做数组切片。但是阿 Rust 真的很严格,不让程序员使用 usize 以外的类型对数组、切片和 Vec 进行索引,这就不得不在索引的时候进行一次 i32 as usize 的类型转换。

\\n

Rust 这么做有诸多好处:

\\n
    \\n
  • 无符号类型可以避免负数索引(译者按:Python 程序员请求出战);
  • \\n
  • usize 与普通指针的大小相同,指针运算不会造成隐式类型转换;
  • \\n
  • 内存操作函数 std::mem::size_of()std::mem::align_of() 返回 usize 类型。
  • \\n
\\n

所以,请尽量使用 usize 类型作为可能涉及索引操作的中间变量的首选类型。

\\n

没人比我更懂 unsafe

\\n

每次当我看到 C 程序员使用 std::mem::transmute() 函数或者裸指针来跳过编译器的借用检查时,我都会想起论坛中那条古老的 Rust 圣经:Obstacles, by Daniel Keep

\\n

建议你现在就去读一读,我可以等。(译者按:有空了给大伙翻译一下。)

\\n

你可能已经身经百战见得多了,精通八种编程语言,所以毫无顾忌地破坏 Rust 精心构筑的规则:创建自引用的结构体、用 unsafe 创建全局变量。而且每一次,你都用同样的借口:“这是个单线程程序,所以 static mut 百分之一万没问题”、“这在 C 语言里跑得好好的”。

\\n

unsafe 很微妙,你必须要对 Rust 的借用检查规则和内存模型有深刻的认识才行。我也不想像祥林嫂一样念叨:“未成年人请在编译器监督下编写 unsafe 多线程代码”,但如果你刚开始学这门语言,我衷心建议你耐心从编译器报错的痛苦中慢慢品味 Rust 的美妙。

\\n

当你成为了 Rust 大师,你可以尽情玩弄 unsafe 代码,但在那之前,我还是想告诉你,unsafe 不是杀死编译器报错的板蓝根,也不是能让你自在书写 C 风味 Rust 代码的作弊码。

\\n

不舍得用命名空间

\\n

C 语言中的另一个常见实践是给函数增加所属的库名或者模块名作为前缀,比如 rune_wasmer_runtime_load() 就表示 rune 库的 wasmer/runtime 模块下的 load() 函数。Rust 提供了非常好用的命名空间机制,请尽情使用它,比如刚刚这个函数就可以写成 rune::wasmer::Runtime::load()

\\n

滥用切片索引

\\n

C 语言离不开 for (int i = 0; i < n; i ++),就像西方不能没有耶路撒冷。

\\n

所以下面的 Rust 的代码也就不足为奇:

\\n
let points: Vec<Coordinate> = ...;\\nlet differences = Vec::new();\\n\\nfor i in 1..points.len() [\\n  let current = points[i];\\n  let previous = points[i-1];\\n  differences.push(current - previous);\\n]\\n
\\n

就像呼吸一样自然。然而,就算是老司机也难免会中招下标越界 bug ,尤其当你想在循环里取前一个值时,你就得花心思去考虑 i 是否是从 1 开始的。

\\n

Rust 很担心你,所以拿出了迭代器,切片类型甚至还有 windows()array_windows() 这种高级函数来获取相邻的元素对。上面的代码可以重写为:

\\n
let points: Vec<Coordinate> = ...;\\nlet mut differences = Vec::new();\\n\\nfor [previous, current] in points.array_windows().copied() {\\n  differences.push(current - previous);\\n}\\n
\\n

甚至可以用链式调用来炫技:

\\n
let differences: Vec<_> = points\\n  .array_windows()\\n  .copied()\\n  .map(|[previous, current]| current - previous)\\n  .collect();\\n
\\n

有些人会主张使用了 map()collect 版本的代码更加“函数式“,我则觉得仁者见仁,智者见智。

\\n

不仅如此,迭代器的性能往往比朴素的 for 循环更好,你可以在这里了解原因。

\\n

滥用迭代器

\\n

一旦你用迭代器用上瘾了,你极有可能跑向对立面:拿着迭代器这个锤子,看啥都像钉子。由 mapfilterand_then() 堆叠成的链式调用会让代码可读性下降,而且频繁使用闭包,会让数据类型变得不再直观。

\\n

下面有个例子,演示了迭代器如何让你的代码变得更复杂,你可以读一读这段代码,并猜猜它是干啥的:

\\n
pub fn functional_blur(input: &Matrix) -> Matrix {\\n    assert!(input.width >= 3);\\n    assert!(input.height >= 3);\\n\\n    // 先保存首尾两行,方便后续使用\\n    let mut rows = input.rows();\\n    let first_row = rows.next().unwrap();\\n    let last_row = rows.next_back().unwrap();\\n\\n    let top_row = input.rows();\\n    let middle_row = input.rows().skip(1);\\n    let bottom_row = input.rows().skip(2);\\n\\n    let blurred_elements = top_row\\n        .zip(middle_row)\\n        .zip(bottom_row)\\n        .flat_map(|((top, middle), bottom)| blur_rows(top, middle, bottom));\\n\\n    let elements: Vec<f32> = first_row\\n        .iter()\\n        .copied()\\n        .chain(blurred_elements)\\n        .chain(last_row.iter().copied())\\n        .collect();\\n\\n    Matrix::new_row_major(elements, input.width, input.height)\\n}\\n\\nfn blur_rows<'a>(\\n    top_row: &'a [f32],\\n    middle_row: &'a [f32],\\n    bottom_row: &'a [f32],\\n) -> impl Iterator<Item = f32> + 'a {\\n    // 保存头尾元素,以备后用\\n    let &first = middle_row.first().unwrap();\\n    let &last = middle_row.last().unwrap();\\n\\n    // 获取上中下的 3x3 矩阵来做平均\\n    let top_window = top_row.windows(3);\\n    let middle_window = middle_row.windows(3);\\n    let bottom_window = bottom_row.windows(3);\\n\\n    // 滑动窗口取均值,除了首尾元素\\n    let averages = top_window\\n        .zip(middle_window)\\n        .zip(bottom_window)\\n        .map(|((top, middle), bottom)| top.iter().chain(middle).chain(bottom).sum::<f32>() / 9.0);\\n\\n    std::iter::once(first)\\n        .chain(averages)\\n        .chain(std::iter::once(last))\\n}\\n
\\n

在线示例

\\n

看起来好像并不难,做个均值滤波罢了,不过我这里有个更好的实现:

\\n
pub fn imperative_blur(input: &Matrix) -> Matrix {\\n    assert!(input.width >= 3);\\n    assert!(input.height >= 3);\\n\\n    // 直接从输入值拷贝返回值矩阵,这样就不需要考虑边界条件\\n    let mut output = input.clone();\\n\\n    for y in 1..(input.height - 1) {\\n        for x in 1..(input.width - 1) {\\n            let mut pixel_value = 0.0;\\n\\n            pixel_value += input[[x - 1, y - 1]];\\n            pixel_value += input[[x, y - 1]];\\n            pixel_value += input[[x + 1, y - 1]];\\n\\n            pixel_value += input[[x - 1, y]];\\n            pixel_value += input[[x, y]];\\n            pixel_value += input[[x + 1, y]];\\n\\n            pixel_value += input[[x - 1, y + 1]];\\n            pixel_value += input[[x, y + 1]];\\n            pixel_value += input[[x + 1, y + 1]];\\n\\n            output[[x, y]] = pixel_value / 9.0;\\n        }\\n    }\\n\\n    output\\n}\\n
\\n

在线示例

\\n

我想你的心里已经有答案了吧。

\\n

不会用模式匹配

\\n

让我们回到一开始的 IndexOf() 函数,我们用 Option 类型举了一个很好的例子,先看下原始代码:

\\n
int index = sentence.IndexOf("fox");\\n\\nif (index != -1)\\n{\\n  string wordsAfterFox = sentence.SubString(index);\\n  Console.WriteLine(wordsAfterFox);\\n}\\n
\\n

然后,你可能会看到这样的 Rust 代码:

\\n
let opt: Option<_> = ...;\\n\\nif opt.is_some() {\\n  let value = opt.unwrap();\\n  ...\\n}\\n
\\n

或者这样的:

\\n
let list: &[f32] = ...;\\n\\nif !list.is_empty() {\\n  let first = list[0];\\n  ...\\n}\\n
\\n

这些条件语句都在避免某些边界条件,不过就像之前说到的哨兵值一样,我们在重构的时候依然会极有可能引入 bug。

\\n

而使用 Rust 的模式匹配,你可以保证当且仅当值有效时才会执行到对应的代码:

\\n
if let Some(value) = opt {\\n  ...\\n}\\n\\nif let [first, ..] = list {\\n  ...\\n}\\n
\\n

相比于之前的代码,由于避免了 opt.unwrap()list[index],模式匹配可以有更好的性能(作者的一点忠告:不要在网上听风就是雨,如果你真的想知道真相,建议写个 Benchmark 验证下)。

\\n

别再构造函数后初始化

\\n

许多语言都会在构造对象后调用对应的初始化函数(init() 之类的),但这有悖于 Rust 的约定:让无效状态不可见。

\\n

假设你在写个 NLP 程序,需要加载一个包含所有关键词的词表:

\\n
let mut dict = Dictionary::new();\\n// 读取文件并且把值存到哈希表或者列表里\\ndict.load_from_file("./words.txt")?;\\n
\\n

然而,如果这么写了,意味着 Dictionary 类有两个状态:空的和满的。

\\n

那么如果后续代码假设 Dictionary 有值,并且直接使用它,那当我们错误地对一个空状态的 Dictionary 进行索引时,就会造成 panic。

\\n

在 Rust 中,最好在构造时就对结构体进行初始化,来避免结构体的空状态。

\\n
let dict = Dictionary::from_file("./words.txt")?;\\n\\nimpl Dictionary {\\n  fn from_file(filename: impl AsRef<Path>) -> Result<Self, Error> {\\n    let text = std::fs::read_to_string(filename)?;\\n    let mut words = Vec::new();\\n    for line in text.lines() {\\n      words.push(line);\\n    }\\n    Ok(Dictionary { words })\\n  }\\n}\\n
\\n

Dictionary::from_file() 直接执行了初始化操作,并返回了初始化后的、立即可用的结构体,从而避免了上述问题。

\\n

当然,遇到这种问题的频率因人而异,完全取决于你的编码经验和代码风格。

\\n

一般来讲,函数式语言强调不可变性,所以函数式语言的使用者会天然地掌握这个经验。毕竟当你不能随便改变某个值时,你也不大可能创建一个初始化了一半的变量,然后再用什么其他值去填满它。

\\n

但面向对象的语言就不太一样了,它可能更鼓励你先构造个空对象,然后再调用具体函数初始化它,毕竟对象引用很容易为 null,而且他们也不关心什么可变性之类的玩意儿……现在你知道为啥那些面向对象语言会经常由于 NullPointerException 崩溃了吧。

\\n

保护性拷贝

\\n

不可变对象的一个显而易见的优点时,你永远可以相信它不会发生变化,而放心地使用它的值。但某些语言,比如 Python 或者 Java,不可变性没有传递性。举个例子,x 是个不可变对象,x.y 却不一定是不可变的,除非显式地定义它的可变性。

\\n

这意味着会出现下面的 Python 代码:

\\n
class ImmutablePerson:\\n  def __init__(self, name: str, age: int, addresses: List[str]):\\n    self._name = name\\n    self._age = age\\n    self._addresses = addresses\\n\\n  # 只读属性\\n  @property\\n  def name(self): return self._name\\n  @property\\n  def age(self): return self._age\\n  @property\\n  def addresses(self): return self._addresses\\n
\\n

后来其他人用了这个号称不可变的 ImmutablePerson,但是却不小心把它搞乱了:

\\n
def send_letters(message: str, addresses: List[str]):\\n  # 注意:发信 api 只接受大写字母,所以这里要预处理\\n  for i, address in enumerate(addresses):\\n    addresses[i] = addresses.upper()\\n\\n  client = PostOfficeClient()\\n  client.send_bulk_mail(message, addresses)\\n\\n\\nperson = ImmutablePerson("Joe Bloggs", 42, ["123 Fake Street"])\\n\\nsend_letters(\\n  f"Dear {person.name}, I Nigerian prince. Please help me moving my monies.",\\n  person.addresses\\n)\\n\\nprint(person.addresses) # ["123 FAKE STREET"]\\n
\\n

我承认,这个例子确实有点刻意了,但是修改一个函数的传参却非常常见(译者按:尤其在某些深度学习项目里)。当你知道你自己定义的 ImmutablePersonaddresses 属性不可变时,不会有什么大问题,但是当你和别人协作,而且别人还不知道 addresses 不可变时,那就出大问题了。

\\n

事情也不是无法挽回,解决这个问题的经典方法是,总是在获取属性值的时候,返回它的拷贝,而非它自己:

\\n
class ImmutablePerson:\\n  ...\\n\\n  @property\\n  def addresses(self): return self._addresses.copy()\\n
\\n

这样就可以保证别人在使用该对象的属性时,不会意外改变它的原始值。

\\n

考虑到这篇文章的主题是 Rust,你可能已经猜到了造成这种问题的根本原因:别名与可变性。

\\n

并且你也可能想到,这种情况不会发生在 Rust 中,生命周期机制以及“有且只能有一处可变引用”的机制,保证了程序员无法在取得变量的所有权情况下去改动它的值,也没法显式地使用 std::sync::Mutex<T> 去改变某个共享引用值。

\\n
\\n

备注:你可能见过别人用 .clone() 来处理借用检查器的报错,然后就大喊“你看,Rust 还不是强迫我们做了保护性拷贝措施?”。我想说的是,这种代码基本都是由于程序员不熟悉生命周期机制,或者代码设计有问题导致的。

\\n
\\n

总结

\\n

本文并不能覆盖所有的最差实践,有些是因为我没亲身经历过,有些则是由于没法给出精简的例子。

\\n

衷心地感谢回复我在 Rust 论坛发布的这个帖子的各位同仁,尽管帖子的最后有点跑偏,各位 Rust 老鸟的论战还是让我受益颇深。

\\n"},"31":{"id":31,"date":"2022/01/21","author":"Yidadaa","title":"2021,新世界","mdContent":"*本文约 6000 字,预计阅读耗时 10 分钟。*\\r\\n\\r\\n> 真要是清水一潭也有点可怕。 \\r\\n> 但世界拥挤不堪……妈妈。 \\r\\n> —— 赛博文学 \\r\\n\\r\\n2021 年 1 月 1 日凌晨 0 点 04 分,我发了一条仅两人可见的动态:2021 年的第一分钟,和妮妮拥吻中度过。\\r\\n\\r\\n世界的这场改变了无数人生活的大灾变,像是与我们无关,在这地球上成千上万个我们听不到也看不到的地方敲响跨年钟声的一刻,在这别处有成千上万个人或欢呼、或吵闹、或欢笑、或沉默不语的跨年瞬间,一切都与在这间小小的宿舍里的我们无关,我们只管将浓情蜜意托付彼此,悄然无声地用湿润的舌苔碰触着。\\r\\n\\r\\n成都的雪已经许久没有来了,也或许是来的时候我也没有觉察,正如被厚积云覆盖的四川盆地上空闪烁的星空,虽然时刻闪耀着,我却总不能看到他们。\\r\\n\\r\\n往年,我寄希望于用数据刻画过去的一年,然而生活总是给我新的体悟,它给我的教训如此深刻,以致无法用任何数字衡量。所以,今年暂且用随想的形式记录过去这一年吧。\\r\\n\\r\\n现在,已然奏起了德彪西的月光曲,思绪随音符流淌。\\r\\n\\r\\n### 一、蜜语\\r\\n\\r\\n钱钟书老爷子说:中年人谈恋爱,就像老房子着火,没得救了。\\r\\n\\r\\n我在临近二十四岁的时候第一次遇见爱情,虽说不算是中年,但总体来讲来的是有些晚了,可能老房子还未变老,房里的蛛网还未结起来,木制构件也没被岁月浸透得太过酥碎,所以干柴烈火燃起来的时候,有一些奋不顾身的热烈,也有一些后悔得起的韧性。\\r\\n\\r\\n疫情来临那一年,是缺失的一年,也是遇见爱情的一年,也是因了爱情的浸润,让我失去了审视过去的动力,成了拖延的理由,所以干脆放弃写那年的总结。\\r\\n\\r\\n这也正是恋爱后一年的注脚,如果一个人被什么美好的事情蒙蔽了头脑,他大抵感受不到光脚踩到的石头,也无瑕顾及曾不小心撞到了什么人,犯下了什么错。\\r\\n\\r\\n人生的帆胀满了风,只顾乘风破浪地前行,彩霞在地平线上飘荡着,帆船手的背影满是热烈的欣喜。\\r\\n\\r\\n要从再往前一年的九月份开始谈,故事才好起个头。2020 年 9 月份,我刚从腾讯实习返校,顺便开始在亚马逊的远程实习,学妹因为一些科研琐事来询问。后来我才知道她对科研并不感兴趣,其实我也对科研不感兴趣,她只喜欢画画,科研只为毕业,我只喜欢代码,科研也只为毕业。\\r\\n\\r\\n但那时我们都是台上的演员,演一出勤学好问的师兄师妹情谊好戏,所以只能装模作样地你来我往,对无聊的科研议题指手画脚。\\r\\n\\r\\n但演员之意不在演戏,在乎假戏真做之间。我已觉得人生了无生趣,但在精神归隐之前,还想体验一下鱼水之情,常言道:英雄难过美人关,我倒要看看美人有何把戏。\\r\\n\\r\\n不料却真着了道,我向来对美食无感,但那几个月却跑遍春熙路的各色餐馆,颇有寻医问药之意,只为能与美人幽会。\\r\\n\\r\\n往返美食的电车上,我和学妹之间隔着一个哆嗦的距离,此间暧昧说也说不清,道也道不明,电车就这么事不关己地往前走着,全然不顾两个各怀鬼胎的演员,谈说着貌合神离的天地。\\r\\n\\r\\n“学妹,你可知叔本华的钟摆理论?人生一世,免不了遭受痛苦,叔本华告诫我们,人这一生,恰似钟摆,要么在欲求不满的痛苦中挣扎,要么在欲望满足的无聊中迷失。”,我右手肘摆成标准的直角,电车扶手的力规整地传导到我的身体,整个人随着弯曲的铁轨微微晃动,像是一片在微波中摇摆的、充满哲思的浮萍,眼角却离不开学妹戴了美瞳的眼睛,铁轨两旁的路灯流星一样在她眼里曳尾,在我心里留下各色烙印。\\r\\n\\r\\n“嗯?学长好懂哦,我只觉得人生好苦,上学好累”,学妹目不转睛地盯着手机,手指翻飞,“不过我有个高中同学,他说国庆要来找我玩,想去国色天香游乐园玩,所以过两天不能陪学长了哦”。\\r\\n\\r\\n“是吗,哈哈,这电车会不会开啊,给爷颠得站不稳了都”。我手臂一下子没了力气,竟有宵小之徒从中作梗,岂可修孰不可修,嘴上也没了威风,土龙路到合信路的这段路可真漫长。\\r\\n\\r\\n两天后,我感冒了,学妹从国色天香回来,我在合信路地铁站接她,顺便去龙湖的药店买感冒药。彼时疫情已肆虐多时,药店管制甚严,我俩一言不发地买完药,回到一组团门前,生硬地道别。然而回到寝室,学妹却发信说今天游乐园好无聊,还是想去龙湖玩。\\r\\n\\r\\n于是在 2020 年十月底的某一天,我们站在龙湖十七层的私人影院店门前阳台上闲谈,等着私人影院的预约包间开场。\\r\\n\\r\\n时至今日,我已记不清当时是晴或阴,但在十七楼的阳台远眺校园,上空总是有一团薄雾氤氲,图书馆被光晕笼罩着。\\r\\n\\r\\n2021 年年底,我拿到了招行寄来腾讯大厦的金葵花联名卡,尾号是 0926,看到数字的那一瞬间,我便为世间的巧合感叹:这尾号正是学妹的生日。而我记住这个生日,不仅仅是因为陪她度过了 2021 年的 21 岁生日,而且也因为她在那个阳台上,略带遗憾地跟我说:我原本想在 19 岁结束前脱单,但看来这个愿望并没有实现。\\r\\n\\r\\n后来在私人影院的沙发上,学妹在我的耳边问我:“你是不是喜欢我”。我毫无疑问地说:“是的”。\\r\\n\\r\\n随后,便是:玉楼冰簟鸳鸯锦,粉融香汗流山枕。帘外辘轳声,敛眉含笑惊。柳阴烟漠漠,低鬓蝉钗落。须作一生拼,尽君今日欢。\\r\\n\\r\\n随后半年,也无非是:相见休言有泪珠,酒阑重得叙欢娱,凤屏鸳枕宿金铺。兰麝细香闻喘息,绮罗纤缕见肌肤,此时还恨薄情无?\\r\\n\\r\\n### 二、入世\\r\\n\\r\\n七月,我毕业了。\\r\\n\\r\\n别离是一种愁绪,是不知何时嵌进指尖的刺,吃痛也是在事发之后。\\r\\n\\r\\n如果你在 2021 年上半年去检查郫县男高硕丰七组团某栋三楼的五间宿舍,会发现它们被走廊顶的几根网线连在了一起,这里必然存在着私通网线的苟且之事,而且显而易见的是,我就是始作俑者之一。\\r\\n\\r\\n费拉不堪的校园网一直在承受着它这个角色的双亲本就应当承受的来自莘莘学子们的真挚问候,这在七年前我们来这个鸟不拉屎的地方读本科的时候就已经形成了一个共识。那时我们甚至还不能带自己的电脑,只能徒步两公里横穿除了大而一无是处的校园去寻找附近的网吧。\\r\\n\\r\\n后来这所以电子信息技术教育声名在外的学校,终于允许它的二年级学生携带禁忌之圣物——笔记本电脑来到学校,并开启他们在计算机科学之境抑或是召唤师峡谷的冒险旅途。\\r\\n\\r\\n而我们在这里成为了研究生老油条之后,必然对每月大几十的网费感到不值,并誓将网费下降到每人每月不足十块钱,所以我们便构建了这么一套共享网络机制。我在提出这个提议时,得到了诸位好兄弟的一致同意,毕竟这群憨批也觉得有便宜不占是王八蛋,只不过在构建起这么一个网络之后,走廊上时常会响起他们对共享网络卡顿之优美的溢美之词。\\r\\n\\r\\n后来,我在繁华帝都的十平米单间里使用五倍于之前网速的网络百无聊赖地在某视频网站高速冲浪时,还是会不由自主地怀念起这群傻逼的粗鄙之语。\\r\\n\\r\\n七月之后,我在北京望京利星行某层报到,用秀丽笔签下了第一份正式聘用合同,并且满怀憧憬地和几十万同期的新生代农民工一起准备为建设美好未来而奋斗,如果我当时知道后来半年发生的事情,嘴角的弧度应该会少上几分。\\r\\n\\r\\n我比大部分同龄农民工要幸运一些,可以和同住校园宿舍六七年的老同学继续合租,七月中旬,我拎着行李来到圣鑫家园 2044 入住客厅隔断的单间,然而住在主卧的启迪却忧心忡忡,他说最近帝都严查客厅隔断,昨晚梦见我的房间隔断轰然倒塌,我在灰尘弥漫的客厅痛哭,我说快别他妈放屁了,你什么时候见过我哭。\\r\\n\\r\\n事实也的确如此,我的房间确实安然无恙,然而入住不到一个月之后,我们几个人在太阳宫派出所大厅里焦急地等待阿 SIR 审问完那个花臂东北黑中介,并深刻挂念着自己的几万块房租能否被讨要回来。\\r\\n\\r\\n生活是条不知疲倦的鲇鱼,在时间涡流中翻滚着,它不在乎你是否刚来到这片鱼塘,尽情地用尾巴慰问我们的脸颊。\\r\\n\\r\\n在派出所对峙的前一天,是一个美好的周六,来到帝都之后,除了需要每天加班到晚上十点的峙龙,我们都对自己的工作十分满意,所以那天早上睡梦中的我们,对急促的敲门声毫无防备。\\r\\n\\r\\n第一次敲门,一对有些拘谨的情侣来问我们这里是不是 2044 号房间,我们亲切地告知他们可能走错楼栋了。\\r\\n\\r\\n第二次敲门,还是他们,问我们为什么还没搬走?我们有些疑惑,但还是亲切地告知他们应该是走错房间了。\\r\\n\\r\\n直到第三次敲门,他们拿出了和我们一摸一样的租房合同,我们才意识到都被那个东北花臂黑中介给骗了,之后房东加入战局,好戏开场,大家齐聚太阳宫派出所,把那个中介纠到阿 SIR 面前对峙,才把房租讨要回来。\\r\\n\\r\\n随后,我们火急火燎地找下一个住处,好歹是再次安顿了下来,并感叹社会实在凶险。\\r\\n\\r\\n但社会竟会如此凶险。\\r\\n\\r\\n2021 年 9 月份,国家“双减”政策执行细则出台,我所在的在线教育公司不得不大张旗鼓地裁员,我也如惊弓之鸟,慌忙寻找下家,在第一家公司的两个多月生活如梦一场,如同十六岁无果的恋爱,给得了爱情的模样,却给不了未来。\\r\\n\\r\\n慌乱之中,成都腾讯企微某组组长捞起我的简历,亲切地问候:北京可苦?不如回成都。\\r\\n\\r\\n成都确实好,这里有大豪斯,香串串,以及七年的成都记忆。我眩晕了,心动到彷徨,一发不可收拾,在那不到三个月的时间,在北京的遭遇可谓是灾难,我已对北京彻底失望,所以爽快违约房租合同,帮室友们交了不菲的违约金,只为回到成都怀抱。\\r\\n\\r\\n离京之前,在陕西面馆里,点了一碗胡辣汤,一股血气冲破满脑门,冲破牙龈,在切齿的缝里流淌出来,流淌到商场地板上,流淌到月亮上。时至深秋,太白在天幕的角落躲躲闪闪,像簌然而下的眼泪,在情人的眼角徘徊。加班的人们匆匆闪过,纷纷地,如尾灯在角膜上留下的残影。\\r\\n\\r\\n我只是没有想到,此后的生活只会不断地让人更加失望。\\r\\n\\r\\n我如愿以偿地住上了宽敞房子,与北京十多平米单间一样的价格,在成都可以住上两室一厅,女友对此颇为满意。\\r\\n\\r\\n然而一个多月过后,在不知道第几次十点半下班后,我回到家中,拖着被加班掏空的身体瘫倒在沙发上,已然一具了无生气的尸体,白天组长略含怒气的话语在脑海盘旋,深夜里,突然惊醒,却在回味如何书写公司的项目代码。\\r\\n\\r\\n玉林路旁,旧友相聚,我眼睑低垂,口吐芬芳,咒骂职场艰辛,无比迫切地想要逃离这一切,几杯红啤下肚,烧烤摊却突然停电,我们便踱到道旁。\\r\\n\\r\\n成都仍旧积云密布,暮霭沉沉,小巷里,一束灯光从半空打下,行人来去匆匆,我驻足片刻,便快步离开。\\r\\n\\r\\n电子科大图书馆前的草坪上,伫立着几颗枝繁叶茂的树,天晴时,学生们或在树下乘凉,或在暖阳里依偎;也有晨雾弥漫时,路灯强力的光笼罩整片草坪,空气里没有风,也没有雨,却能真切地嗅到水雾的凉爽气息,那几棵树就这么静静地站着,置身事外地站着,无蝉鸣泣,树影也不婆娑。\\r\\n\\r\\n我恨它们冷漠,恨它们沉默不语,恨它们就只会静静地看着我们,我大吼,你是不是看不到众生痛苦,你是不是视而不见!\\r\\n\\r\\n声音在辽阔的草坪上回旋,消逝在浓雾深处,树们法相庄严,无喜无悲。\\r\\n\\r\\n有些话可以对人说,有些话却只能对树说,但有时对树也开不了口,我的确开不了口,在看到那条暮霭沉沉的小巷中洒下的灯光时,却无端想到树下的欢笑和依偎。\\r\\n\\r\\n我才是傻逼,总奢望在成都的浓雾里看到暖阳,总奢望能刺破云层看到星光。\\r\\n\\r\\n他们总说品学楼是个品字形,我说放屁,这不就是个鸡巴吗?他们笑我粗俗,只会说污秽之语,但是我没有告诉他们,每次洗澡的时候,我都会想起学校的那栋建筑,谁也无法料到,我曾鄙夷的不屑一顾的归属感,竟以这种粗俗的方式永久而强烈地留在了每一天的生活里。\\r\\n\\r\\n### 三、致知\\r\\n\\r\\n生活太苦,日子总是要过,少不了借酒浇愁,代码就是我的酒。\\r\\n\\r\\n我好午夜酗酒,从本科就好这口,凌晨两点灯火阑珊,峙龙躺在床上睡如死猪,我十指飞舞,誓要与键盘大战三百回合,突然峙龙惊醒,骂我键盘太吵,我们进行了一番寝室密友亲切友好的和谐交流,峙龙终于体力不支,再度沉沉睡去。\\r\\n\\r\\n总是有专家发表惊世骇俗言论,说游戏是电子海洛因。我嗤之以鼻,游戏?狗都不玩。代码才是真正的电子海洛因,掌握二十六个英文字母,十几个关键字,你就是计算机的神,什么图灵冯诺依曼迪杰斯特拉,肩膀任你踩踏,拿起键盘,亦如黎明中的花朵,比特于内存之中绽放。\\r\\n\\r\\n我固然喜欢写代码,打工挣钱?行!写代码?行!打工写代码挣钱?当然行!加班打工写代码挣钱?那必然是不行。\\r\\n\\r\\n过度劳动是灵感的毒药,人的大脑是只猛虎,吃不饱睡不足,只能算病猫,只有吃饱歇足了,才是能吃人能下山的猛虎。\\r\\n\\r\\n现在有人过来捏着老虎鼻子灌毒药,老虎指定是要吃人的。\\r\\n\\r\\n2021 的前半年,在学校里一边接私活挣钱,一边谈恋爱,一边写毕设。虽然私活的代码有些枯燥,毕设全靠无中生有,但生活还算滋润。\\r\\n\\r\\n后来入职前司,每日七点下班,稍作休息后即可重振精神,猛虎归来,趁着夜色,可以熟读源码三百行,也可手搓框架四五个。晚上思绪格外活跃,再加上隔三岔五健身保持精力充沛,一切都格外美好。\\r\\n\\r\\n后来便遭遇了那些变故,前文已形之笔墨,此处不谈。\\r\\n\\r\\n新公司极尽福报之所能,在业界声誉斐然,号称“起夜微信不加班,年终百万也枉然”。我在入职前已做足了心理准备,不料还是人民资本家技高一筹,每日奔波劳碌于需求,下班之后再无余力。\\r\\n\\r\\n最终还是下定决心,准备离开这个是非之地,心情便一下愉悦不少。\\r\\n\\r\\n回到代码本身,程序员大致可分为两个派路,一派精研技术,力求登峰造极;一派注重体验,追求创造价值。虽然大部分半吊子程序员都很难归到这两类里,但最近一年还是有幸遇到了几位同仁,有人操练类型体操,有人灵感另辟蹊径,都令我敬佩不已。\\r\\n\\r\\n而我必然是属于半吊子程序员之类。之前一直在做毫无技术含量的外包需求,只能果腹,顺便积累项目经验,却既难精研技术,也难打磨用户体验。\\r\\n\\r\\n遂在工作之后悔过自新,向同仁学习,既有学习业界开源项目重造框架,又在开辟新方向,熟读 Rust 心法,颇有要在 GUI 领域做一番事业之志。\\r\\n\\r\\n谈及代码,免不了文人相轻兼夜郎自大之嫌,所幸有开源社区斧正态度。\\r\\n\\r\\n生活亦不易,当多入技术之境忘我钻研,人生苦短,世间七情六欲难窥其真相,唯诉诸逻辑方得正解。\\r\\n\\r\\n### 四、结\\r\\n\\r\\n行文至此,笔墨竭尽,月光曲落多时,爵士乐曲调逡巡,窗外霓虹灯渐微,建筑楼顶的航空障碍灯错落有致。\\r\\n\\r\\n记忆是四维尺度上的信息,极尽文字之所能,也只能管中窥豹,九牛取之一毛。但记忆的胶卷在脑中过了一圈,我似乎能闻到它一帧帧划过,与输片轮摩擦生热烘焙出的特殊味道,这就是世间滋味吗?\\r\\n\\r\\n过去这一年,我从温暖的大西洋流中挣脱出来,一头扎进冰冷的北冰洋,个中冷暖,涕泪有感。\\r\\n\\r\\n再来到这个崭新的又有些熟悉的世界,彷徨多于笃定,苦痛多于欢愉,甚至路程匆匆,很多人来不及深交,很多答案也来不及求索。\\r\\n\\r\\n请允许我庸俗地将时间之弦跳跃拨动,回到和学妹第一次约定的饭桌上,在我说出“六十岁就想暴毙”之前,她就坦言“四十岁准备暴毙”,不由得感叹:人生天地间,忽如远行客,能寻慷慨赴死伴侣,生亦何欢,死亦何苦?\\r\\n\\r\\n","content":"

本文约 6000 字,预计阅读耗时 10 分钟。

\\n
\\n

真要是清水一潭也有点可怕。
\\n但世界拥挤不堪……妈妈。
\\n—— 赛博文学

\\n
\\n

2021 年 1 月 1 日凌晨 0 点 04 分,我发了一条仅两人可见的动态:2021 年的第一分钟,和妮妮拥吻中度过。

\\n

世界的这场改变了无数人生活的大灾变,像是与我们无关,在这地球上成千上万个我们听不到也看不到的地方敲响跨年钟声的一刻,在这别处有成千上万个人或欢呼、或吵闹、或欢笑、或沉默不语的跨年瞬间,一切都与在这间小小的宿舍里的我们无关,我们只管将浓情蜜意托付彼此,悄然无声地用湿润的舌苔碰触着。

\\n

成都的雪已经许久没有来了,也或许是来的时候我也没有觉察,正如被厚积云覆盖的四川盆地上空闪烁的星空,虽然时刻闪耀着,我却总不能看到他们。

\\n

往年,我寄希望于用数据刻画过去的一年,然而生活总是给我新的体悟,它给我的教训如此深刻,以致无法用任何数字衡量。所以,今年暂且用随想的形式记录过去这一年吧。

\\n

现在,已然奏起了德彪西的月光曲,思绪随音符流淌。

\\n

一、蜜语

\\n

钱钟书老爷子说:中年人谈恋爱,就像老房子着火,没得救了。

\\n

我在临近二十四岁的时候第一次遇见爱情,虽说不算是中年,但总体来讲来的是有些晚了,可能老房子还未变老,房里的蛛网还未结起来,木制构件也没被岁月浸透得太过酥碎,所以干柴烈火燃起来的时候,有一些奋不顾身的热烈,也有一些后悔得起的韧性。

\\n

疫情来临那一年,是缺失的一年,也是遇见爱情的一年,也是因了爱情的浸润,让我失去了审视过去的动力,成了拖延的理由,所以干脆放弃写那年的总结。

\\n

这也正是恋爱后一年的注脚,如果一个人被什么美好的事情蒙蔽了头脑,他大抵感受不到光脚踩到的石头,也无瑕顾及曾不小心撞到了什么人,犯下了什么错。

\\n

人生的帆胀满了风,只顾乘风破浪地前行,彩霞在地平线上飘荡着,帆船手的背影满是热烈的欣喜。

\\n

要从再往前一年的九月份开始谈,故事才好起个头。2020 年 9 月份,我刚从腾讯实习返校,顺便开始在亚马逊的远程实习,学妹因为一些科研琐事来询问。后来我才知道她对科研并不感兴趣,其实我也对科研不感兴趣,她只喜欢画画,科研只为毕业,我只喜欢代码,科研也只为毕业。

\\n

但那时我们都是台上的演员,演一出勤学好问的师兄师妹情谊好戏,所以只能装模作样地你来我往,对无聊的科研议题指手画脚。

\\n

但演员之意不在演戏,在乎假戏真做之间。我已觉得人生了无生趣,但在精神归隐之前,还想体验一下鱼水之情,常言道:英雄难过美人关,我倒要看看美人有何把戏。

\\n

不料却真着了道,我向来对美食无感,但那几个月却跑遍春熙路的各色餐馆,颇有寻医问药之意,只为能与美人幽会。

\\n

往返美食的电车上,我和学妹之间隔着一个哆嗦的距离,此间暧昧说也说不清,道也道不明,电车就这么事不关己地往前走着,全然不顾两个各怀鬼胎的演员,谈说着貌合神离的天地。

\\n

“学妹,你可知叔本华的钟摆理论?人生一世,免不了遭受痛苦,叔本华告诫我们,人这一生,恰似钟摆,要么在欲求不满的痛苦中挣扎,要么在欲望满足的无聊中迷失。”,我右手肘摆成标准的直角,电车扶手的力规整地传导到我的身体,整个人随着弯曲的铁轨微微晃动,像是一片在微波中摇摆的、充满哲思的浮萍,眼角却离不开学妹戴了美瞳的眼睛,铁轨两旁的路灯流星一样在她眼里曳尾,在我心里留下各色烙印。

\\n

“嗯?学长好懂哦,我只觉得人生好苦,上学好累”,学妹目不转睛地盯着手机,手指翻飞,“不过我有个高中同学,他说国庆要来找我玩,想去国色天香游乐园玩,所以过两天不能陪学长了哦”。

\\n

“是吗,哈哈,这电车会不会开啊,给爷颠得站不稳了都”。我手臂一下子没了力气,竟有宵小之徒从中作梗,岂可修孰不可修,嘴上也没了威风,土龙路到合信路的这段路可真漫长。

\\n

两天后,我感冒了,学妹从国色天香回来,我在合信路地铁站接她,顺便去龙湖的药店买感冒药。彼时疫情已肆虐多时,药店管制甚严,我俩一言不发地买完药,回到一组团门前,生硬地道别。然而回到寝室,学妹却发信说今天游乐园好无聊,还是想去龙湖玩。

\\n

于是在 2020 年十月底的某一天,我们站在龙湖十七层的私人影院店门前阳台上闲谈,等着私人影院的预约包间开场。

\\n

时至今日,我已记不清当时是晴或阴,但在十七楼的阳台远眺校园,上空总是有一团薄雾氤氲,图书馆被光晕笼罩着。

\\n

2021 年年底,我拿到了招行寄来腾讯大厦的金葵花联名卡,尾号是 0926,看到数字的那一瞬间,我便为世间的巧合感叹:这尾号正是学妹的生日。而我记住这个生日,不仅仅是因为陪她度过了 2021 年的 21 岁生日,而且也因为她在那个阳台上,略带遗憾地跟我说:我原本想在 19 岁结束前脱单,但看来这个愿望并没有实现。

\\n

后来在私人影院的沙发上,学妹在我的耳边问我:“你是不是喜欢我”。我毫无疑问地说:“是的”。

\\n

随后,便是:玉楼冰簟鸳鸯锦,粉融香汗流山枕。帘外辘轳声,敛眉含笑惊。柳阴烟漠漠,低鬓蝉钗落。须作一生拼,尽君今日欢。

\\n

随后半年,也无非是:相见休言有泪珠,酒阑重得叙欢娱,凤屏鸳枕宿金铺。兰麝细香闻喘息,绮罗纤缕见肌肤,此时还恨薄情无?

\\n

二、入世

\\n

七月,我毕业了。

\\n

别离是一种愁绪,是不知何时嵌进指尖的刺,吃痛也是在事发之后。

\\n

如果你在 2021 年上半年去检查郫县男高硕丰七组团某栋三楼的五间宿舍,会发现它们被走廊顶的几根网线连在了一起,这里必然存在着私通网线的苟且之事,而且显而易见的是,我就是始作俑者之一。

\\n

费拉不堪的校园网一直在承受着它这个角色的双亲本就应当承受的来自莘莘学子们的真挚问候,这在七年前我们来这个鸟不拉屎的地方读本科的时候就已经形成了一个共识。那时我们甚至还不能带自己的电脑,只能徒步两公里横穿除了大而一无是处的校园去寻找附近的网吧。

\\n

后来这所以电子信息技术教育声名在外的学校,终于允许它的二年级学生携带禁忌之圣物——笔记本电脑来到学校,并开启他们在计算机科学之境抑或是召唤师峡谷的冒险旅途。

\\n

而我们在这里成为了研究生老油条之后,必然对每月大几十的网费感到不值,并誓将网费下降到每人每月不足十块钱,所以我们便构建了这么一套共享网络机制。我在提出这个提议时,得到了诸位好兄弟的一致同意,毕竟这群憨批也觉得有便宜不占是王八蛋,只不过在构建起这么一个网络之后,走廊上时常会响起他们对共享网络卡顿之优美的溢美之词。

\\n

后来,我在繁华帝都的十平米单间里使用五倍于之前网速的网络百无聊赖地在某视频网站高速冲浪时,还是会不由自主地怀念起这群傻逼的粗鄙之语。

\\n

七月之后,我在北京望京利星行某层报到,用秀丽笔签下了第一份正式聘用合同,并且满怀憧憬地和几十万同期的新生代农民工一起准备为建设美好未来而奋斗,如果我当时知道后来半年发生的事情,嘴角的弧度应该会少上几分。

\\n

我比大部分同龄农民工要幸运一些,可以和同住校园宿舍六七年的老同学继续合租,七月中旬,我拎着行李来到圣鑫家园 2044 入住客厅隔断的单间,然而住在主卧的启迪却忧心忡忡,他说最近帝都严查客厅隔断,昨晚梦见我的房间隔断轰然倒塌,我在灰尘弥漫的客厅痛哭,我说快别他妈放屁了,你什么时候见过我哭。

\\n

事实也的确如此,我的房间确实安然无恙,然而入住不到一个月之后,我们几个人在太阳宫派出所大厅里焦急地等待阿 SIR 审问完那个花臂东北黑中介,并深刻挂念着自己的几万块房租能否被讨要回来。

\\n

生活是条不知疲倦的鲇鱼,在时间涡流中翻滚着,它不在乎你是否刚来到这片鱼塘,尽情地用尾巴慰问我们的脸颊。

\\n

在派出所对峙的前一天,是一个美好的周六,来到帝都之后,除了需要每天加班到晚上十点的峙龙,我们都对自己的工作十分满意,所以那天早上睡梦中的我们,对急促的敲门声毫无防备。

\\n

第一次敲门,一对有些拘谨的情侣来问我们这里是不是 2044 号房间,我们亲切地告知他们可能走错楼栋了。

\\n

第二次敲门,还是他们,问我们为什么还没搬走?我们有些疑惑,但还是亲切地告知他们应该是走错房间了。

\\n

直到第三次敲门,他们拿出了和我们一摸一样的租房合同,我们才意识到都被那个东北花臂黑中介给骗了,之后房东加入战局,好戏开场,大家齐聚太阳宫派出所,把那个中介纠到阿 SIR 面前对峙,才把房租讨要回来。

\\n

随后,我们火急火燎地找下一个住处,好歹是再次安顿了下来,并感叹社会实在凶险。

\\n

但社会竟会如此凶险。

\\n

2021 年 9 月份,国家“双减”政策执行细则出台,我所在的在线教育公司不得不大张旗鼓地裁员,我也如惊弓之鸟,慌忙寻找下家,在第一家公司的两个多月生活如梦一场,如同十六岁无果的恋爱,给得了爱情的模样,却给不了未来。

\\n

慌乱之中,成都腾讯企微某组组长捞起我的简历,亲切地问候:北京可苦?不如回成都。

\\n

成都确实好,这里有大豪斯,香串串,以及七年的成都记忆。我眩晕了,心动到彷徨,一发不可收拾,在那不到三个月的时间,在北京的遭遇可谓是灾难,我已对北京彻底失望,所以爽快违约房租合同,帮室友们交了不菲的违约金,只为回到成都怀抱。

\\n

离京之前,在陕西面馆里,点了一碗胡辣汤,一股血气冲破满脑门,冲破牙龈,在切齿的缝里流淌出来,流淌到商场地板上,流淌到月亮上。时至深秋,太白在天幕的角落躲躲闪闪,像簌然而下的眼泪,在情人的眼角徘徊。加班的人们匆匆闪过,纷纷地,如尾灯在角膜上留下的残影。

\\n

我只是没有想到,此后的生活只会不断地让人更加失望。

\\n

我如愿以偿地住上了宽敞房子,与北京十多平米单间一样的价格,在成都可以住上两室一厅,女友对此颇为满意。

\\n

然而一个多月过后,在不知道第几次十点半下班后,我回到家中,拖着被加班掏空的身体瘫倒在沙发上,已然一具了无生气的尸体,白天组长略含怒气的话语在脑海盘旋,深夜里,突然惊醒,却在回味如何书写公司的项目代码。

\\n

玉林路旁,旧友相聚,我眼睑低垂,口吐芬芳,咒骂职场艰辛,无比迫切地想要逃离这一切,几杯红啤下肚,烧烤摊却突然停电,我们便踱到道旁。

\\n

成都仍旧积云密布,暮霭沉沉,小巷里,一束灯光从半空打下,行人来去匆匆,我驻足片刻,便快步离开。

\\n

电子科大图书馆前的草坪上,伫立着几颗枝繁叶茂的树,天晴时,学生们或在树下乘凉,或在暖阳里依偎;也有晨雾弥漫时,路灯强力的光笼罩整片草坪,空气里没有风,也没有雨,却能真切地嗅到水雾的凉爽气息,那几棵树就这么静静地站着,置身事外地站着,无蝉鸣泣,树影也不婆娑。

\\n

我恨它们冷漠,恨它们沉默不语,恨它们就只会静静地看着我们,我大吼,你是不是看不到众生痛苦,你是不是视而不见!

\\n

声音在辽阔的草坪上回旋,消逝在浓雾深处,树们法相庄严,无喜无悲。

\\n

有些话可以对人说,有些话却只能对树说,但有时对树也开不了口,我的确开不了口,在看到那条暮霭沉沉的小巷中洒下的灯光时,却无端想到树下的欢笑和依偎。

\\n

我才是傻逼,总奢望在成都的浓雾里看到暖阳,总奢望能刺破云层看到星光。

\\n

他们总说品学楼是个品字形,我说放屁,这不就是个鸡巴吗?他们笑我粗俗,只会说污秽之语,但是我没有告诉他们,每次洗澡的时候,我都会想起学校的那栋建筑,谁也无法料到,我曾鄙夷的不屑一顾的归属感,竟以这种粗俗的方式永久而强烈地留在了每一天的生活里。

\\n

三、致知

\\n

生活太苦,日子总是要过,少不了借酒浇愁,代码就是我的酒。

\\n

我好午夜酗酒,从本科就好这口,凌晨两点灯火阑珊,峙龙躺在床上睡如死猪,我十指飞舞,誓要与键盘大战三百回合,突然峙龙惊醒,骂我键盘太吵,我们进行了一番寝室密友亲切友好的和谐交流,峙龙终于体力不支,再度沉沉睡去。

\\n

总是有专家发表惊世骇俗言论,说游戏是电子海洛因。我嗤之以鼻,游戏?狗都不玩。代码才是真正的电子海洛因,掌握二十六个英文字母,十几个关键字,你就是计算机的神,什么图灵冯诺依曼迪杰斯特拉,肩膀任你踩踏,拿起键盘,亦如黎明中的花朵,比特于内存之中绽放。

\\n

我固然喜欢写代码,打工挣钱?行!写代码?行!打工写代码挣钱?当然行!加班打工写代码挣钱?那必然是不行。

\\n

过度劳动是灵感的毒药,人的大脑是只猛虎,吃不饱睡不足,只能算病猫,只有吃饱歇足了,才是能吃人能下山的猛虎。

\\n

现在有人过来捏着老虎鼻子灌毒药,老虎指定是要吃人的。

\\n

2021 的前半年,在学校里一边接私活挣钱,一边谈恋爱,一边写毕设。虽然私活的代码有些枯燥,毕设全靠无中生有,但生活还算滋润。

\\n

后来入职前司,每日七点下班,稍作休息后即可重振精神,猛虎归来,趁着夜色,可以熟读源码三百行,也可手搓框架四五个。晚上思绪格外活跃,再加上隔三岔五健身保持精力充沛,一切都格外美好。

\\n

后来便遭遇了那些变故,前文已形之笔墨,此处不谈。

\\n

新公司极尽福报之所能,在业界声誉斐然,号称“起夜微信不加班,年终百万也枉然”。我在入职前已做足了心理准备,不料还是人民资本家技高一筹,每日奔波劳碌于需求,下班之后再无余力。

\\n

最终还是下定决心,准备离开这个是非之地,心情便一下愉悦不少。

\\n

回到代码本身,程序员大致可分为两个派路,一派精研技术,力求登峰造极;一派注重体验,追求创造价值。虽然大部分半吊子程序员都很难归到这两类里,但最近一年还是有幸遇到了几位同仁,有人操练类型体操,有人灵感另辟蹊径,都令我敬佩不已。

\\n

而我必然是属于半吊子程序员之类。之前一直在做毫无技术含量的外包需求,只能果腹,顺便积累项目经验,却既难精研技术,也难打磨用户体验。

\\n

遂在工作之后悔过自新,向同仁学习,既有学习业界开源项目重造框架,又在开辟新方向,熟读 Rust 心法,颇有要在 GUI 领域做一番事业之志。

\\n

谈及代码,免不了文人相轻兼夜郎自大之嫌,所幸有开源社区斧正态度。

\\n

生活亦不易,当多入技术之境忘我钻研,人生苦短,世间七情六欲难窥其真相,唯诉诸逻辑方得正解。

\\n

四、结

\\n

行文至此,笔墨竭尽,月光曲落多时,爵士乐曲调逡巡,窗外霓虹灯渐微,建筑楼顶的航空障碍灯错落有致。

\\n

记忆是四维尺度上的信息,极尽文字之所能,也只能管中窥豹,九牛取之一毛。但记忆的胶卷在脑中过了一圈,我似乎能闻到它一帧帧划过,与输片轮摩擦生热烘焙出的特殊味道,这就是世间滋味吗?

\\n

过去这一年,我从温暖的大西洋流中挣脱出来,一头扎进冰冷的北冰洋,个中冷暖,涕泪有感。

\\n

再来到这个崭新的又有些熟悉的世界,彷徨多于笃定,苦痛多于欢愉,甚至路程匆匆,很多人来不及深交,很多答案也来不及求索。

\\n

请允许我庸俗地将时间之弦跳跃拨动,回到和学妹第一次约定的饭桌上,在我说出“六十岁就想暴毙”之前,她就坦言“四十岁准备暴毙”,不由得感叹:人生天地间,忽如远行客,能寻慷慨赴死伴侣,生亦何欢,死亦何苦?

\\n"},"34":{"id":34,"date":"2022/11/07","author":"Yidadaa","title":"如何使用 5000 块组装一台顶配 Mac Studio","mdContent":"> 写给程序员的小尺寸高性能黑苹果主机装配指南,全文约 10000 字。\\r\\n\\r\\n> 本文同步发表至:[FlowUs](https://flowus.cn/yifei/share/cb8f8b2f-591f-4a34-a901-b714a4c81bcc) / [知乎](https://zhuanlan.zhihu.com/p/580506404) / [Github](https://github.com/Yidadaa/Yidadaa.github.io/issues/34) / [博客](https://blog.simplenaive.cn/34)\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374112-bac58562-f71f-4f32-b674-b012a275d56a.png)\\r\\n左:Mac Studio,右:穷逼版\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374176-9667385a-dc3d-414e-86b6-f6c88cb4afc7.png)\\r\\n与 24 寸显示器的对比\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374261-61910710-fe87-40c8-91a8-c59bcd20b5c0.png)\\r\\n与 330ml 杯子的对比\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374347-dadcac5b-fde2-4b7f-a74c-c0e9da1b257b.png)\\r\\n日常使用负载\\r\\n\\r\\n## 长话短说\\r\\n|性能对比|CPU|内存|硬盘|性能(R23)|价格|体积|\\r\\n|-|-|-|-|-|-|-|\\r\\n|Mac Studio|M1 Ultra|64GB|1TB 固态|多核 23705|29999|197mm * 197mm * 95mm = 3.68L|\\r\\n|穷逼版|i7 12700|64GB|1TB 固态|多核 21568|4946|196mm * 218mm * 117mm = 4.9L|\\r\\n\\r\\n- 本文只面向程序员群体,优先保证编译性能和开发舒适度,不面向视频剪辑等创意工作者,不考虑视频剪辑性能;\\r\\n- 重点考虑 CPU 单核和多核性能,优先内存和硬盘容量,考虑机箱体积,不太考虑内存频率,不追求硬盘速度,不太考虑显卡性能,不太考虑噪音,完全不考虑外观精致程度;\\r\\n- 优点:便宜,量大管饱,装机完成后,可当做白苹果使用,无痛升级后续系统,可支持 MacOS、Windows 和 Linux 三系统共存;\\r\\n- 缺点:折腾,组装机箱和安装系统需要有一定动手能力,不适用于日薪大于 1w 的用户,不适用于无计算机硬件常识用户;\\r\\n- 硬件到位之后,在网络良好情况下,半天即可完成装机;\\r\\n- 多核编译同一个项目,M1 Max 需 6min,本文主机需 4min,多核性能对比与 R23 跑分对比大致保持一致。\\r\\n\\r\\n\\r\\n## 阅读须知\\r\\n\\r\\n- 适用群体:\\r\\n - 写代码的\\r\\n - 买不起 Mac Studio 的\\r\\n - 有一定的折腾能力的\\r\\n- 不适用群体:\\r\\n - 打游戏的\\r\\n - 富哥富姐请直接使用钞能力\\r\\n - 剪视频的\\r\\n - 懒得折腾的(可以购置硬件后,购买淘宝黑苹果装机服务)\\r\\n- 本文包含以下内容:\\r\\n - 提供了购买硬件和装机过程中的一些需要考虑的事项\\r\\n - 提供了分别对标 M1 Mac mini、M1 max Mac studio 和 M1 ultra Mac studio 性能(不含 GPU 和磁盘性能)且同等体积的装机购置清单\\r\\n - 提供了安装黑苹果所需的硬件考虑事项以及系统安装须知\\r\\n\\r\\n## 为什么需要装这样一台机器\\r\\n\\r\\n> 可以跳过此章节。\\r\\n\\r\\n总而言之,对于我来说,主要有两个原因:\\r\\n\\r\\n- 主要原因:公司电脑性能拉跨,满足不了开发需要;\\r\\n\\r\\n- 次要原因:目前在用的 MacOS 13 Ventura 系统拉跨,降级麻烦。\\r\\n\\r\\n所以,我需要搞一台高性能主机来替换掉公司发的老旧 MBP。\\r\\n\\r\\n其实公司配的 2020 款 MBP 并不算老,就是性能有点差,10 代标压 i5-1038NG7 处理器加 16GB 板载内存,多核性能大概是 M1 的一半,省着点还是可以用的。\\r\\n\\r\\n但我在 MacOS 13 技术预览版刚放出来时候,就迫不及待地手贱升级了一波,然后成功被新系统的各种卡顿 Bug 和内存占用虚高问题治好了低血压。即便到如今已经发布了正式版,这些问题仍旧存在,以至于日常开发时随便开个 VS Code 再加几个网页,内存就基本见底了。\\r\\n\\r\\n后来为了能保证正常开发,我不得不花一周时间把 Vim 打造成主力 IDE,以便直接在开发机上写代码。\\r\\n\\r\\n此外,除了 MacOS 拉跨,公司的项目本身也对设备性能要求很高。\\r\\n\\r\\n身后的同事老哥前段时间斥巨资入手了一款中配 M1 Max 款 Mac Studio,原因就是公司发放的设备很难满足开发需求。我们日常的技术栈是 C++ 和 React,前者在我那台十代标压 i5 上,全量编译整个项目需要花费 20 分钟到 30 分钟的时间,编译个几次项目,半天就没了,倒是非常适合划水。虽然大多数时间并不会全量编译,但每次增量编译耗时也在分钟级别,非常可观,十分难受。\\r\\n\\r\\n后者 React 更是重量级,NodeJS + HMR + 浏览器堪称内存黑洞,再配合 C++ 建立的 Clang 符号表索引,16GB 板载内存直接被榨干,一天下来大多数时间都在等编译和页面响应。\\r\\n\\r\\n当然这也并不是完全没有好处,由于等待的成本过于高昂,我们写下每行代码前都要深思熟虑,每次编译前都要检查个半天,所以那段时间的代码质量总感觉也高了不少 😆。\\r\\n\\r\\n后来我们安排了一台 36 核 72 线程(含两颗 18C36T Intel Xeon Gold 6240 CPU)的开发机,第一次用它编译项目的时候,有种便秘两周之后突然窜稀的畅快感觉,以前 20 多分钟的编译,现在两分钟就完事了,比以前跑单测都快。\\r\\n\\r\\n然而即便用了开发机和 Vim 之后,我还是经常被系统卡顿和内存不足烦得要死,随便开几个网页,该卡还是卡,就这样将就了两个多月,在某次系统彻底无响应之后,我一咬牙一狠心,决定加亿点钱解决这个问题。\\r\\n\\r\\n于是我便问了下身后老哥的 M1 Max 的编译时间,居然只要 6 分钟,已经完全够用了,怪不得他不怎么用开发机,我马上点开了苹果官网:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374857-d6067e58-1765-4da2-8929-a4b58b24b145.png)\\r\\n\\r\\n然后看到这个价格我火速关闭,那没事了,告辞。\\r\\n\\r\\n看来富哥的路是走不通了,只能使用穷逼的道路曲线救国了。\\r\\n\\r\\n## 性能\\r\\n\\r\\n苹果在发布会上把 M1 系列芯片吹得天花乱坠,拳打 Intel i9-11900,脚踢 RTX 3090,让我们来看看它的真面目:\\r\\n\\r\\n|芯片名称|规格|R23 单核跑分|R23 多核跑分|功耗|\\r\\n|-|-|-|-|-|\\r\\n|Intel Core i5 1038NG7|4 核 8 线程|1116|4979|28W|\\r\\n|Apple M1|8 核|1510|7687|6.8W ~ 39W|\\r\\n|Intel Core i5 12400|6 核 12 线程|1697|11905|65W|\\r\\n|Apple M1 Max|10 核|1533|12337|11W ~ 115W|\\r\\n|Intel Core i7 12700|12 核 20 线程|1879|21568|PL1: 65W, PL2 180W|\\r\\n|Apple M1 Ultra|20 核|1506|23705|13W ~ 215W|\\r\\n|Intel Core i9 12900|16 核 24 线程|1988|26455|PL1: 65W, PL2 200W|\\r\\n\\r\\n确实很强,比 10 代 intel 强多了,但是面对 12 代 intel 还是不太行,毕竟功耗在这摆着,i9 12900 勉强守住了桌面 CPU 的底裤。\\r\\n\\r\\n苹果 M1 系列的能耗比确实惊人,但是我要放在公司使用,完全不用担心电费问题,功耗都是图一乐,于是经过一周的调研,确定了硬件配置单。\\r\\n\\r\\n## 硬件配置单\\r\\n\\r\\n> 下列配置中的硬件,除显卡和网卡外,均为全新价格。\\r\\n\\r\\n> 如需查看更多配置单(M1 / M1 Max 级别),请跳转到文章末尾。\\r\\n\\r\\n|部件|产品|价格|备注|\\r\\n|-|-|-|-|\\r\\n|CPU|Intel i7 12700f 散片|2110|f 后缀代表不集成核显,比带核显版本便宜 100 块左右|\\r\\n|主板|铭瑄 H610itx 挑战者|569|最便宜的能带得动 i7 12700 的 itx 主板|\\r\\n|内存条|酷兽 32GB 3000Mhz 两根共 64GB|798|最便宜的 DDR4 内存,有 2666mhz / 3000mhz / 3200Mhz 三个版本,价格一样,哪个便宜有货买哪个|\\r\\n|硬盘|光威弈 Pro 512GB nvme ssd|295|随便选的,可以加一百多块上 1TB 固态,反正便宜|\\r\\n|散热器|利民 Axp90 x53 + LGA17xx 扣具|159|选购时需要考虑机箱适配的散热器高度,需要加 18 元额外购买利民 LGA17xx 扣具|\\r\\n|机箱|傻瓜超人 K39 4.9L + 显卡延长线|199|196mm * 117mm * 218mm = 4.9L 体积,当前配置的体积最小最便宜的选择|\\r\\n|电源|益衡 7030B 300W Flex 1U 电源|398|这个价格是加 100 块让店家换全模组电源线和静音风扇之后的版本,组装的时候理线会更方便|\\r\\n|显卡|AMD RX550 2GB/4GB Polaris 核心|300|二手亮机卡,有 Lexa 和 Polaris 核心两个版本,建议买 4GB 显存 Polaris 核心版本,各版本价格无区别|\\r\\n|网卡|博通 BCM94360CS2 + m2 ngff 正向转接卡|118|也可以选择 Intel AX201 或者更低端的 Intel 9560,能省个几十块钱,但需要额外配置驱动,没必要|\\r\\n\\r\\n我们一项一项来看,为什么要选用这些配置。\\r\\n\\r\\n### CPU 和主板\\r\\n\\r\\n首先,要再次明确一下目标,我们想要 M1 Ultra 级别的性能,那就只能在 i7 12700 及以上的 CPU 中选,大概有这么几个选项:\\r\\n\\r\\n- Intel i7 12700f 和 12700\\r\\n\\r\\n- Intel i7 12700kf 和 12700k\\r\\n\\r\\n- Intel i9 12900f\\r\\n\\r\\n- Intel i9 12900kf 和 12900k\\r\\n\\r\\n**为什么不选 AMD 的 CPU?**因为性价比不高,12 代 CPU 同价位性能比 AMD 5000 系列同价位性能稍强,价格稍便宜,而且目前 AMD 装黑苹果需要额外配置,有点麻烦,所以完全没有理由选 AMD。\\r\\n\\r\\n**为什么不选 13 代 Intel CPU?**因为性价比不高,目前只发布了带 K 后缀的高性能版本,需要搭配更好的主板和电源,而且 13 代 CPU 也相对更贵。\\r\\n\\r\\n然后,i7 和 i9 分别都有 f / k / kf 几个版本:\\r\\n\\r\\n- 其中凡是带 f 的,都是不带集成显卡的版本,考虑到我们是装黑苹果,12 代集成显卡不能被驱动,所以必须得配个独立显卡,选择 f 版本就行,可以省下 100 块;\\r\\n\\r\\n- 其中凡是带 k 的,都代表着性能增强版,大概比不带 k 的版本性能强 10% - 15%,价格也要贵 5% - 10%,酌情购买。\\r\\n\\r\\n由于 i9 处理器需要主板有更强的供电能力,主板的价格就水涨船高,所以我们退而求其次,选择 i7 12700,性能只比 M1 Ultra 弱 10%。当然,也可以加点钱上 12700k,这样性能和 M1 Ultra 持平,不过这点差距在实际使用时基本没什么区别,12700k 对主板供电能力也有一定的要求,丐板估计带不动。\\r\\n\\r\\n主板就不提了,铭瑄 H610itx 在 600 块价位基本无敌,找不到其他能一起打的。唯一缺点就是 itx 主板接口有点少,只能插两根内存条,目前消费级内存条单条最大也就 32GB,所以最多只能上 64GB 内存。\\r\\n\\r\\n而且这种低端丐板没有额外的 M.2 接口,只能插一条 Nvme 固态,要扩容的话,要么换固态,要么加 SATA 硬盘。不过还好 512GB SSD 够用了,即便是 1TB 的 SSD 也只要 400 出头,可以自己决定要不要加钱。\\r\\n\\r\\n购买建议:\\r\\n\\r\\n- 购买散片板 U 套装即可\\r\\n\\r\\n### 内存条和硬盘\\r\\n\\r\\n酷兽和光威是同一个厂,不同的产品线,酷兽主打性价比,399 块的 32GB 内存条要啥自行车,直接来两条插满。\\r\\n\\r\\n此外这个内存条有 2666mhz / 3000mhz / 3200mhz 三个频率版本,理论上铭瑄 h610itx 主板是支持 3200mhz 高频内存的,只需要在 BIOS 里设置一下就行,但是说实话内存频率感知不强,只有核显打游戏才对内存频率有要求,如果用来开发,随便买就行,高频内存属实浪费预算,哪个有货买哪个。\\r\\n\\r\\n最近固态硬盘也是白菜价,1tb 固态已经降到了 400 以下,当然也有更贵的性能更好的版本,但是对于开发来说感知不强,3000mb/s 和 7000mb/s 虽然看起来差距挺大,但是实际用的时候也就几百毫秒的差距,感知不出来,所以随便买个便宜的就行,重点还是看容量。\\r\\n\\r\\n购买建议:\\r\\n\\r\\n- 哪个便宜买哪个,频率和读写速度并不重要;\\r\\n- Mac Studio 的统一内存,官方读速是 800GB/s,与 3200mhz DDR4 内存条读速差不多;\\r\\n- 如果磁盘性能也想对标一下 Mac Studio,Mac Studio 硬盘速度为:读 5000MB/s,写 4000MB/s,大致与 PCIE 4.0 固态速度接近,所以可以酌情购买 PCIE 4.0 固态硬盘(铭瑄的这块主板支持,精粤的不支持),同体积大概是 PCIE 3.0 硬盘的 1.5 倍价格,512GB 版本大概 400 块可以搞定。\\r\\n\\r\\n\\r\\n### 散热器、机箱和电源\\r\\n\\r\\n这三兄弟要一起看,其实显卡也应该拿过来一起看,但是开发场景,显卡不是很重要,所以显卡另说吧。\\r\\n\\r\\n之所以要放一起看,主要是尺寸问题,我想要体积尽可能接近 Mac Studio,Mac Studio 的体积是 197mm * 197mm * 9.5mm = 3.68L,一开始我买的是下面这个机箱:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375317-17a17f5d-b8ae-424d-aab5-3cf3054e979e.png)\\r\\n\\r\\n整体尺寸是 187mm * 230mm * 97mm = 4.2L,非常小巧。\\r\\n\\r\\n我选择了其中的 A 背板版本,没有显卡位,把空间全部让给散热器和电源,散热器也是买的 Axp90 x53(90mm 宽,53mm 高)。\\r\\n\\r\\n因为我一开始并没有打算装黑苹果,想直接用 Linux 来开发,但是后来发现 Linux 没办法入公司内网,而且企业微信也是残废状态,所以才考虑装黑苹果,于是换成了下面的机箱:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375363-2379fe91-e33e-45f3-816a-1c58aad6c11a.png)\\r\\n\\r\\n体积大了一圈,来到了 196mm * 117mm * 218mm = 4.9L,不过好在便宜,加显卡延长线才 200 块,比上一个 300 块的机箱便宜很多,而且可以加装一个 19cm 以内的双槽短卡,非常适合本文的需求。\\r\\n\\r\\n从上面机箱的商品介绍就可以知道,这个机箱支持 56mm 以内的散热器,一般来讲,散热器的尺寸与散热能力正相关,基本上体积越大,散热能力越好,在看了一圈视频之后,出现次数的就是利民的axp90 系列的几个散热器,价格在 100 元到 150 元之间:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375397-361c99d5-7cf0-489b-837e-f4fb5244390f.png)\\r\\n\\r\\n可以看到主要有 36mm / 47mm / 53mm 三种高度,名字 AXP90 则代表 90mm * 90mm 的尺寸,为了最大化机箱利用率,我选用了 53mm 高度版本,实测整机满载温度可以控制在 80 摄氏度以内,日常使用则只有 27 摄氏度左右,好悬没给 CPU 给冻感冒。\\r\\n\\r\\n此外,如果你想选用一些更小尺寸的机箱,在选散热器的时候一定要注意,最好预留 3mm 以上的空间余量,比如如果限高 56mm 的机箱,如果配了一个 55mm 高度的散热器,风扇叶片和机箱离得太近,会出现十分明显的啸叫。\\r\\n\\r\\n然后是电源,前面了解到 i7 12700 的满载功耗是 180w,再加上 50w 亮机显卡的功耗,总计 230w 的实际载荷,按照 80% 的电源转化率,我们只需要选择 290w 以上的电源即可满足需求。在尺寸方面,由于我们想让机箱的体积尽可能的小,而小尺寸 itx 机箱往往搭配使用小 1u 尺寸的 Flex 电源,这类电源的价格要比常规尺寸的电源稍贵一点,如果想要省钱,可以选购支持 SFX 电源的更大尺寸的机箱。\\r\\n\\r\\n最终我选择了益衡的 7030B 300w 电源:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375452-b24da35f-415b-48cc-8953-af0ebe584500.png)\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375491-e805645b-fc76-4d2c-957f-03bb3a463178.png)\\r\\n\\r\\n小 1u 电源还有个缺点是会比较吵,因为受体积限制,风扇必须提高转速才能保证散热,运行时会有比较明显的高频噪声,所以我选择加了 100 块,买了店家改装之后的版本(上图右侧)。\\r\\n\\r\\n改装版本换了更静音的风扇,并且配备了全模组电源线,大概长下面这个样子:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375578-2df51c37-0c56-486c-ba22-d36fd79793b7.png)\\r\\n\\r\\n电源线统一成了黑色的软线,比原装的五颜六色的硬线好看多了,而且装机的时候理线会非常简单,这一点在小机箱里尤其关键,所以这 100 块还是挺值的。\\r\\n\\r\\n购买建议:\\r\\n- 购买全模组小 1u 电源,考虑购买全模组 + 静音风扇的改装版本\\r\\n- 购买散热器时注意机箱散热器限高\\r\\n- 利民散热器默认附带的主板支架只能与上一代 Intel CPU 搭配使用,需要额外花费 18 元购买利民 LGA17xx 扣具\\r\\n\\r\\n### 显卡\\r\\n\\r\\n显卡需要着重讲一下,因为 MacOS 非常挑显卡,目前市售的 RTX 系列显卡基本都是驱动不了的,只要一些比较老的 Nvidia 显卡才能驱动。而大多数 AMD 显卡都能驱动,比如最近出的 6600 和 6700,但是 6400 除外,详细的 GPU 列表可以[查看这里](https://dortania.github.io/GPU-Buyers-Guide/modern-gpus/amd-gpu.html#native-amd-gpus)。\\r\\n\\r\\n由于我们并不需要特别强劲的显卡性能,所以只需要选择能够正常驱动的低功耗亮机卡即可,我一开始就直接从比较老的 R7 和 R9 系列显卡开始看:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375686-da4a53e8-daf6-4927-87dd-b5586b5628cc.png)\\r\\n\\r\\n可以看到有非常多的选项,我在淘宝搜了一圈之后,发现最低端的 R7 240/250 显卡只要 100 块,于是就直接下单买了一块,结果入手后折腾了许久才发现,这张 2013 年发售的老显卡,最高只能运行在 MacOS 10.x 系统上,最新的 12.x 系统是没法完美驱动的。我花了大量的时间搜索国内外关于这张 Oland 核心的显卡驱动帖子,没有发现任何人能成功在 11.x 及以上的系统上成功运行过它,虽然可以使用仿冒显卡 ID 的方法成功安装并进入系统,但是却无法使用 Metal GPU 加速,导致系统所有的高斯模糊效果以及动效都是缺失的,完全无法满足日常使用。\\r\\n\\r\\n于是我就又斥巨资花了 300 块买了一张 Rx550,这张显卡发售于 2017 年,网上有相当多的黑苹果视频表示这张卡可以完美工作在最新的 MacOS 13.0 系统上,而且完全不需要任何额外操作,插上就能用。当然,前提是买到了正确的版本的显卡,这张显卡有两种核心版本:Lexa 核心和 Buffin 核心(或称 Polaris 核心),其中 Buffin(Polaris) 核心是可以即插即用,但是 Lexa 核心则需要[仿冒参数](https://www.bilibili.com/read/cv15800495)才能工作。\\r\\n\\r\\n很不幸我买的这张 Rx550 2GB 联想拆机卡是 Lexa 核心,不过由于有了之前折腾 R7 240 的经历,对我来说还算简单。\\r\\n\\r\\n此外这张显卡有 2GB 显存和 4GB 显存两个版本,价格基本一样,我个人建议购买 4GB 版本,因为我在实际使用时发现,MacOS 12.x 系统很容易就把 2GB 显存吃满了,所以能买大的还是买大点的。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375752-81118659-e6fe-443e-bf82-bad353cf5940.png)\\r\\n\\r\\n盈通的这张 RX550 4GB 就很适合,虽然是二手,有翻车的风险,但是我们并不会用来打游戏,所以只要它能点亮就行,反正日常负载不会太高,暴毙的可能性比较小。\\r\\n\\r\\n如果实在担心翻车,可以加点钱买全新版本,这张卡有全新正品在售,价格在 500 块到 600 块之间:\\r\\n\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375812-0c4657dc-fded-407c-836b-132ded517957.png)\\r\\n\\r\\n购买建议:\\r\\n\\r\\n- 购买 RX550 4GB Polaris 核心版本\\r\\n- 省钱就买二手,图稳就加点钱买全新\\r\\n- 购买显卡时注意显卡尺寸是否兼容机箱\\r\\n\\r\\n### 无线网卡\\r\\n无线网卡也值得花点篇幅来说一说,目前黑苹果无线网卡可选 Intel NGFF 接口的一系列网卡,以及搭配转接板使用的博通 BCM 系列苹果拆机显卡:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375892-727c87ae-e39e-48eb-8dfb-fe4495c209cf.png)\\r\\n\\r\\n如果从省事的角度来看,直接买博通的拆机网卡即可,插上就能用。如果想省点钱,就买 Intel 最低端的 3168 系列:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375922-f435efa8-1651-462d-bfb6-8cddf0228072.png)\\r\\n\\r\\n其实在几年前,Intel 的无线网卡是没办法在苹果系统上使用的,后来有个大佬搓了一个驱动出来,然后 Intel 几乎所有型号的无线网卡一夜之间焕发第二春,具体的支持列表可以查看下面的链接:[Compatibility | OpenIntelWireless](https://openintelwireless.github.io/itlwm/Compat.html#dvm-iwn)。\\r\\n\\r\\n此外无线网卡的天线,有内置和外置两种区别:\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n\\r\\n两者的区别:\\r\\n- 外置天线是机箱后面的小尾巴,看起来可能不太美观,但是信号比较稳定,增益较强;\\r\\n- 内置天线则需要粘在机箱上,容易受机箱内部杂波干扰,增益较小,信号不太稳定,但由于不需要伸出机箱,所以会比较美观一点。\\r\\n\\r\\n我选择了 Intel 9560 内置天线版本,花费 48 块,实装之后可以完美使用 WIFI 和蓝牙,隔空投送也可以正常使用,但是内置天线信号不稳定,蓝牙连接妙控板非常卡顿,基本无法日常使用。而且由于网卡规格较低,虽然标称 2.4G 300Mbps / 5G 1733Mbps,但实测只能跑到 50Mbps,可能最大的原因还是这个内置天线的信号太差,干扰太多。\\r\\n\\r\\n不过问题不大,这个主机的 WIFI 纯粹是为了应付公司的入网认证,认证完了之后就直接关掉用有线连接了,如果你对无线网络有需求,建议购买高规格的外置天线版本。\\r\\n\\r\\n购买建议:\\r\\n- 购买博通 BCM 94360CS + 正向转接卡 + 外置天线版本,不用折腾,信号更好。\\r\\n\\r\\n\\r\\n## 装机\\r\\n\\r\\n十分推荐先观看此视频:[DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!](【DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!】 https://www.bilibili.com/video/BV1v54y1Z7Er/?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9),视频中的配置与本文配置基本一致,可以参考。\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n先上所有硬件的全家福,图中不包含显卡和网卡,因为拍摄照片的时候显卡和网卡还没到。\\r\\n\\r\\n### 安装 CPU\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n主板说明书会提示如何安装 CPU,一定要小心不要弄坏了针脚,主板上的金属压杆会比较紧,确认 CPU 按照防呆口安装妥当后,稍加用力按下压杆,将 CPU 固定好即可。\\r\\n\\r\\n### 上电测试\\r\\n先别急着往机箱里塞,把内存条和硬盘插好之后,连接电源线和键鼠,用螺丝刀碰触主板上的开机引脚,显示器能够显示 BIOS 界面,则表示成功点亮。\\r\\n\\r\\n### 安装散热器\\r\\n按照散热器说明书进行安装即可,注意需要额外购买利民 LGA17xx 扣具。\\r\\n\\r\\n### 安装机箱\\r\\n按照机箱说明书安装即可,建议先安装电源,再安装主板,然后接电源线,安装显卡延长线,安装显卡。\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n## 成品展示\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n最终的成品大概比 Mac Studio 大了一圈,当然精致程度是没法比的,如果想要精致,可以加钱买更贵的全铝合金一体成型机箱(5.6L 体积):\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200376871-9d4540ef-a61e-44cf-b8f7-576fc212d69e.png)\\r\\n\\r\\n## 系统\\r\\n可以直接跳转到流程清单部分,如果你不需要安装 MacOS,可以无视下列内容。\\r\\n\\r\\n### 为什么不用 Windows 和 Linux?\\r\\n在最开始决定装机的时候,我就在考虑要使用什么操作系统作为主力开发环境,前文已经提到我需要频繁编译,所以编译性能是我首要考虑的问题,这样 Window 肯定就不能用了,Windows 下 C++ 编译性能一向令人诟病,即便有了 WSL,由于它运行在 Hyper-V 虚拟机中,导致 IO 性能跟不上,实际的性能大概是 Linux 原生的 80% 左右,直接损耗了 20% 的性能,要知道 Intel 的 CPU 一次代际升级的性能也才差不多 25%,Window 直接就让 CPU 性能倒退一代,这绝对是无法接受的。\\r\\n\\r\\n我随后的想法是使用 Linux 作为日常开发系统,并使用 KVM 安装 Windows 虚拟机来解决常用软件问题,我也确实尝试了这种做法,并依次使用了下列 Linux 发行版:\\r\\n\\r\\n- 使用 KDE 桌面的 Debian,由于 Stable 版本的 Linux 内核太老,导致无法在 12 代 Intel 平台上正常安装,尝试几次后均未果,作罢;\\r\\n- 使用 KDE 桌面的 Ubuntu,也就是 KUbuntu,这个发行版确实很不错,KDE 对高分屏的支持非常不错,而且窗口主题和动效都很细腻,我用了大概一周时间,在上面使用 KVM 分别安装了 Windows 10 LTSC 以及 Mac OS 12 的虚拟机,前者用于运行企业微信,后者则是装来玩的,由于没有独显直通,Mac OS 虚拟机的运行流畅度十分感人。在 Linux 上运行 Windows 软件的另一个方法是使用 Wine,但无奈兼容性欠佳,无法正常运行最新版本的企业微信,微信倒是可以正常使用;\\r\\n- 使用 Gnome 的 Ubuntu,Gnome 对高分屏的支持太差劲,大多数应用没有对 Wayland 做兼容,甚至浏览器在分数倍缩放下都很糊,而且同样无法通过 Wine 运行企业微信,作罢;\\r\\n- 最后是国产的 Deepin,应该是最贴近日常使用的 Linux 发行版了,应用商店里有 Deepin 团队维护的 Wine 版微信和企业微信,但是企业微信还有点问题,虽然可以正常聊天,但是邮箱界面始终白屏,另外 Tim 也是卧龙凤雏,可以安装但无法启动。\\r\\n\\r\\n我大概用了一周多的 Deepin,并成功在上面编译运行了公司的项目,编译时间大概是开发机的 2 倍速度,可以看到 i7 12700 的多核性能相当强悍。\\r\\n\\r\\n但是网络问题始终没有办法很好的解决,公司的内网认证软件是阿里出品的阿里郎的阉割版本,其入网认证条件非常严格,无法在 Windows 虚拟机中完成认证,更不用提 Wine 了,也没有提供 Linux 版本,导致我的主机就算插上网线也无法访问内网资源,而且这个软件入网必须要有无线网卡,大多数 USB 网卡都无法在 Linux 下免驱运行。\\r\\n\\r\\n为了解决这个问题,我琢磨了很久计算机网络相关的知识,大概有这么几种方法:\\r\\n\\r\\n- 最容易想到的方法自然是用公司的苹果笔记本入网,然后当跳板机开代理给主机用,确实可以用,但是基于代理的方法,无法保证主机的所有流量都走代理,某些应用可能会绕开系统的代理策略,自行建立连接,这种方法不太稳定;\\r\\n- 然后比较容易想到的是使用 Mac 的网络共享,无奈在入网认证使用 802.1x 的安全策略,系统会直接禁用掉网络共享,这条路也走不通;\\r\\n- 最后想到的方法时把 Mac 模拟成一台路由器,直接网线直连主机,然后手动分配 IP 地址即可,经过一番搜索,其实只要开启 Mac 的 IP 路由转发即可,然后将来自 USB 网卡的所有流量都转发到无线网卡上,即可大功告成。\\r\\n\\r\\n关于折腾 Linux 日用开发环境的记录,可以看这篇文章:\\r\\n\\r\\n[Linux 开发环境备忘录](https://flowus.cn/5a11ca75-455d-4a67-840a-42939ef91c2c)\\r\\n\\r\\n最后由于 Linux 上无法找到顺手的快速切换窗口的软件(类似 Mac Spotlight 的功能),还是决定安装黑苹果,可以直接无缝复用 MBP 的那一套工作流。\\r\\n\\r\\n### 主要工具:OpenCore\\r\\n安装黑苹果是一件非常需要耐心的事情,目前主流的安装黑苹果的工具是 OpenCore,其官方教程非常详细,流程也非常长,新手看了也非常头大:\\r\\n\\r\\n[OpenCore Install Guide](https://dortania.github.io/OpenCore-Install-Guide/)\\r\\n\\r\\n在翻阅许久之后,我决定放弃跟着官方教程走,官方手册的内容只适合当作速查表来使用,如果跟着从零开始做,不知道搞到猴年马月。\\r\\n\\r\\n先列举一下我认为比较有用的内容,首先是显卡购买指南:\\r\\n\\r\\n[Introduction | GPU Buyers Guide](https://dortania.github.io/GPU-Buyers-Guide/)\\r\\n\\r\\n里面列举了各种可以用于黑苹果的显卡型号,以及它们支持的系统版本,当然这个指南中的某些老显卡的信息是错的,比如我前面提到的 R7 240,手册上说可以在最新的系统中通过仿冒 ID 的方式运行,但其实只能在 OS 10.x 以前的系统上运行,不过大多数信息都是准确的,可以放心参考。\\r\\n\\r\\n其次是 GPU 仿冒 ID 指南,如果你想折腾一下不直接免驱的显卡,可以参考它来仿冒 ID:\\r\\n\\r\\n[Renaming GPUs (SSDT-GPU-SPOOF) | Getting Started With ACPI](https://dortania.github.io/Getting-Started-With-ACPI/Universal/spoof.html)\\r\\n\\r\\n我个人不推荐用这种方式来搞,太费时间,而且不一定有用,还是建议直接购买免驱显卡。\\r\\n\\r\\n### 流程清单\\r\\n如果自己搞不定,可以直接在淘宝购买黑苹果装机服务,远程手把手指导,价格在 100 元到几百元不等。\\r\\n\\r\\n可以看这个视频,了解大概步骤:[刷新记录!12代平台12400F仅需24秒即可完成黑苹果系统安装,请问还有谁?](https://www.bilibili.com/video/BV1K341137pj/?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9)\\r\\n\\r\\n通用流程:\\r\\n\\r\\n- 硬件准备:\\r\\n - 16GB 及以上容量的内存卡或者移动硬盘,并格式化为 FAT32 格式;\\r\\n - 一台 Windows 电脑(Linux 或 Mac 也行);\\r\\n - 组装好的等待安装系统的主机;\\r\\n - 良好的有线网络环境;\\r\\n - 耐心。\\r\\n- 准备 EFI 引导文件:[各版本 EFI 引导文件](https://flowus.cn/6bb40d7b-8da6-464b-a551-0d0f0d663746)\\r\\n - 如果你的硬件配置是本文推荐的配置,那么直接下载上面列表中的安装包,解压即可;\\r\\n - 或者你可以在 Github 上搜索自己的主板名称,比如我就参考了这个仓库([h610itx + i5 12400 + rx560 + bcm943602cs 网卡](https://github.com/Crack-DanShiFu/Hackintosh-MAXSUN--H610ITX-I512400-rx560))和这个仓库([h610itx + i5 12490f + rx560 + intel ax201 网卡](https://github.com/LimeVista/Hackintosh-H610-12490F-AX201)),在他们的基础上进行了少量修改,就适配了本文的硬件;\\r\\n - 经过上述步骤,你会获得一个 EFI 文件夹,将其拷贝到 U 盘或者移动硬盘中即可,如果你的硬件与上述现有的配置都不一样,请查阅常见问题章节。\\r\\n- 准备基础安装镜像:\\r\\n - OpenCore 提供了一个很细致的教程来下载安装镜像,基础镜像大概 600MB 左右,按照这个教程([在 Windows 上下载安装镜像](https://dortania.github.io/OpenCore-Install-Guide/installer-guide/winblows-install.html#downloading-macos))下载镜像文件并复制到 U 盘即可。\\r\\n - 将 U 盘插入主机,按下 DEL 键进入主板的 BIOS 界面,如果你使用了其他主板,请自行搜索如何进入 BIOS,然后将 U 盘设置为第一启动项即可;\\r\\n- 启动启动后进入安装界面:\\r\\n - 选择磁盘工具,将 SSD 所在盘符格式化为 APFS 格式,并命名为 MacOS(或者其他任何英文名);\\r\\n - 退出磁盘工具,点击安装 MacOS,进入常规安装流程;\\r\\n- 在网络良好情况下,自动重启若干次后,大概 40 分钟即可安装成功;\\r\\n- 安装成功后,自动重启进入系统选择界面,选择 MacOS 盘符进入即可;\\r\\n- 将 EFI 文件夹复制到 SSD 硬盘上,可参考这个教程:[完善引导](https://apple.sqlsec.com/5-%E5%AE%9E%E6%88%98%E6%BC%94%E7%A4%BA/5-6.html);\\r\\n- 到现在,你已经可以正常使用 MacOS,但是还无法使用 Apple ID 和 iCloud,你还需要生成自己的硬件序列号,请参考这个教程:[为自己的黑苹果生成随机三码](https://sleele.com/2019/03/21/smbios/);\\r\\n- 至此,大功告成。\\r\\n\\r\\n## 常见问题\\r\\n\\r\\n### 如何查看我买到的显卡的核心?\\r\\n\\r\\n#### 搜索\\r\\n\\r\\n1. 优先询问卖家,其次在搜索引擎中搜索显卡型号 + 核心即可。\\r\\n\\r\\n#### 使用 PE 工具\\r\\n\\r\\n1. 下载安装[微 PE 工具箱](https://www.wepe.com.cn/download.html),并安装到 U 盘上;\\r\\n2. 下载 GPU-Z:[https://www.techpowerup.com/gpuz/](https://www.techpowerup.com/gpuz/),复制到已经安装了 PE 工具的 U 盘中;\\r\\n3. 进入 BIOS,将 U 盘设置为第一引导,进入 PE 系统,运行 GPU-Z,即可看到显卡核心。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200377738-ce96c52f-d78e-43b8-8e02-922d58260553.png)\\r\\n第一个红框处,即是显卡核心名称。\\r\\n\\r\\n#### 使用现有的 Windows 主机\\r\\n\\r\\n1. 把显卡插到现有的 Windows 主机上;\\r\\n2. 下载安装 GPU-Z,即可看到显卡核心。\\r\\n\\r\\n### 买到了 Lexa 核心的 Rx550,该怎么办?\\r\\n\\r\\n参考这个教程,仿冒 ID 即可:\\r\\n\\r\\n[【黑苹果】通过仿冒ID驱动Lexa核心的Radeon RX 550](https://www.bilibili.com/read/cv15800495)\\r\\n\\r\\n### 12 代处理器如何开启小核心支持?\\r\\n\\r\\n如果你是在别人的 EFI 文件基础上修改,并且别人的 EFI 文件基于 12 代 i3 或者 i5 处理器,而你的处理器是 12 代 i7 及以上,则需要开启小核心支持,查阅:\\r\\n\\r\\n[黑苹果开启十二代酷睿能效核心的驱动_豆豆本豆儿的博客-CSDN博客](https://blog.csdn.net/Z17362251225/article/details/125412246)\\r\\n\\r\\n## 装机单\\r\\n> 价格可能略有浮动。\\r\\n\\r\\n> M1 Max 和 M1 Pro 性能几乎一致,不作区分。\\r\\n\\r\\n### M1 Mac Mini 同等性能(3155 元,32GB + 512GB SSD)\\r\\n|部件|产品|价格|备注|\\r\\n|-|-|-|-|\\r\\n|CPU|Intel i3 12100f 散片|668||\\r\\n|主板|铭瑄 h610itx 挑战者|568||\\r\\n|内存条|酷兽夜枭 16GB 3200Mhz 两根共 32GB|470|京东自营即可|\\r\\n|硬盘|光威弈 Pro 512GB nvme ssd|295|按自己喜好选即可|\\r\\n|散热器|利民 Axp90 x47 + LGA17xx 扣具|139|不用太强力的散热器,随便压一压|\\r\\n|机箱|傻瓜超人 K39 + 定制显卡延长线|200|一定要买定制显卡延长线|\\r\\n|电源|益衡 7030B 300W Flex 1U 电源|398|记得买全模组定制版本|\\r\\n|显卡|盈通 Rx550 4GB|299|淘宝二手,记得询问客服是否是 Polaris 核心|\\r\\n|网卡|博通 BCM94360CS2 + m2 ngff 正向转接卡|118|记得买外置天线|\\r\\n\\r\\n### M1 Max Mac Studio 同等性能(3537 元,32GB + 512GB SSD)\\r\\n\\r\\n|部件|产品|价格|备注|\\r\\n|-|-|-|-|\\r\\n|CPU|Intel i5 12400f 散片|1050||\\r\\n|主板|铭瑄 h610itx 挑战者|568||\\r\\n|内存条|酷兽夜枭 16GB 3200Mhz 两根共 32GB|470|京东自营即可|\\r\\n|硬盘|光威弈 Pro 512GB nvme ssd|295|按自己喜好选即可|\\r\\n|散热器|利民 Axp90 x47 + LGA17xx 扣具|139|不用太强力的散热器,随便压一压|\\r\\n|机箱|傻瓜超人 K39 + 定制显卡延长线|200|一定要买定制显卡延长线|\\r\\n|电源|益衡 7030B 300W Flex 1U 电源|398|记得买全模组定制版本|\\r\\n|显卡|盈通 Rx550 4GB|299|淘宝二手,记得询问客服是否是 Polaris 核心|\\r\\n|网卡|博通 BCM94360CS2 + m2 ngff 正向转接卡|118|记得买外置天线|\\r\\n\\r\\n### M1 Ultra Mac Studio 同等性能(5159 元,64GB + 1TB SSD)\\r\\n\\r\\n|部件|产品|价格|备注|\\r\\n|-|-|-|-|\\r\\n|CPU|Intel i7 12700f 散片|2110|淘宝找个人多的店买就行|\\r\\n|主板|铭瑄 h610itx 挑战者|568|官方旗舰店|\\r\\n|内存条|酷兽夜枭 32GB 3200Mhz 两根共 64GB|798|京东自营即可|\\r\\n|硬盘|光威弈 Pro 1TB nvme ssd|499|如果不需要 1TB,可以换成 512GB,酌情购买|\\r\\n|散热器|利民 Axp90 x53 + LGA17xx 扣具|169|买 53mm 版本,记得买 17xx 扣具|\\r\\n|机箱|傻瓜超人 K39 + 定制显卡延长线|200|一定要买定制显卡延长线|\\r\\n|电源|益衡 7030B 300W Flex 1U 电源|398|记得买全模组定制版本|\\r\\n|显卡|盈通 Rx550 4GB|299|淘宝二手,记得询问客服是否是 Polaris 核心|\\r\\n|网卡|博通 BCM94360CS2 + m2 ngff 转接卡|118|记得买 ngff 转接卡,带外置天线版本|\\r\\n","content":"
\\n

写给程序员的小尺寸高性能黑苹果主机装配指南,全文约 10000 字。

\\n
\\n
\\n

本文同步发表至:FlowUs / 知乎 / Github / 博客

\\n
\\n

\\"image\\"\\n左:Mac Studio,右:穷逼版

\\n

\\"image\\"\\n与 24 寸显示器的对比

\\n

\\"image\\"\\n与 330ml 杯子的对比

\\n

\\"image\\"\\n日常使用负载

\\n

长话短说

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
性能对比CPU内存硬盘性能(R23)价格体积
Mac StudioM1 Ultra64GB1TB 固态多核 2370529999197mm * 197mm * 95mm = 3.68L
穷逼版i7 1270064GB1TB 固态多核 215684946196mm * 218mm * 117mm = 4.9L
\\n
    \\n
  • 本文只面向程序员群体,优先保证编译性能和开发舒适度,不面向视频剪辑等创意工作者,不考虑视频剪辑性能;
  • \\n
  • 重点考虑 CPU 单核和多核性能,优先内存和硬盘容量,考虑机箱体积,不太考虑内存频率,不追求硬盘速度,不太考虑显卡性能,不太考虑噪音,完全不考虑外观精致程度;
  • \\n
  • 优点:便宜,量大管饱,装机完成后,可当做白苹果使用,无痛升级后续系统,可支持 MacOS、Windows 和 Linux 三系统共存;
  • \\n
  • 缺点:折腾,组装机箱和安装系统需要有一定动手能力,不适用于日薪大于 1w 的用户,不适用于无计算机硬件常识用户;
  • \\n
  • 硬件到位之后,在网络良好情况下,半天即可完成装机;
  • \\n
  • 多核编译同一个项目,M1 Max 需 6min,本文主机需 4min,多核性能对比与 R23 跑分对比大致保持一致。
  • \\n
\\n

阅读须知

\\n
    \\n
  • 适用群体:\\n
      \\n
    • 写代码的
    • \\n
    • 买不起 Mac Studio 的
    • \\n
    • 有一定的折腾能力的
    • \\n
    \\n
  • \\n
  • 不适用群体:\\n
      \\n
    • 打游戏的
    • \\n
    • 富哥富姐请直接使用钞能力
    • \\n
    • 剪视频的
    • \\n
    • 懒得折腾的(可以购置硬件后,购买淘宝黑苹果装机服务)
    • \\n
    \\n
  • \\n
  • 本文包含以下内容:\\n
      \\n
    • 提供了购买硬件和装机过程中的一些需要考虑的事项
    • \\n
    • 提供了分别对标 M1 Mac mini、M1 max Mac studio 和 M1 ultra Mac studio 性能(不含 GPU 和磁盘性能)且同等体积的装机购置清单
    • \\n
    • 提供了安装黑苹果所需的硬件考虑事项以及系统安装须知
    • \\n
    \\n
  • \\n
\\n

为什么需要装这样一台机器

\\n
\\n

可以跳过此章节。

\\n
\\n

总而言之,对于我来说,主要有两个原因:

\\n
    \\n
  • \\n

    主要原因:公司电脑性能拉跨,满足不了开发需要;

    \\n
  • \\n
  • \\n

    次要原因:目前在用的 MacOS 13 Ventura 系统拉跨,降级麻烦。

    \\n
  • \\n
\\n

所以,我需要搞一台高性能主机来替换掉公司发的老旧 MBP。

\\n

其实公司配的 2020 款 MBP 并不算老,就是性能有点差,10 代标压 i5-1038NG7 处理器加 16GB 板载内存,多核性能大概是 M1 的一半,省着点还是可以用的。

\\n

但我在 MacOS 13 技术预览版刚放出来时候,就迫不及待地手贱升级了一波,然后成功被新系统的各种卡顿 Bug 和内存占用虚高问题治好了低血压。即便到如今已经发布了正式版,这些问题仍旧存在,以至于日常开发时随便开个 VS Code 再加几个网页,内存就基本见底了。

\\n

后来为了能保证正常开发,我不得不花一周时间把 Vim 打造成主力 IDE,以便直接在开发机上写代码。

\\n

此外,除了 MacOS 拉跨,公司的项目本身也对设备性能要求很高。

\\n

身后的同事老哥前段时间斥巨资入手了一款中配 M1 Max 款 Mac Studio,原因就是公司发放的设备很难满足开发需求。我们日常的技术栈是 C++ 和 React,前者在我那台十代标压 i5 上,全量编译整个项目需要花费 20 分钟到 30 分钟的时间,编译个几次项目,半天就没了,倒是非常适合划水。虽然大多数时间并不会全量编译,但每次增量编译耗时也在分钟级别,非常可观,十分难受。

\\n

后者 React 更是重量级,NodeJS + HMR + 浏览器堪称内存黑洞,再配合 C++ 建立的 Clang 符号表索引,16GB 板载内存直接被榨干,一天下来大多数时间都在等编译和页面响应。

\\n

当然这也并不是完全没有好处,由于等待的成本过于高昂,我们写下每行代码前都要深思熟虑,每次编译前都要检查个半天,所以那段时间的代码质量总感觉也高了不少 😆。

\\n

后来我们安排了一台 36 核 72 线程(含两颗 18C36T Intel Xeon Gold 6240 CPU)的开发机,第一次用它编译项目的时候,有种便秘两周之后突然窜稀的畅快感觉,以前 20 多分钟的编译,现在两分钟就完事了,比以前跑单测都快。

\\n

然而即便用了开发机和 Vim 之后,我还是经常被系统卡顿和内存不足烦得要死,随便开几个网页,该卡还是卡,就这样将就了两个多月,在某次系统彻底无响应之后,我一咬牙一狠心,决定加亿点钱解决这个问题。

\\n

于是我便问了下身后老哥的 M1 Max 的编译时间,居然只要 6 分钟,已经完全够用了,怪不得他不怎么用开发机,我马上点开了苹果官网:

\\n

\\"image\\"

\\n

然后看到这个价格我火速关闭,那没事了,告辞。

\\n

看来富哥的路是走不通了,只能使用穷逼的道路曲线救国了。

\\n

性能

\\n

苹果在发布会上把 M1 系列芯片吹得天花乱坠,拳打 Intel i9-11900,脚踢 RTX 3090,让我们来看看它的真面目:

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
芯片名称规格R23 单核跑分R23 多核跑分功耗
Intel Core i5 1038NG74 核 8 线程1116497928W
Apple M18 核151076876.8W ~ 39W
Intel Core i5 124006 核 12 线程16971190565W
Apple M1 Max10 核15331233711W ~ 115W
Intel Core i7 1270012 核 20 线程187921568PL1: 65W, PL2 180W
Apple M1 Ultra20 核15062370513W ~ 215W
Intel Core i9 1290016 核 24 线程198826455PL1: 65W, PL2 200W
\\n

确实很强,比 10 代 intel 强多了,但是面对 12 代 intel 还是不太行,毕竟功耗在这摆着,i9 12900 勉强守住了桌面 CPU 的底裤。

\\n

苹果 M1 系列的能耗比确实惊人,但是我要放在公司使用,完全不用担心电费问题,功耗都是图一乐,于是经过一周的调研,确定了硬件配置单。

\\n

硬件配置单

\\n
\\n

下列配置中的硬件,除显卡和网卡外,均为全新价格。

\\n
\\n
\\n

如需查看更多配置单(M1 / M1 Max 级别),请跳转到文章末尾。

\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
部件产品价格备注
CPUIntel i7 12700f 散片2110f 后缀代表不集成核显,比带核显版本便宜 100 块左右
主板铭瑄 H610itx 挑战者569最便宜的能带得动 i7 12700 的 itx 主板
内存条酷兽 32GB 3000Mhz 两根共 64GB798最便宜的 DDR4 内存,有 2666mhz / 3000mhz / 3200Mhz 三个版本,价格一样,哪个便宜有货买哪个
硬盘光威弈 Pro 512GB nvme ssd295随便选的,可以加一百多块上 1TB 固态,反正便宜
散热器利民 Axp90 x53 + LGA17xx 扣具159选购时需要考虑机箱适配的散热器高度,需要加 18 元额外购买利民 LGA17xx 扣具
机箱傻瓜超人 K39 4.9L + 显卡延长线199196mm * 117mm * 218mm = 4.9L 体积,当前配置的体积最小最便宜的选择
电源益衡 7030B 300W Flex 1U 电源398这个价格是加 100 块让店家换全模组电源线和静音风扇之后的版本,组装的时候理线会更方便
显卡AMD RX550 2GB/4GB Polaris 核心300二手亮机卡,有 Lexa 和 Polaris 核心两个版本,建议买 4GB 显存 Polaris 核心版本,各版本价格无区别
网卡博通 BCM94360CS2 + m2 ngff 正向转接卡118也可以选择 Intel AX201 或者更低端的 Intel 9560,能省个几十块钱,但需要额外配置驱动,没必要
\\n

我们一项一项来看,为什么要选用这些配置。

\\n

CPU 和主板

\\n

首先,要再次明确一下目标,我们想要 M1 Ultra 级别的性能,那就只能在 i7 12700 及以上的 CPU 中选,大概有这么几个选项:

\\n
    \\n
  • \\n

    Intel i7 12700f 和 12700

    \\n
  • \\n
  • \\n

    Intel i7 12700kf 和 12700k

    \\n
  • \\n
  • \\n

    Intel i9 12900f

    \\n
  • \\n
  • \\n

    Intel i9 12900kf 和 12900k

    \\n
  • \\n
\\n

**为什么不选 AMD 的 CPU?**因为性价比不高,12 代 CPU 同价位性能比 AMD 5000 系列同价位性能稍强,价格稍便宜,而且目前 AMD 装黑苹果需要额外配置,有点麻烦,所以完全没有理由选 AMD。

\\n

**为什么不选 13 代 Intel CPU?**因为性价比不高,目前只发布了带 K 后缀的高性能版本,需要搭配更好的主板和电源,而且 13 代 CPU 也相对更贵。

\\n

然后,i7 和 i9 分别都有 f / k / kf 几个版本:

\\n
    \\n
  • \\n

    其中凡是带 f 的,都是不带集成显卡的版本,考虑到我们是装黑苹果,12 代集成显卡不能被驱动,所以必须得配个独立显卡,选择 f 版本就行,可以省下 100 块;

    \\n
  • \\n
  • \\n

    其中凡是带 k 的,都代表着性能增强版,大概比不带 k 的版本性能强 10% - 15%,价格也要贵 5% - 10%,酌情购买。

    \\n
  • \\n
\\n

由于 i9 处理器需要主板有更强的供电能力,主板的价格就水涨船高,所以我们退而求其次,选择 i7 12700,性能只比 M1 Ultra 弱 10%。当然,也可以加点钱上 12700k,这样性能和 M1 Ultra 持平,不过这点差距在实际使用时基本没什么区别,12700k 对主板供电能力也有一定的要求,丐板估计带不动。

\\n

主板就不提了,铭瑄 H610itx 在 600 块价位基本无敌,找不到其他能一起打的。唯一缺点就是 itx 主板接口有点少,只能插两根内存条,目前消费级内存条单条最大也就 32GB,所以最多只能上 64GB 内存。

\\n

而且这种低端丐板没有额外的 M.2 接口,只能插一条 Nvme 固态,要扩容的话,要么换固态,要么加 SATA 硬盘。不过还好 512GB SSD 够用了,即便是 1TB 的 SSD 也只要 400 出头,可以自己决定要不要加钱。

\\n

购买建议:

\\n
    \\n
  • 购买散片板 U 套装即可
  • \\n
\\n

内存条和硬盘

\\n

酷兽和光威是同一个厂,不同的产品线,酷兽主打性价比,399 块的 32GB 内存条要啥自行车,直接来两条插满。

\\n

此外这个内存条有 2666mhz / 3000mhz / 3200mhz 三个频率版本,理论上铭瑄 h610itx 主板是支持 3200mhz 高频内存的,只需要在 BIOS 里设置一下就行,但是说实话内存频率感知不强,只有核显打游戏才对内存频率有要求,如果用来开发,随便买就行,高频内存属实浪费预算,哪个有货买哪个。

\\n

最近固态硬盘也是白菜价,1tb 固态已经降到了 400 以下,当然也有更贵的性能更好的版本,但是对于开发来说感知不强,3000mb/s 和 7000mb/s 虽然看起来差距挺大,但是实际用的时候也就几百毫秒的差距,感知不出来,所以随便买个便宜的就行,重点还是看容量。

\\n

购买建议:

\\n
    \\n
  • 哪个便宜买哪个,频率和读写速度并不重要;
  • \\n
  • Mac Studio 的统一内存,官方读速是 800GB/s,与 3200mhz DDR4 内存条读速差不多;
  • \\n
  • 如果磁盘性能也想对标一下 Mac Studio,Mac Studio 硬盘速度为:读 5000MB/s,写 4000MB/s,大致与 PCIE 4.0 固态速度接近,所以可以酌情购买 PCIE 4.0 固态硬盘(铭瑄的这块主板支持,精粤的不支持),同体积大概是 PCIE 3.0 硬盘的 1.5 倍价格,512GB 版本大概 400 块可以搞定。
  • \\n
\\n

散热器、机箱和电源

\\n

这三兄弟要一起看,其实显卡也应该拿过来一起看,但是开发场景,显卡不是很重要,所以显卡另说吧。

\\n

之所以要放一起看,主要是尺寸问题,我想要体积尽可能接近 Mac Studio,Mac Studio 的体积是 197mm * 197mm * 9.5mm = 3.68L,一开始我买的是下面这个机箱:

\\n

\\"image\\"

\\n

整体尺寸是 187mm * 230mm * 97mm = 4.2L,非常小巧。

\\n

我选择了其中的 A 背板版本,没有显卡位,把空间全部让给散热器和电源,散热器也是买的 Axp90 x53(90mm 宽,53mm 高)。

\\n

因为我一开始并没有打算装黑苹果,想直接用 Linux 来开发,但是后来发现 Linux 没办法入公司内网,而且企业微信也是残废状态,所以才考虑装黑苹果,于是换成了下面的机箱:

\\n

\\"image\\"

\\n

体积大了一圈,来到了 196mm * 117mm * 218mm = 4.9L,不过好在便宜,加显卡延长线才 200 块,比上一个 300 块的机箱便宜很多,而且可以加装一个 19cm 以内的双槽短卡,非常适合本文的需求。

\\n

从上面机箱的商品介绍就可以知道,这个机箱支持 56mm 以内的散热器,一般来讲,散热器的尺寸与散热能力正相关,基本上体积越大,散热能力越好,在看了一圈视频之后,出现次数的就是利民的axp90 系列的几个散热器,价格在 100 元到 150 元之间:

\\n

\\"image\\"

\\n

可以看到主要有 36mm / 47mm / 53mm 三种高度,名字 AXP90 则代表 90mm * 90mm 的尺寸,为了最大化机箱利用率,我选用了 53mm 高度版本,实测整机满载温度可以控制在 80 摄氏度以内,日常使用则只有 27 摄氏度左右,好悬没给 CPU 给冻感冒。

\\n

此外,如果你想选用一些更小尺寸的机箱,在选散热器的时候一定要注意,最好预留 3mm 以上的空间余量,比如如果限高 56mm 的机箱,如果配了一个 55mm 高度的散热器,风扇叶片和机箱离得太近,会出现十分明显的啸叫。

\\n

然后是电源,前面了解到 i7 12700 的满载功耗是 180w,再加上 50w 亮机显卡的功耗,总计 230w 的实际载荷,按照 80% 的电源转化率,我们只需要选择 290w 以上的电源即可满足需求。在尺寸方面,由于我们想让机箱的体积尽可能的小,而小尺寸 itx 机箱往往搭配使用小 1u 尺寸的 Flex 电源,这类电源的价格要比常规尺寸的电源稍贵一点,如果想要省钱,可以选购支持 SFX 电源的更大尺寸的机箱。

\\n

最终我选择了益衡的 7030B 300w 电源:

\\n

\\"image\\"

\\n

\\"image\\"

\\n

小 1u 电源还有个缺点是会比较吵,因为受体积限制,风扇必须提高转速才能保证散热,运行时会有比较明显的高频噪声,所以我选择加了 100 块,买了店家改装之后的版本(上图右侧)。

\\n

改装版本换了更静音的风扇,并且配备了全模组电源线,大概长下面这个样子:

\\n

\\"image\\"

\\n

电源线统一成了黑色的软线,比原装的五颜六色的硬线好看多了,而且装机的时候理线会非常简单,这一点在小机箱里尤其关键,所以这 100 块还是挺值的。

\\n

购买建议:

\\n
    \\n
  • 购买全模组小 1u 电源,考虑购买全模组 + 静音风扇的改装版本
  • \\n
  • 购买散热器时注意机箱散热器限高
  • \\n
  • 利民散热器默认附带的主板支架只能与上一代 Intel CPU 搭配使用,需要额外花费 18 元购买利民 LGA17xx 扣具
  • \\n
\\n

显卡

\\n

显卡需要着重讲一下,因为 MacOS 非常挑显卡,目前市售的 RTX 系列显卡基本都是驱动不了的,只要一些比较老的 Nvidia 显卡才能驱动。而大多数 AMD 显卡都能驱动,比如最近出的 6600 和 6700,但是 6400 除外,详细的 GPU 列表可以查看这里

\\n

由于我们并不需要特别强劲的显卡性能,所以只需要选择能够正常驱动的低功耗亮机卡即可,我一开始就直接从比较老的 R7 和 R9 系列显卡开始看:

\\n

\\"image\\"

\\n

可以看到有非常多的选项,我在淘宝搜了一圈之后,发现最低端的 R7 240/250 显卡只要 100 块,于是就直接下单买了一块,结果入手后折腾了许久才发现,这张 2013 年发售的老显卡,最高只能运行在 MacOS 10.x 系统上,最新的 12.x 系统是没法完美驱动的。我花了大量的时间搜索国内外关于这张 Oland 核心的显卡驱动帖子,没有发现任何人能成功在 11.x 及以上的系统上成功运行过它,虽然可以使用仿冒显卡 ID 的方法成功安装并进入系统,但是却无法使用 Metal GPU 加速,导致系统所有的高斯模糊效果以及动效都是缺失的,完全无法满足日常使用。

\\n

于是我就又斥巨资花了 300 块买了一张 Rx550,这张显卡发售于 2017 年,网上有相当多的黑苹果视频表示这张卡可以完美工作在最新的 MacOS 13.0 系统上,而且完全不需要任何额外操作,插上就能用。当然,前提是买到了正确的版本的显卡,这张显卡有两种核心版本:Lexa 核心和 Buffin 核心(或称 Polaris 核心),其中 Buffin(Polaris) 核心是可以即插即用,但是 Lexa 核心则需要仿冒参数才能工作。

\\n

很不幸我买的这张 Rx550 2GB 联想拆机卡是 Lexa 核心,不过由于有了之前折腾 R7 240 的经历,对我来说还算简单。

\\n

此外这张显卡有 2GB 显存和 4GB 显存两个版本,价格基本一样,我个人建议购买 4GB 版本,因为我在实际使用时发现,MacOS 12.x 系统很容易就把 2GB 显存吃满了,所以能买大的还是买大点的。

\\n

\\"image\\"

\\n

盈通的这张 RX550 4GB 就很适合,虽然是二手,有翻车的风险,但是我们并不会用来打游戏,所以只要它能点亮就行,反正日常负载不会太高,暴毙的可能性比较小。

\\n

如果实在担心翻车,可以加点钱买全新版本,这张卡有全新正品在售,价格在 500 块到 600 块之间:

\\n

\\"image\\"

\\n

购买建议:

\\n
    \\n
  • 购买 RX550 4GB Polaris 核心版本
  • \\n
  • 省钱就买二手,图稳就加点钱买全新
  • \\n
  • 购买显卡时注意显卡尺寸是否兼容机箱
  • \\n
\\n

无线网卡

\\n

无线网卡也值得花点篇幅来说一说,目前黑苹果无线网卡可选 Intel NGFF 接口的一系列网卡,以及搭配转接板使用的博通 BCM 系列苹果拆机显卡:

\\n

\\"image\\"

\\n

如果从省事的角度来看,直接买博通的拆机网卡即可,插上就能用。如果想省点钱,就买 Intel 最低端的 3168 系列:

\\n

\\"image\\"

\\n

其实在几年前,Intel 的无线网卡是没办法在苹果系统上使用的,后来有个大佬搓了一个驱动出来,然后 Intel 几乎所有型号的无线网卡一夜之间焕发第二春,具体的支持列表可以查看下面的链接:Compatibility | OpenIntelWireless

\\n

此外无线网卡的天线,有内置和外置两种区别:

\\n\\"image\\"\\n\\"image\\"\\n

两者的区别:

\\n
    \\n
  • 外置天线是机箱后面的小尾巴,看起来可能不太美观,但是信号比较稳定,增益较强;
  • \\n
  • 内置天线则需要粘在机箱上,容易受机箱内部杂波干扰,增益较小,信号不太稳定,但由于不需要伸出机箱,所以会比较美观一点。
  • \\n
\\n

我选择了 Intel 9560 内置天线版本,花费 48 块,实装之后可以完美使用 WIFI 和蓝牙,隔空投送也可以正常使用,但是内置天线信号不稳定,蓝牙连接妙控板非常卡顿,基本无法日常使用。而且由于网卡规格较低,虽然标称 2.4G 300Mbps / 5G 1733Mbps,但实测只能跑到 50Mbps,可能最大的原因还是这个内置天线的信号太差,干扰太多。

\\n

不过问题不大,这个主机的 WIFI 纯粹是为了应付公司的入网认证,认证完了之后就直接关掉用有线连接了,如果你对无线网络有需求,建议购买高规格的外置天线版本。

\\n

购买建议:

\\n
    \\n
  • 购买博通 BCM 94360CS + 正向转接卡 + 外置天线版本,不用折腾,信号更好。
  • \\n
\\n

装机

\\n

十分推荐先观看此视频:[DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!](【DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!】 https://www.bilibili.com/video/BV1v54y1Z7Er/?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9),视频中的配置与本文配置基本一致,可以参考。

\\n\\"image\\"\\n

先上所有硬件的全家福,图中不包含显卡和网卡,因为拍摄照片的时候显卡和网卡还没到。

\\n

安装 CPU

\\n\\"image\\"\\n

主板说明书会提示如何安装 CPU,一定要小心不要弄坏了针脚,主板上的金属压杆会比较紧,确认 CPU 按照防呆口安装妥当后,稍加用力按下压杆,将 CPU 固定好即可。

\\n

上电测试

\\n

先别急着往机箱里塞,把内存条和硬盘插好之后,连接电源线和键鼠,用螺丝刀碰触主板上的开机引脚,显示器能够显示 BIOS 界面,则表示成功点亮。

\\n

安装散热器

\\n

按照散热器说明书进行安装即可,注意需要额外购买利民 LGA17xx 扣具。

\\n

安装机箱

\\n

按照机箱说明书安装即可,建议先安装电源,再安装主板,然后接电源线,安装显卡延长线,安装显卡。

\\n\\"image\\"\\n

成品展示

\\n\\"image\\"\\n\\"image\\"\\n

最终的成品大概比 Mac Studio 大了一圈,当然精致程度是没法比的,如果想要精致,可以加钱买更贵的全铝合金一体成型机箱(5.6L 体积):

\\n

\\"image\\"

\\n

系统

\\n

可以直接跳转到流程清单部分,如果你不需要安装 MacOS,可以无视下列内容。

\\n

为什么不用 Windows 和 Linux?

\\n

在最开始决定装机的时候,我就在考虑要使用什么操作系统作为主力开发环境,前文已经提到我需要频繁编译,所以编译性能是我首要考虑的问题,这样 Window 肯定就不能用了,Windows 下 C++ 编译性能一向令人诟病,即便有了 WSL,由于它运行在 Hyper-V 虚拟机中,导致 IO 性能跟不上,实际的性能大概是 Linux 原生的 80% 左右,直接损耗了 20% 的性能,要知道 Intel 的 CPU 一次代际升级的性能也才差不多 25%,Window 直接就让 CPU 性能倒退一代,这绝对是无法接受的。

\\n

我随后的想法是使用 Linux 作为日常开发系统,并使用 KVM 安装 Windows 虚拟机来解决常用软件问题,我也确实尝试了这种做法,并依次使用了下列 Linux 发行版:

\\n
    \\n
  • 使用 KDE 桌面的 Debian,由于 Stable 版本的 Linux 内核太老,导致无法在 12 代 Intel 平台上正常安装,尝试几次后均未果,作罢;
  • \\n
  • 使用 KDE 桌面的 Ubuntu,也就是 KUbuntu,这个发行版确实很不错,KDE 对高分屏的支持非常不错,而且窗口主题和动效都很细腻,我用了大概一周时间,在上面使用 KVM 分别安装了 Windows 10 LTSC 以及 Mac OS 12 的虚拟机,前者用于运行企业微信,后者则是装来玩的,由于没有独显直通,Mac OS 虚拟机的运行流畅度十分感人。在 Linux 上运行 Windows 软件的另一个方法是使用 Wine,但无奈兼容性欠佳,无法正常运行最新版本的企业微信,微信倒是可以正常使用;
  • \\n
  • 使用 Gnome 的 Ubuntu,Gnome 对高分屏的支持太差劲,大多数应用没有对 Wayland 做兼容,甚至浏览器在分数倍缩放下都很糊,而且同样无法通过 Wine 运行企业微信,作罢;
  • \\n
  • 最后是国产的 Deepin,应该是最贴近日常使用的 Linux 发行版了,应用商店里有 Deepin 团队维护的 Wine 版微信和企业微信,但是企业微信还有点问题,虽然可以正常聊天,但是邮箱界面始终白屏,另外 Tim 也是卧龙凤雏,可以安装但无法启动。
  • \\n
\\n

我大概用了一周多的 Deepin,并成功在上面编译运行了公司的项目,编译时间大概是开发机的 2 倍速度,可以看到 i7 12700 的多核性能相当强悍。

\\n

但是网络问题始终没有办法很好的解决,公司的内网认证软件是阿里出品的阿里郎的阉割版本,其入网认证条件非常严格,无法在 Windows 虚拟机中完成认证,更不用提 Wine 了,也没有提供 Linux 版本,导致我的主机就算插上网线也无法访问内网资源,而且这个软件入网必须要有无线网卡,大多数 USB 网卡都无法在 Linux 下免驱运行。

\\n

为了解决这个问题,我琢磨了很久计算机网络相关的知识,大概有这么几种方法:

\\n
    \\n
  • 最容易想到的方法自然是用公司的苹果笔记本入网,然后当跳板机开代理给主机用,确实可以用,但是基于代理的方法,无法保证主机的所有流量都走代理,某些应用可能会绕开系统的代理策略,自行建立连接,这种方法不太稳定;
  • \\n
  • 然后比较容易想到的是使用 Mac 的网络共享,无奈在入网认证使用 802.1x 的安全策略,系统会直接禁用掉网络共享,这条路也走不通;
  • \\n
  • 最后想到的方法时把 Mac 模拟成一台路由器,直接网线直连主机,然后手动分配 IP 地址即可,经过一番搜索,其实只要开启 Mac 的 IP 路由转发即可,然后将来自 USB 网卡的所有流量都转发到无线网卡上,即可大功告成。
  • \\n
\\n

关于折腾 Linux 日用开发环境的记录,可以看这篇文章:

\\n

Linux 开发环境备忘录

\\n

最后由于 Linux 上无法找到顺手的快速切换窗口的软件(类似 Mac Spotlight 的功能),还是决定安装黑苹果,可以直接无缝复用 MBP 的那一套工作流。

\\n

主要工具:OpenCore

\\n

安装黑苹果是一件非常需要耐心的事情,目前主流的安装黑苹果的工具是 OpenCore,其官方教程非常详细,流程也非常长,新手看了也非常头大:

\\n

OpenCore Install Guide

\\n

在翻阅许久之后,我决定放弃跟着官方教程走,官方手册的内容只适合当作速查表来使用,如果跟着从零开始做,不知道搞到猴年马月。

\\n

先列举一下我认为比较有用的内容,首先是显卡购买指南:

\\n

Introduction | GPU Buyers Guide

\\n

里面列举了各种可以用于黑苹果的显卡型号,以及它们支持的系统版本,当然这个指南中的某些老显卡的信息是错的,比如我前面提到的 R7 240,手册上说可以在最新的系统中通过仿冒 ID 的方式运行,但其实只能在 OS 10.x 以前的系统上运行,不过大多数信息都是准确的,可以放心参考。

\\n

其次是 GPU 仿冒 ID 指南,如果你想折腾一下不直接免驱的显卡,可以参考它来仿冒 ID:

\\n

Renaming GPUs (SSDT-GPU-SPOOF) | Getting Started With ACPI

\\n

我个人不推荐用这种方式来搞,太费时间,而且不一定有用,还是建议直接购买免驱显卡。

\\n

流程清单

\\n

如果自己搞不定,可以直接在淘宝购买黑苹果装机服务,远程手把手指导,价格在 100 元到几百元不等。

\\n

可以看这个视频,了解大概步骤:刷新记录!12代平台12400F仅需24秒即可完成黑苹果系统安装,请问还有谁?

\\n

通用流程:

\\n
    \\n
  • 硬件准备:\\n
      \\n
    • 16GB 及以上容量的内存卡或者移动硬盘,并格式化为 FAT32 格式;
    • \\n
    • 一台 Windows 电脑(Linux 或 Mac 也行);
    • \\n
    • 组装好的等待安装系统的主机;
    • \\n
    • 良好的有线网络环境;
    • \\n
    • 耐心。
    • \\n
    \\n
  • \\n
  • 准备 EFI 引导文件:各版本 EFI 引导文件\\n
      \\n
    • 如果你的硬件配置是本文推荐的配置,那么直接下载上面列表中的安装包,解压即可;
    • \\n
    • 或者你可以在 Github 上搜索自己的主板名称,比如我就参考了这个仓库(h610itx + i5 12400 + rx560 + bcm943602cs 网卡)和这个仓库(h610itx + i5 12490f + rx560 + intel ax201 网卡),在他们的基础上进行了少量修改,就适配了本文的硬件;
    • \\n
    • 经过上述步骤,你会获得一个 EFI 文件夹,将其拷贝到 U 盘或者移动硬盘中即可,如果你的硬件与上述现有的配置都不一样,请查阅常见问题章节。
    • \\n
    \\n
  • \\n
  • 准备基础安装镜像:\\n
      \\n
    • OpenCore 提供了一个很细致的教程来下载安装镜像,基础镜像大概 600MB 左右,按照这个教程(在 Windows 上下载安装镜像)下载镜像文件并复制到 U 盘即可。
    • \\n
    • 将 U 盘插入主机,按下 DEL 键进入主板的 BIOS 界面,如果你使用了其他主板,请自行搜索如何进入 BIOS,然后将 U 盘设置为第一启动项即可;
    • \\n
    \\n
  • \\n
  • 启动启动后进入安装界面:\\n
      \\n
    • 选择磁盘工具,将 SSD 所在盘符格式化为 APFS 格式,并命名为 MacOS(或者其他任何英文名);
    • \\n
    • 退出磁盘工具,点击安装 MacOS,进入常规安装流程;
    • \\n
    \\n
  • \\n
  • 在网络良好情况下,自动重启若干次后,大概 40 分钟即可安装成功;
  • \\n
  • 安装成功后,自动重启进入系统选择界面,选择 MacOS 盘符进入即可;
  • \\n
  • 将 EFI 文件夹复制到 SSD 硬盘上,可参考这个教程:完善引导
  • \\n
  • 到现在,你已经可以正常使用 MacOS,但是还无法使用 Apple ID 和 iCloud,你还需要生成自己的硬件序列号,请参考这个教程:为自己的黑苹果生成随机三码
  • \\n
  • 至此,大功告成。
  • \\n
\\n

常见问题

\\n

如何查看我买到的显卡的核心?

\\n

搜索

\\n
    \\n
  1. 优先询问卖家,其次在搜索引擎中搜索显卡型号 + 核心即可。
  2. \\n
\\n

使用 PE 工具

\\n
    \\n
  1. 下载安装微 PE 工具箱,并安装到 U 盘上;
  2. \\n
  3. 下载 GPU-Z:https://www.techpowerup.com/gpuz/,复制到已经安装了 PE 工具的 U 盘中;
  4. \\n
  5. 进入 BIOS,将 U 盘设置为第一引导,进入 PE 系统,运行 GPU-Z,即可看到显卡核心。
  6. \\n
\\n

\\"image\\"\\n第一个红框处,即是显卡核心名称。

\\n

使用现有的 Windows 主机

\\n
    \\n
  1. 把显卡插到现有的 Windows 主机上;
  2. \\n
  3. 下载安装 GPU-Z,即可看到显卡核心。
  4. \\n
\\n

买到了 Lexa 核心的 Rx550,该怎么办?

\\n

参考这个教程,仿冒 ID 即可:

\\n

【黑苹果】通过仿冒ID驱动Lexa核心的Radeon RX 550

\\n

12 代处理器如何开启小核心支持?

\\n

如果你是在别人的 EFI 文件基础上修改,并且别人的 EFI 文件基于 12 代 i3 或者 i5 处理器,而你的处理器是 12 代 i7 及以上,则需要开启小核心支持,查阅:

\\n

黑苹果开启十二代酷睿能效核心的驱动_豆豆本豆儿的博客-CSDN博客

\\n

装机单

\\n
\\n

价格可能略有浮动。

\\n
\\n
\\n

M1 Max 和 M1 Pro 性能几乎一致,不作区分。

\\n
\\n

M1 Mac Mini 同等性能(3155 元,32GB + 512GB SSD)

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
部件产品价格备注
CPUIntel i3 12100f 散片668
主板铭瑄 h610itx 挑战者568
内存条酷兽夜枭 16GB 3200Mhz 两根共 32GB470京东自营即可
硬盘光威弈 Pro 512GB nvme ssd295按自己喜好选即可
散热器利民 Axp90 x47 + LGA17xx 扣具139不用太强力的散热器,随便压一压
机箱傻瓜超人 K39 + 定制显卡延长线200一定要买定制显卡延长线
电源益衡 7030B 300W Flex 1U 电源398记得买全模组定制版本
显卡盈通 Rx550 4GB299淘宝二手,记得询问客服是否是 Polaris 核心
网卡博通 BCM94360CS2 + m2 ngff 正向转接卡118记得买外置天线
\\n

M1 Max Mac Studio 同等性能(3537 元,32GB + 512GB SSD)

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
部件产品价格备注
CPUIntel i5 12400f 散片1050
主板铭瑄 h610itx 挑战者568
内存条酷兽夜枭 16GB 3200Mhz 两根共 32GB470京东自营即可
硬盘光威弈 Pro 512GB nvme ssd295按自己喜好选即可
散热器利民 Axp90 x47 + LGA17xx 扣具139不用太强力的散热器,随便压一压
机箱傻瓜超人 K39 + 定制显卡延长线200一定要买定制显卡延长线
电源益衡 7030B 300W Flex 1U 电源398记得买全模组定制版本
显卡盈通 Rx550 4GB299淘宝二手,记得询问客服是否是 Polaris 核心
网卡博通 BCM94360CS2 + m2 ngff 正向转接卡118记得买外置天线
\\n

M1 Ultra Mac Studio 同等性能(5159 元,64GB + 1TB SSD)

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
部件产品价格备注
CPUIntel i7 12700f 散片2110淘宝找个人多的店买就行
主板铭瑄 h610itx 挑战者568官方旗舰店
内存条酷兽夜枭 32GB 3200Mhz 两根共 64GB798京东自营即可
硬盘光威弈 Pro 1TB nvme ssd499如果不需要 1TB,可以换成 512GB,酌情购买
散热器利民 Axp90 x53 + LGA17xx 扣具169买 53mm 版本,记得买 17xx 扣具
机箱傻瓜超人 K39 + 定制显卡延长线200一定要买定制显卡延长线
电源益衡 7030B 300W Flex 1U 电源398记得买全模组定制版本
显卡盈通 Rx550 4GB299淘宝二手,记得询问客服是否是 Polaris 核心
网卡博通 BCM94360CS2 + m2 ngff 转接卡118记得买 ngff 转接卡,带外置天线版本
\\n"}}')},285:function(n,t,e){"use strict";e(266)},286:function(n,t,e){var l=e(105)(!1);l.push([n.i,'.clearfix:after,.vssue .vssue-new-comment .vssue-new-comment-footer:after{display:block;clear:both;content:""}.vssue{width:100%;color:#2c3e50;font-size:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";padding:10px}.vssue .vssue-button{outline:none;cursor:pointer;padding:10px 20px;font-size:1.05;font-weight:700;color:#900;background-color:transparent;border:2px solid #900;border-radius:10px}.vssue .vssue-button:disabled{cursor:not-allowed;color:#eaecef;border-color:#eaecef}.vssue .vssue-button:disabled .vssue-icon{fill:#eaecef}.vssue .vssue-button:not(:disabled).vssue-button-default{color:#a3aab1;border-color:#a3aab1}.vssue .vssue-button:not(:disabled).vssue-button-primary{color:#900;border-color:#900}.vssue .vssue-icon{width:1em;height:1em;vertical-align:-.15em;fill:#900;overflow:hidden}.vssue .vssue-icon-loading{animation:vssue-keyframe-rotation 1s linear infinite}@keyframes vssue-keyframe-rotation{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.vssue .fade-appear-active,.vssue .fade-enter-active{transition:all .3s ease}.vssue .fade-leave-active{transition:all .3s cubic-bezier(1,.5,.8,1)}.vssue .fade-appear,.vssue .fade-enter,.vssue .fade-leave-to{opacity:0}.vssue .vssue-notice{position:relative;z-index:100;transform:translateY(-11px)}.vssue .vssue-notice .vssue-alert{position:absolute;z-index:101;cursor:pointer;top:0;padding:10px 20px;width:100%;color:#900;border:2px solid #ff9494;border-radius:5px;background-color:#ffeded}.vssue .vssue-notice .vssue-progress{position:absolute;top:0;left:0;height:2px;background-color:#900}.vssue .vssue-status{text-align:center;padding-top:20px;padding-bottom:10px;color:#900}.vssue .vssue-status .vssue-icon{font-size:1.4em}.vssue .vssue-status .vssue-status-info{margin-top:10px;margin-bottom:10px}.vssue .vssue-header{padding-bottom:10px;border-bottom:1px solid #eaecef;margin-bottom:10px;overflow:hidden}.vssue .vssue-header .vssue-header-powered-by{float:right}.vssue .vssue-new-comment{border-bottom:1px solid #eaecef;margin-top:10px;margin-bottom:10px}.vssue .vssue-new-comment .vssue-comment-avatar{float:left;width:50px;height:50px}.vssue .vssue-new-comment .vssue-comment-avatar img{width:50px;height:50px}.vssue .vssue-new-comment .vssue-comment-avatar .vssue-icon{cursor:pointer;padding:5px;font-size:50px;fill:#757f8a}.vssue .vssue-new-comment .vssue-new-comment-body{position:relative}@media screen and (max-width:576px){.vssue .vssue-new-comment .vssue-new-comment-body{margin-left:60px}}@media screen and (min-width:577px){.vssue .vssue-new-comment .vssue-new-comment-body{margin-left:70px}}.vssue .vssue-new-comment .vssue-new-comment-body .vssue-new-comment-loading{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.vssue .vssue-new-comment .vssue-new-comment-footer{margin-top:10px;margin-bottom:10px}.vssue .vssue-new-comment .vssue-new-comment-footer .vssue-current-user{color:#a3aab1}.vssue .vssue-new-comment .vssue-new-comment-footer .vssue-current-user .vssue-logout{cursor:pointer;text-decoration:underline;color:#a3aab1;font-weight:400}@media screen and (max-width:576px){.vssue .vssue-new-comment .vssue-new-comment-footer{text-align:center}.vssue .vssue-new-comment .vssue-new-comment-footer .vssue-new-comment-operations{margin-top:10px}}@media screen and (min-width:577px){.vssue .vssue-new-comment .vssue-new-comment-footer{margin-left:70px;text-align:right}.vssue .vssue-new-comment .vssue-new-comment-footer .vssue-current-user{float:left}}.vssue .vssue-new-comment .vssue-new-comment-input{resize:none;outline:none;width:100%;padding:15px;font-size:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";background-color:#ffeded;border:1px solid #eaecef;border-radius:5px}.vssue .vssue-new-comment .vssue-new-comment-input:disabled{cursor:not-allowed;background-color:#f0f2f4}.vssue .vssue-new-comment .vssue-new-comment-input:focus{background-color:#fff;border-color:#ff4d4d;box-shadow:0 0 1px 1px #ff4d4d}.vssue .vssue-new-comment .vssue-new-comment-input::-moz-placeholder{color:#a3aab1}.vssue .vssue-new-comment .vssue-new-comment-input::placeholder{color:#a3aab1}.vssue .vssue-comments .vssue-comment{margin:15px 0}.vssue .vssue-comments .vssue-comment.vssue-comment-edit-mode .vssue-comment-main{border-color:#ff4d4d;box-shadow:0 0 1px 1px #ff4d4d}.vssue .vssue-comments .vssue-comment.vssue-comment-disabled{pointer-events:none}.vssue .vssue-comments .vssue-comment.vssue-comment-disabled .vssue-comment-body{background-color:#f9f9fa}.vssue .vssue-comments .vssue-comment .vssue-comment-avatar{float:left;width:50px;height:50px}.vssue .vssue-comments .vssue-comment .vssue-comment-avatar img{width:50px;height:50px}@media screen and (max-width:576px){.vssue .vssue-comments .vssue-comment .vssue-comment-body{margin-left:60px}}@media screen and (min-width:577px){.vssue .vssue-comments .vssue-comment .vssue-comment-body{margin-left:70px}}.vssue .vssue-comments .vssue-comment .vssue-comment-header{padding:10px 15px;overflow:hidden;border-top-left-radius:5px;border-top-right-radius:5px;border:1px solid #eaecef;border-bottom:none}.vssue .vssue-comments .vssue-comment .vssue-comment-header .vssue-comment-created-at{float:right;cursor:default;color:#a3aab1}.vssue .vssue-comments .vssue-comment .vssue-comment-main{padding:15px;border:1px solid #eaecef}.vssue .vssue-comments .vssue-comment .vssue-comment-main .vssue-edit-comment-input{resize:none;outline:none;border:none;width:100%;background:transparent}.vssue .vssue-comments .vssue-comment .vssue-comment-footer{padding:10px 15px;overflow:hidden;border-bottom-left-radius:5px;border-bottom-right-radius:5px;border:1px solid #eaecef;border-top:none}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-hint{cursor:default;color:#a3aab1}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-reactions .vssue-comment-reaction{cursor:pointer;display:inline-block;margin-right:8px;color:#900}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-operations{float:right;color:#900}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-operations .vssue-comment-operation{cursor:pointer;margin-left:8px}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-operations .vssue-comment-operation.vssue-comment-operation-muted{color:#a3aab1}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-operations .vssue-comment-operation.vssue-comment-operation-muted .vssue-icon{fill:#a3aab1}.vssue .vssue-pagination{cursor:default;display:flex;padding:5px;color:#a3aab1}@media screen and (max-width:576px){.vssue .vssue-pagination{flex-direction:column;justify-content:center;text-align:center}}.vssue .vssue-pagination .vssue-pagination-loading,.vssue .vssue-pagination .vssue-pagination-page,.vssue .vssue-pagination .vssue-pagination-per-page{flex:1}@media screen and (max-width:576px){.vssue .vssue-pagination .vssue-pagination-page{margin-top:10px}}@media screen and (min-width:577px){.vssue .vssue-pagination .vssue-pagination-page{text-align:right}}.vssue .vssue-pagination .vssue-pagination-select{outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid #ff4d4d;padding-left:.2rem;padding-right:1rem;background-color:transparent;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg%20class=%27icon%27%20viewBox=%270%200%201024%201024%27%20xmlns=%27http://www.w3.org/2000/svg%27%3E%3Cdefs%3E%3Cstyle/%3E%3C/defs%3E%3Cpath%20d=%27M676.395%20432.896a21.333%2021.333%200%200%200-30.166%200L511.061%20568.021%20377.728%20434.645a21.333%2021.333%200%200%200-30.165%2030.166l148.394%20148.48a21.419%2021.419%200%200%200%2030.208%200l150.23-150.187a21.333%2021.333%200%200%200%200-30.208%27/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:100%}.vssue .vssue-pagination .vssue-pagination-select:disabled{cursor:not-allowed}.vssue .vssue-pagination .vssue-pagination-select:focus{background-color:#fff;box-shadow:0 0 .2px .2px #ff4d4d}.vssue .vssue-pagination .vssue-pagination-link{display:inline-block;min-width:1em;text-align:center}.vssue .vssue-pagination .vssue-pagination-link.disabled{pointer-events:none}.vssue .vssue-pagination .vssue-pagination-link:not(.disabled){color:#900;font-weight:500;cursor:pointer}.vssue,.vssue *{box-sizing:border-box}.vssue :not(.vssue-comment-content) a{cursor:pointer;font-weight:500;color:#900;text-decoration:none}.vssue :not(.vssue-comment-content) hr{display:block;height:1px;border:0;border-top:1px solid #eaecef;margin:1.2rem 0;padding:0}body{margin:0;padding:0;font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif}.page{box-sizing:border-box;width:90%;max-width:900px;margin:auto}.loading-light{position:relative}.loading-light:after{content:"";position:absolute;height:20px;width:1000px;background-color:#fff;opacity:.4;left:-500px;animation:fly-to-right 2s ease infinite}@keyframes fly-to-right{0%{transform:translateX(0) rotate(45deg)}to{transform:translateX(1000px) rotate(45deg)}}.post-page{margin-bottom:50px}.title{display:flex;justify-content:center}.post-title{margin-top:120px;color:#333;font-weight:700;font-size:30px;position:relative;text-align:center;max-width:80%}.post-title:before{content:"“";left:-60px}.post-title:after,.post-title:before{position:absolute;font-size:55px;color:#eee}.post-title:after{content:"”";right:-60px}.post-content{word-break:break-word}.post-content .katex{font-size:16px}a{font-weight:500;color:#900;text-decoration:none}p a code{font-weight:400;color:#900}p{font-family:Georgia,"Nimbus Roman No9 L","Songti SC","Noto Serif CJK SC","Source Han Serif SC","Source Han Serif CN",STSong,"AR PL New Sung","AR PL SungtiL GB",NSimSun,SimSun,"TW\\-Sung","WenQuanYi Bitmap Song","AR PL UMing CN","AR PL UMing HK","AR PL UMing TW","AR PL UMing TW MBE",PMingLiU,MingLiU,serif}kbd{border:.15rem solid #ddd;border-bottom:.25rem solid #ddd;border-radius:.15rem;padding:0 .15em}blockquote,kbd{background:#eee}blockquote{font-size:1rem;color:#999;border-left:.2rem solid #dfe2e5;margin:1rem 0;padding:5px 10px}blockquote>p{margin:0}.post-content ol,.post-content ul,blockquote>p{font-family:Baskerville,"Times New Roman","Liberation Serif",STFangsong,FangSong,FangSong_GB2312,"CWTEX\\-F",serif}.post-content ol,.post-content ul{padding-left:1.2em}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25;font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif;margin-top:50px}h1:hover .header-anchor,h2:hover .header-anchor,h3:hover .header-anchor,h4:hover .header-anchor,h5:hover .header-anchor,h6:hover .header-anchor{opacity:1}h1{font-size:2rem;display:none}h2{font-size:1.45rem;padding-bottom:.3rem}h3{font-size:1.35rem}a.header-anchor{font-size:.85em;float:left;margin-left:-.87em;padding-right:.23em;margin-top:.125em;opacity:0;color:#ddd}a.header-anchor:hover{text-decoration:none}.line-number,code,kbd{font-family:Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif}pre{border:1px solid #aaa;border-radius:5px}pre>code{background:transparent;padding:0;margin:0}code,pre{font-size:14px}code{background:#eee;padding:2px 4px;margin:0 2px;border-radius:4px}ol,p,ul{line-height:1.7}li.task-list-item{list-style:none;display:flex}li.task-list-item input[type^=checkbox]{margin:7px 9px 0 -18px}hr{border:0;border-top:1px solid #eee}table{border-collapse:collapse;margin:1rem 0;display:block;overflow-x:auto}tr{border-top:1px solid #dfe2e5}tr:nth-child(2n){background-color:#f6f8fa}td,th{border:1px solid #dfe2e5;padding:.6em 1em}img{max-width:720px;width:100%;margin:auto;display:block;border:1px solid rgba(0,0,0,.1)}@media screen{.info{display:flex;justify-content:center;align-content:center;margin:30px 0 60px;font-family:Baskerville,"Times New Roman","Liberation Serif",STFangsong,FangSong,FangSong_GB2312,"CWTEX\\-F",serif}.info .author,.info .count,.info .date{margin:0 7px}}.vssue{font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif;margin-top:100px;padding:0}.vssue .vssue-header-powered-by,.vssue .vssue-pagination-page,.vssue .vssue-pagination-per-page{display:none}.vssue .vssue-new-comment{border:0}.vssue .vssue-current-user{line-height:2.5}.vssue .vssue-button-submit-comment:not(:disabled):hover{background-color:#eee}.vssue p{font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif}.vssue .vssue-button-login{border-color:transparent!important}@media screen and (max-width:576px){p{font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif}.post-title{max-width:100%;overflow:hidden;font-size:26px}.katex-display{overflow-x:scroll;overflow-y:hidden}.info{font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif;font-size:14px}}@media print{.page{width:100%}.title{margin-top:30%;font-family:Baskerville,Georgia,"Liberation Serif","Kaiti SC",STKaiti,"AR PL UKai CN","AR PL UKai HK","AR PL UKai TW","AR PL UKai TW MBE","AR PL KaitiM GB",KaiTi,KaiTi_GB2312,DFKai-SB,"TW\\-Kai",serif;font-weight:700}.post-title:after,.post-title:before{content:""}.info{text-align:center;margin-top:40%;page-break-after:always;font-family:Baskerville,Georgia,"Liberation Serif","Kaiti SC",STKaiti,"AR PL UKai CN","AR PL UKai HK","AR PL UKai TW","AR PL UKai TW MBE","AR PL KaitiM GB",KaiTi,KaiTi_GB2312,DFKai-SB,"TW\\-Kai",serif}.info .author{font-size:20px;line-height:2}.count{display:none}code{word-break:break-all}pre{page-break-inside:avoid}.footer,.vssue{display:none}}',""]),n.exports=l},290:function(n,t,e){"use strict";e.r(t);var l=e(12),r=(e(79),e(158),e(28),Object.assign||function(n){for(var i=1;i1&&void 0!==arguments[1]?arguments[1]:{},l=window.Promise||function(n){function t(){}n(t,t)},m=function(n){var t=n.target;t!==D?-1!==$.indexOf(t)&&L({target:t}):z()},f=function(){if(!B&&I.original){var n=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(P-n)>E.scrollOffset&&setTimeout(z,150)}},x=function(n){var t=n.key||n.keyCode;"Escape"!==t&&"Esc"!==t&&27!==t||z()},v=function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=n;if(n.background&&(D.style.background=n.background),n.container&&n.container instanceof Object&&(t.container=r({},E.container,n.container)),n.template){var template=o(n.template)?n.template:document.querySelector(n.template);t.template=template}return E=r({},E,t),$.forEach((function(image){image.dispatchEvent(y("medium-zoom:update",{detail:{zoom:G}}))})),G},k=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return n(r({},E,t))},j=function(){for(var n=arguments.length,t=Array(n),e=0;e0?t.reduce((function(n,t){return[].concat(n,h(t))}),[]):$;return l.forEach((function(image){image.classList.remove("medium-zoom-image"),image.dispatchEvent(y("medium-zoom:detach",{detail:{zoom:G}}))})),$=$.filter((function(image){return-1===l.indexOf(image)})),G},_=function(n,t){var e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return $.forEach((function(image){image.addEventListener("medium-zoom:"+n,t,e)})),R.push({type:"medium-zoom:"+n,listener:t,options:e}),G},S=function(n,t){var e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return $.forEach((function(image){image.removeEventListener("medium-zoom:"+n,t,e)})),R=R.filter((function(e){return!(e.type==="medium-zoom:"+n&&e.listener.toString()===t.toString())})),G},C=function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=n.target,e=function(){var n={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},t=void 0,e=void 0;if(E.container)if(E.container instanceof Object)t=(n=r({},n,E.container)).width-n.left-n.right-2*E.margin,e=n.height-n.top-n.bottom-2*E.margin;else{var l=(o(E.container)?E.container:document.querySelector(E.container)).getBoundingClientRect(),m=l.width,h=l.height,d=l.left,w=l.top;n=r({},n,{width:m,height:h,left:d,top:w})}t=t||n.width-2*E.margin,e=e||n.height-2*E.margin;var y=I.zoomedHd||I.original,f=c(y)?t:y.naturalWidth||t,x=c(y)?e:y.naturalHeight||e,v=y.getBoundingClientRect(),k=v.top,j=v.left,M=v.width,_=v.height,S=Math.min(f,t)/M,C=Math.min(x,e)/_,z=Math.min(S,C),L="scale("+z+") translate3d("+((t-M)/2-j+E.margin+n.left)/z+"px, "+((e-_)/2-k+E.margin+n.top)/z+"px, 0)";I.zoomed.style.transform=L,I.zoomedHd&&(I.zoomedHd.style.transform=L)};return new l((function(n){if(t&&-1===$.indexOf(t))n(G);else{if(I.zoomed)n(G);else{if(t)I.original=t;else{if(!($.length>0))return void n(G);var l=$;I.original=l[0]}if(I.original.dispatchEvent(y("medium-zoom:open",{detail:{zoom:G}})),P=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,B=!0,I.zoomed=w(I.original),document.body.appendChild(D),E.template){var template=o(E.template)?E.template:document.querySelector(E.template);I.template=document.createElement("div"),I.template.appendChild(template.content.cloneNode(!0)),document.body.appendChild(I.template)}if(document.body.appendChild(I.zoomed),window.requestAnimationFrame((function(){document.body.classList.add("medium-zoom--opened")})),I.original.classList.add("medium-zoom-image--hidden"),I.zoomed.classList.add("medium-zoom-image--opened"),I.zoomed.addEventListener("click",z),I.zoomed.addEventListener("transitionend",(function t(){B=!1,I.zoomed.removeEventListener("transitionend",t),I.original.dispatchEvent(y("medium-zoom:opened",{detail:{zoom:G}})),n(G)})),I.original.getAttribute("data-zoom-src")){I.zoomedHd=I.zoomed.cloneNode(),I.zoomedHd.removeAttribute("srcset"),I.zoomedHd.removeAttribute("sizes"),I.zoomedHd.src=I.zoomed.getAttribute("data-zoom-src"),I.zoomedHd.onerror=function(){clearInterval(r),console.warn("Unable to reach the zoom image target "+I.zoomedHd.src),I.zoomedHd=null,e()};var r=setInterval((function(){I.zoomedHd.complete&&(clearInterval(r),I.zoomedHd.classList.add("medium-zoom-image--opened"),I.zoomedHd.addEventListener("click",z),document.body.appendChild(I.zoomedHd),e())}),10)}else if(I.original.hasAttribute("srcset")){I.zoomedHd=I.zoomed.cloneNode(),I.zoomedHd.removeAttribute("sizes"),I.zoomedHd.removeAttribute("loading");var m=I.zoomedHd.addEventListener("load",(function(){I.zoomedHd.removeEventListener("load",m),I.zoomedHd.classList.add("medium-zoom-image--opened"),I.zoomedHd.addEventListener("click",z),document.body.appendChild(I.zoomedHd),e()}))}else e()}}}))},z=function(){return new l((function(n){if(!B&&I.original){B=!0,document.body.classList.remove("medium-zoom--opened"),I.zoomed.style.transform="",I.zoomedHd&&(I.zoomedHd.style.transform=""),I.template&&(I.template.style.transition="opacity 150ms",I.template.style.opacity=0),I.original.dispatchEvent(y("medium-zoom:close",{detail:{zoom:G}})),I.zoomed.addEventListener("transitionend",(function t(){I.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(I.zoomed),I.zoomedHd&&document.body.removeChild(I.zoomedHd),document.body.removeChild(D),I.zoomed.classList.remove("medium-zoom-image--opened"),I.template&&document.body.removeChild(I.template),B=!1,I.zoomed.removeEventListener("transitionend",t),I.original.dispatchEvent(y("medium-zoom:closed",{detail:{zoom:G}})),I.original=null,I.zoomed=null,I.zoomedHd=null,I.template=null,n(G)}))}else n(G)}))},L=function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=n.target;return I.original?z():C({target:t})},A=function(){return E},N=function(){return $},F=function(){return I.original},$=[],R=[],B=!1,P=0,E=e,I={original:null,zoomed:null,zoomedHd:null,template:null};"[object Object]"===Object.prototype.toString.call(t)?E=t:(t||"string"==typeof t)&&j(t),E=r({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},E);var D=d(E.background);document.addEventListener("click",m),document.addEventListener("keyup",x),document.addEventListener("scroll",f),window.addEventListener("resize",z);var G={open:C,close:z,toggle:L,update:v,clone:k,attach:j,detach:M,on:_,off:S,getOptions:A,getImages:N,getZoomedImage:F};return G},x=e(107),v=e.n(x),k=(e(146),e(258)),j=e(259),M=e(284),_={components:{Header:k.a,Footer:j.a},data:function(){return{zoom:null,pageCount:v.a.themeConfig.pageCount,title:"",author:"",date:"",content:"",should404:!1,id:""}},fetch:function(){var n=this;if(!this.$route.params.id)return this.should404=!0;var t=this.$route.params.id.split(".")[0];if(!M[t])return this.should404=!0;Object.entries(M[t]).forEach((function(t){var e=Object(l.a)(t,2),r=e[0],m=e[1];return n[r]=m}))},mounted:function(){this.should404&&(location.href="/404"),this.zoom=f("p img")},head:function(){var n=["//cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css","//cdn.jsdelivr.net/npm/highlight.js@10.5.0/styles/github.css"].map((function(n){return{rel:"stylesheet",href:n}}));return{title:this.title,link:n}}},S=(e(285),e(41)),component=Object(S.a)(_,(function(){var n=this,t=n._self._c;return t("div",[t("Header"),n._v(" "),t("div",{staticClass:"page post-page"},[t("div",{staticClass:"title"},[t("div",{staticClass:"post-title"},[n._v(n._s(n.title))])]),n._v(" "),t("div",{staticClass:"info"},[t("div",{staticClass:"author"},[n._v(n._s(n.author))]),n._v(" "),t("div",{staticClass:"date"},[n._v(n._s(n.date))]),n._v(" "),n.pageCount?t("div",{staticClass:"count"},[t("span",{attrs:{id:"busuanzi_value_page_pv"}}),n._v(" "),t("span",[n._v("views")])]):n._e()]),n._v(" "),t("div",{staticClass:"post-content",domProps:{innerHTML:n._s(n.content)}}),n._v(" "),t("ClientOnly",[t("Vssue",{attrs:{title:"Vssue Demo",issueId:n.id}})],1)],1),n._v(" "),t("Footer")],1)}),[],!1,null,null,null);t.default=component.exports}}]); \ No newline at end of file +(window.webpackJsonp=window.webpackJsonp||[]).push([[7],{250:function(n,t,e){var content=e(253);content.__esModule&&(content=content.default),"string"==typeof content&&(content=[[n.i,content,""]]),content.locals&&(n.exports=content.locals);(0,e(106).default)("0ac003ac",content,!0,{sourceMap:!1})},251:function(n,t,e){var content=e(257);content.__esModule&&(content=content.default),"string"==typeof content&&(content=[[n.i,content,""]]),content.locals&&(n.exports=content.locals);(0,e(106).default)("256bc528",content,!0,{sourceMap:!1})},252:function(n,t,e){"use strict";e(250)},253:function(n,t,e){var l=e(105)(!1);l.push([n.i,'.header-wrap[data-v-249ad96f]{border-bottom:1px solid rgba(0,0,0,.05)}.header[data-v-249ad96f]{display:flex;justify-content:space-between;align-items:center;padding:20px 0}.header .left .motto[data-v-249ad96f]{font-family:Baskerville,Georgia,"Liberation Serif","Kaiti SC",STKaiti,"AR PL UKai CN","AR PL UKai HK","AR PL UKai TW","AR PL UKai TW MBE","AR PL KaitiM GB",KaiTi,KaiTi_GB2312,DFKai-SB,"TW\\-Kai",serif;font-size:16px;color:#000}.header .left .nav[data-v-249ad96f]{font-size:16px;margin-top:10px;color:#900}.header .left .nav a.nav-item[data-v-249ad96f]{color:#900}.header .left .nav a.nav-item[data-v-249ad96f]:not(:last-child):after{content:"/";margin-left:10px;margin-right:10px}.header .left .nav a.nav-item[data-v-249ad96f]:hover{color:#c00}@media print{.nav[data-v-249ad96f],.right[data-v-249ad96f]{display:none}.header-wrap[data-v-249ad96f]{border:0}}@media screen and (max-width:$MQMobile){.header[data-v-249ad96f]{flex-wrap:wrap}.header .left[data-v-249ad96f]{text-align:center;margin-bottom:20px}.left[data-v-249ad96f],.right[data-v-249ad96f]{width:100%}.motto[data-v-249ad96f]{margin-bottom:20px}}',""]),n.exports=l},254:function(n,t,e){var l=e(3);l(l.P,"Array",{fill:e(255)}),e(64)("fill")},255:function(n,t,e){"use strict";var l=e(35),r=e(108),m=e(21);n.exports=function(n){for(var t=l(this),e=m(t.length),o=arguments.length,c=r(o>1?arguments[1]:void 0,e),h=o>2?arguments[2]:void 0,d=void 0===h?e:r(h,e);d>c;)t[c++]=n;return t}},256:function(n,t,e){"use strict";e(251)},257:function(n,t,e){var l=e(105)(!1);l.push([n.i,'.footer[data-v-2c1886a6]{background:#333;color:#fff}.footer .footer-content[data-v-2c1886a6]{padding:20px 0;display:flex;flex-wrap:wrap;justify-content:space-between;color:hsla(0,0%,100%,.6)}.footer .footer-content .power[data-v-2c1886a6]{font-style:normal}.footer .footer-content .logo[data-v-2c1886a6]{font-size:16px;padding:5px 10px;border:1px dashed #fff;display:inline-block;margin-bottom:20px;color:#fff}.footer .footer-content .logo[data-v-2c1886a6]:hover{background-color:#fff;color:#000}.footer .footer-content .footer-title[data-v-2c1886a6]{color:#fff;font-size:12px;margin-bottom:5px;font-weight:bolder;transform:scaley(.8);text-shadow:1px 1px 1px rgba(0,0,0,.4)}.footer .footer-content .footer-text[data-v-2c1886a6]{font-size:12px;margin-bottom:20px}.footer .footer-content .footer-count[data-v-2c1886a6]{font-size:12px;opacity:0;font-size:0}.footer .footer-content .links a.link[data-v-2c1886a6]{color:hsla(0,0%,100%,.6);margin-right:10px;text-decoration:underline}.footer .footer-content .links a.link[data-v-2c1886a6]:hover{background-color:#fff;color:#000}.counter[data-v-2c1886a6]{margin-bottom:10px}.counter .counter-title[data-v-2c1886a6]{color:#fff;font-size:12px;margin-bottom:5px;font-weight:bolder;transform:scaley(.8);text-shadow:1px 1px 1px rgba(0,0,0,.4)}.counter .counter-content[data-v-2c1886a6]{border-radius:2px;padding:5px 10px;background:#fff;box-shadow:inset 1px 2px 10px rgba(0,0,0,.6);border-color:#444 #555 #444 rgba(0,0,0,.6);border-style:solid;border-width:5px}.counter .counter-content .counter-number[data-v-2c1886a6]{display:inline-block;padding:3px 5px;position:relative;background:#333;color:#fff;border-radius:5px;min-width:10px;text-align:center;margin-right:5px}.counter .counter-content .counter-number[data-v-2c1886a6]:last-child{margin:0}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(3){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(3):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(6){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(6):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(9){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(9):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(12){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(12):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(15){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(15):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(18){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(18):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(21){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(21):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(24){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(24):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(27){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(27):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(30){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(30):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(33){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(33):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(36){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(36):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(39){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(39):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(42){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(42):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(45){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(45):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(48){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(48):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(51){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(51):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(54){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(54):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(57){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(57):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(60){margin-left:10px}.counter .counter-content .counter-number[data-v-2c1886a6]:nth-last-child(60):before{content:"";width:2px;height:4px;background:#333;position:absolute;left:-8px;bottom:7px;border-radius:4px}.counter .counter-content .counter-number[data-v-2c1886a6]:first-child{background-color:#8b0000;margin-left:0}.counter .counter-content .counter-number[data-v-2c1886a6]:first-child:before{display:none}',""]),n.exports=l},258:function(n,t,e){"use strict";e(157),e(23);var l=e(107),r=e.n(l),m={data:function(){return{nav:r.a.themeConfig.nav,headTitle:r.a.themeConfig.headTitle}}},o=(e(252),e(41)),component=Object(o.a)(m,(function(){var n=this,t=n._self._c;return t("div",{staticClass:"header-wrap"},[t("div",{staticClass:"header page"},[t("div",{staticClass:"left"},[t("div",{staticClass:"motto"},n._l(n.headTitle,(function(line,e){return t("div",{key:e,staticClass:"line"},[n._v(n._s(line))])})),0),n._v(" "),t("div",{staticClass:"nav"},n._l(n.nav,(function(link,e){return t("a",{key:e,staticClass:"nav-item",attrs:{href:link.link}},[n._v(n._s(link.name))])})),0)]),n._v(" "),t("div",{staticClass:"right"})])])}),[],!1,null,"249ad96f",null);t.a=component.exports},259:function(n,t,e){"use strict";e(157),e(23),e(254);var l=e(107),r=e.n(l),m={data:function(){return{year:(new Date).getFullYear(),friendLinks:r.a.themeConfig.friendLinks,extraFooters:r.a.themeConfig?r.a.themeConfig.extraFooters:[],pvNumber:"00000",uvNumber:"00000",pageCount:r.a.themeConfig.pageCount}},mounted:function(){var n=this,t=document.querySelector("#busuanzi_container_site_pv");new MutationObserver((function(t){var e=t[0].target,l=e.firstChild.textContent,r=e.lastChild.textContent;e.style.display="none";var m=Math.max(l.length,r.length,6);n.pvNumber=new Array(m-l.length).fill("0").join("")+l,n.uvNumber=new Array(m-r.length).fill("0").join("")+r})).observe(t,{attributes:!0})}},o=(e(256),e(41)),component=Object(o.a)(m,(function(){var n=this,t=n._self._c;return t("div",{staticClass:"footer"},[t("div",{staticClass:"footer-content page"},[t("div",{staticClass:"left"},[t("div",{staticClass:"footer-title"},[n._v("FRIEND LINKS")]),n._v(" "),t("div",{staticClass:"links footer-text"},n._l(n.friendLinks,(function(link,e){return t("a",{key:e,staticClass:"link",attrs:{href:link.link}},[n._v(n._s(link.name))])})),0),n._v(" "),n.pageCount?t("div",{staticClass:"footer-text"},[n._m(0),n._v(" "),t("div",{staticClass:"counter"},[t("div",{staticClass:"counter-title"},[n._v("PAGE VIEWS")]),n._v(" "),t("div",{staticClass:"counter-content"},n._l(n.pvNumber,(function(e,l){return t("span",{key:l,staticClass:"counter-number"},[n._v(n._s(e))])})),0)]),n._v(" "),t("div",{staticClass:"counter"},[t("div",{staticClass:"counter-title"},[n._v("USER VIEWS")]),n._v(" "),t("div",{staticClass:"counter-content"},n._l(n.uvNumber,(function(e,l){return t("span",{key:l,staticClass:"counter-number"},[n._v(n._s(e))])})),0)])]):n._e(),n._v(" "),n._l(n.extraFooters,(function(e,l){return t("div",{key:l},[t("div",{staticClass:"footer-title"},[n._v(n._s(e.title))]),n._v(" "),t("div",{staticClass:"footer-text"},[n._v(n._s(e.text))])])}))],2),n._v(" "),n._m(1)])])}),[function(){var n=this._self._c;return n("span",{staticClass:"footer-count",attrs:{id:"busuanzi_container_site_pv"}},[n("span",{attrs:{id:"busuanzi_value_site_pv"}}),this._v(" "),n("span",{attrs:{id:"busuanzi_value_site_uv"}})])},function(){var n=this,t=n._self._c;return t("div",{staticClass:"right"},[t("div",{staticClass:"footer-title power"},[n._v("POWERED BY")]),n._v(" "),t("a",{staticClass:"logo",attrs:{href:"https://github.com/Yidadaa/Issue-Blog-With-Github-Action"}},[n._v("ISSUE BLOG")])])}],!1,null,"2c1886a6",null);t.a=component.exports},266:function(n,t,e){var content=e(286);content.__esModule&&(content=content.default),"string"==typeof content&&(content=[[n.i,content,""]]),content.locals&&(n.exports=content.locals);(0,e(106).default)("2dd2eefc",content,!0,{sourceMap:!1})},284:function(n){n.exports=JSON.parse('{"3":{"id":3,"date":"2017/08/18","author":"Yidadaa","title":"经典统计学习方法——决策树(ID3/C4.5/CART)","mdContent":"最近开始技术♂转型,开始搞机器学习,使用教材是李航老师的《统计学习方法》,基本涵盖了经典的机器学习方法,只不过缺少神经网络部分,准备看完这本书之后,继续学习 [CS231n](http://cs231n.stanford.edu/) 。 \\r\\n\\r\\n前三章的内容都比较基础,第一章介绍了统计学习的基本要素以及基本步骤,第二、三章分别介绍了两种基础的模型:感知机和k近邻法,都比较简单,50行代码就能搞定。 \\r\\n\\r\\n从第四章开始,本书的难度开始逐渐拔高,书中出现了越来越多的数学证明,第四章的朴素贝叶斯分类器中,用到了很多概率论的知识,比如极大似然估计、 贝叶斯估计等。如果要深入机器学习原理的话,还是要回归到线性代数和概率论的学习中。 \\r\\n\\r\\n本文着重讲第五章的内容——决策树。\\r\\n## 决策树简述 \\r\\n决策树是本书出现的第一种真正意义上的高可用方法,当然前一章的朴素贝叶斯分类器也算,但从复杂度熵来讲,决策树更胜一筹。 \\r\\n\\r\\n决策树是一种基本的分类与回归方法,决策树的学习包括:特征选择、决策树的生成和决策树的修剪。其中特征选择决定了决策树的结构,决策树的生成和修剪阶段主要决定决策树的复杂度(深度)。 \\r\\n## 决策树的特征选择\\r\\n决策树特征选择方式的不同,衍生出了不同的算法,比如ID3、C4.5、CART等。以这三种为例,其实ID3与C4.5只是特征选择函数不同,前者依照信息增益,后者依照信息增益比,生成的树的结构很相似,同时也使用相同的剪枝算法。CART较为特殊,后文将其分开来讲。\\r\\n## ID3与C4.5算法\\r\\n决策树生成过程中,通过计算不同特征的划分带来的信息增益,可以决定是否继续生成决策树。 \\r\\n\\r\\n这两种算法生成的决策树,每一支子树都是一个特征值的可能值,使用时只需要按照标定的顺序,对输入变量各个特征值沿着树从根节点一直比对,延续到的叶节点就标定了该输入变量的最终分类。","content":"

最近开始技术♂转型,开始搞机器学习,使用教材是李航老师的《统计学习方法》,基本涵盖了经典的机器学习方法,只不过缺少神经网络部分,准备看完这本书之后,继续学习 CS231n

\\n

前三章的内容都比较基础,第一章介绍了统计学习的基本要素以及基本步骤,第二、三章分别介绍了两种基础的模型:感知机和k近邻法,都比较简单,50行代码就能搞定。

\\n

从第四章开始,本书的难度开始逐渐拔高,书中出现了越来越多的数学证明,第四章的朴素贝叶斯分类器中,用到了很多概率论的知识,比如极大似然估计、 贝叶斯估计等。如果要深入机器学习原理的话,还是要回归到线性代数和概率论的学习中。

\\n

本文着重讲第五章的内容——决策树。

\\n

决策树简述

\\n

决策树是本书出现的第一种真正意义上的高可用方法,当然前一章的朴素贝叶斯分类器也算,但从复杂度熵来讲,决策树更胜一筹。

\\n

决策树是一种基本的分类与回归方法,决策树的学习包括:特征选择、决策树的生成和决策树的修剪。其中特征选择决定了决策树的结构,决策树的生成和修剪阶段主要决定决策树的复杂度(深度)。

\\n

决策树的特征选择

\\n

决策树特征选择方式的不同,衍生出了不同的算法,比如ID3、C4.5、CART等。以这三种为例,其实ID3与C4.5只是特征选择函数不同,前者依照信息增益,后者依照信息增益比,生成的树的结构很相似,同时也使用相同的剪枝算法。CART较为特殊,后文将其分开来讲。

\\n

ID3与C4.5算法

\\n

决策树生成过程中,通过计算不同特征的划分带来的信息增益,可以决定是否继续生成决策树。

\\n

这两种算法生成的决策树,每一支子树都是一个特征值的可能值,使用时只需要按照标定的顺序,对输入变量各个特征值沿着树从根节点一直比对,延续到的叶节点就标定了该输入变量的最终分类。

\\n"},"4":{"id":4,"date":"2017/08/29","author":"Yidadaa","title":"物欲","mdContent":"初中时清心寡欲,最大的愿望是能每天从午饭钱中剩下五毛钱,然后周末的时候能有钱去一趟网吧,在有限的一两个小时内下满那张2G的内存卡,那时的同龄人经常有人问我去不去网吧,我说去,但是不玩游戏,只下载东西。仅有的网吧时光,启蒙了我对计算机的认识,但仅仅停留在使用的阶段,但那时我已有了一台学习机,供我娱乐以及编程。彼时的物欲,仅仅局限在3.5寸的屏幕内。\\r\\n\\r\\n高中时安卓设备崛起,我也第一次离家寄宿,可以掌控更多的生活费,正是如此,我的物欲开始膨胀。我十分渴望一台智能设备,并最终接二连三地缩减三餐费用,把这些钱拿来买各种各样的电子设备。数码杂志上朝思暮想的东西握在手里,我激动异常,把每一个用来睡眠和休息的夜晚和课间花在了各种各样的游戏上,我也渐渐变成了夜猫子。彼时,我的物欲渐渐膨胀,从3.5寸到4.3寸,再到7寸。\\r\\n\\r\\n高考前夕,我拥有了自己的笔记本电脑。进入大学后,这台电脑帮助我产出了各色的海报和成千上万行代码,当第一笔实习工资打到工资卡上时,我长舒一口气——我仿佛经能掌控自己的生活了。可是好景不长,我察觉到并不是这么回事,一不留神,白条就负债累累,更强大更智能的硬件层出不穷,自己的钱包和身体一样虚弱不堪,长年累月的熬夜犹如一场凌迟,一点一点掏空身体。此时,物欲已成长为庞然大物,不仅开始反噬,而且蒙蔽了我的双眼。\\r\\n\\r\\n我在最难熬的时候,装模作样地看过一些哲学书籍,也时常思索些高深的命题,虽然看起来很可笑,但这种思考让我明白,我是一种智慧生命,我会思考自己为何存在。但更多时候,我还是被物欲所支配着,渴望这个,渴望那个,时不时地拖延偷懒,到了真正考验自己本事的时候,被人打脸打的啪啪响,心里还不服,暗搓搓地想着等我哪天认真起来了,让你们都好看。**可是我知道,那始终是一句空话,就像我过去立的这么多flag一样,毫无意义。**\\r\\n\\r\\n转念一想,自己这幅德行,全都赖到“物欲”头上吗?物欲始终是个人为定义的概念,无法脱离行为而存在,若是没有那些虚与委蛇和敷衍了事时种下的因,又拿来这般扭曲不堪的果呢?我不得不承认我的心态已经有了“跑偏”的迹象,彼时对金钱嗤之以鼻,此时为求果腹不择手段(当然这是夸张的说法,只是觉得把大学的时光花费在乱七八糟的东西上实在该打),若不自省,日后不留神就会犯下大错。\\r\\n\\r\\n我还没想明白,自己究竟想要什么,站在这边的山上,觉得那边的山高,思来想去却没半点行路的意思。","content":"

初中时清心寡欲,最大的愿望是能每天从午饭钱中剩下五毛钱,然后周末的时候能有钱去一趟网吧,在有限的一两个小时内下满那张2G的内存卡,那时的同龄人经常有人问我去不去网吧,我说去,但是不玩游戏,只下载东西。仅有的网吧时光,启蒙了我对计算机的认识,但仅仅停留在使用的阶段,但那时我已有了一台学习机,供我娱乐以及编程。彼时的物欲,仅仅局限在3.5寸的屏幕内。

\\n

高中时安卓设备崛起,我也第一次离家寄宿,可以掌控更多的生活费,正是如此,我的物欲开始膨胀。我十分渴望一台智能设备,并最终接二连三地缩减三餐费用,把这些钱拿来买各种各样的电子设备。数码杂志上朝思暮想的东西握在手里,我激动异常,把每一个用来睡眠和休息的夜晚和课间花在了各种各样的游戏上,我也渐渐变成了夜猫子。彼时,我的物欲渐渐膨胀,从3.5寸到4.3寸,再到7寸。

\\n

高考前夕,我拥有了自己的笔记本电脑。进入大学后,这台电脑帮助我产出了各色的海报和成千上万行代码,当第一笔实习工资打到工资卡上时,我长舒一口气——我仿佛经能掌控自己的生活了。可是好景不长,我察觉到并不是这么回事,一不留神,白条就负债累累,更强大更智能的硬件层出不穷,自己的钱包和身体一样虚弱不堪,长年累月的熬夜犹如一场凌迟,一点一点掏空身体。此时,物欲已成长为庞然大物,不仅开始反噬,而且蒙蔽了我的双眼。

\\n

我在最难熬的时候,装模作样地看过一些哲学书籍,也时常思索些高深的命题,虽然看起来很可笑,但这种思考让我明白,我是一种智慧生命,我会思考自己为何存在。但更多时候,我还是被物欲所支配着,渴望这个,渴望那个,时不时地拖延偷懒,到了真正考验自己本事的时候,被人打脸打的啪啪响,心里还不服,暗搓搓地想着等我哪天认真起来了,让你们都好看。可是我知道,那始终是一句空话,就像我过去立的这么多flag一样,毫无意义。

\\n

转念一想,自己这幅德行,全都赖到“物欲”头上吗?物欲始终是个人为定义的概念,无法脱离行为而存在,若是没有那些虚与委蛇和敷衍了事时种下的因,又拿来这般扭曲不堪的果呢?我不得不承认我的心态已经有了“跑偏”的迹象,彼时对金钱嗤之以鼻,此时为求果腹不择手段(当然这是夸张的说法,只是觉得把大学的时光花费在乱七八糟的东西上实在该打),若不自省,日后不留神就会犯下大错。

\\n

我还没想明白,自己究竟想要什么,站在这边的山上,觉得那边的山高,思来想去却没半点行路的意思。

\\n"},"5":{"id":5,"date":"2017/09/07","author":"Yidadaa","title":"LaTex on Linux配置指南 - TexLive","mdContent":"> 系统:Ubuntu 17.04 \\r\\n> 软件搭配:texLive2017 + VSCode\\r\\n\\r\\n## 简述 \\r\\n\\r\\n导师要求我用英文写周报,而且强制要求用LaTex完成,于是乎就花了点时间配置了一下LaTex环境。目前有很多IDE形式的LaTex集成环境了,但是我喜欢用一个编辑器搞定所有事情,于是选用了TexLive + VSCode的配置,其他配置可以参考这个[知乎答案](https://www.zhihu.com/question/19954023)。\\r\\n\\r\\n![2017-09-07 19-21-20](https://user-images.githubusercontent.com/16968934/30162000-78822d32-9405-11e7-9094-7d234b2a1b2a.png)\\r\\n\\r\\n\\r\\n## 安装VSCode \\r\\n\\r\\nVSCode是微软出的新一代编辑器,拥有很强大的插件市场支持,几乎可以胜任所有的开发工作。安装步骤如下:\\r\\n\\r\\n1. 从[官网](https://code.visualstudio.com/)下载`.deb`安装包;\\r\\n2. 在命令行中,切换到安装包所在目录,执行`dpkg -i 安装包名字.deb`;\\r\\n3. 安装安装包所需依赖,继续输入`apt-get -f install`。\\r\\n\\r\\n至此VSCode就安装完成了,打开VSCode,按下快捷键`CTRL+SHIFT+X`,搜索插件**LaTex Workshop**,安装并重启编辑器。\\r\\n\\r\\n至此,编辑器就装好了。\\r\\n\\r\\n## 安装TexLive\\r\\n\\r\\nTexLive是一个tex发行版,其他的介绍就不啰嗦了,直接开始安装。\\r\\n\\r\\n安装过程很简单,执行以下命令:\\r\\n```\\r\\nsudo apt-get install texlive-xetex texlive-latex-extra texlive-science\\r\\n```\\r\\n\\r\\n## 使用\\r\\n打开VSCode,新建一个目录,然后在目录中新建一个`.tex`文件,即可使用tex进行写作了,如果要开启即时预览功能,需要先编译该tex文件,按下`F1`,输入命令`build LaTex project`并回车,你会发现目录下面生成了一堆乱七八糟的东西,不用管,再次按下`F1`,输入命令`View PDF file in new tab`,就可以开启即时预览了,每一次保存后,预览窗口就会自动刷新。\\r\\n\\r\\n此外,为了支持中文,需要修改`Latex Workshop`的设置项,在VScode的设置文件中添加下面几行:\\r\\n```json\\r\\n\\"latex-workshop.latex.recipes\\": [\\r\\n {\\r\\n \\"name\\": \\"xelatex\\",\\r\\n \\"tools\\": [\\r\\n \\"xelatex\\"\\r\\n ]\\r\\n },\\r\\n {\\r\\n \\"name\\": \\"bibtex->xelatex\\",\\r\\n \\"tools\\": [\\r\\n \\"xelatex\\",\\r\\n \\"bibtex\\",\\r\\n \\"xelatex\\",\\r\\n \\"xelatex\\"\\r\\n ]\\r\\n }\\r\\n],\\r\\n\\"latex-workshop.latex.tools\\": [\\r\\n {\\r\\n \\"name\\": \\"xelatex\\",\\r\\n \\"command\\": \\"xelatex\\",\\r\\n \\"args\\": [\\r\\n \\"%DOC%.tex\\"\\r\\n ]\\r\\n }, {\\r\\n \\"name\\": \\"bibtex\\",\\r\\n \\"command\\": \\"bibtex\\",\\r\\n \\"args\\": [\\r\\n \\"%DOCFILE%\\"\\r\\n ]\\r\\n }\\r\\n]\\r\\n```\\r\\n这里笔者提供了两条编译命令流,一个是不需要引用的纯`xelatex`,一般用于课程设计报告的书写;另一个是包含了`bibtex`的命令流,一般用于会议论文的书写。\\r\\n\\r\\n然后新建一个文件测试一下:\\r\\n```latex\\r\\n\\\\documentclass[12pt]{article}\\r\\n\\\\usepackage{fontspec}\\r\\n\\\\setmainfont{宋体}\\r\\n\\\\title{镜中}\\r\\n\\\\author{张枣}\\r\\n\\\\date{}\\r\\n\\r\\n\\\\begin{document}\\r\\n\\\\begin{center}\\r\\n 只要想起一生中后悔的事情\\\\\\\\\\r\\n 梅花便落了下来\\\\\\\\\\r\\n 比如看她游泳到河的另一岸\\\\\\\\\\r\\n 比如登上一株松木梯子\\\\\\\\\\r\\n 危险的事情固然美丽\\\\\\\\\\r\\n 不如看她骑马归来\\\\\\\\\\r\\n 面颊温暖,\\\\\\\\\\r\\n 羞愧。低下头,回答着皇帝\\\\\\\\\\r\\n 一面镜子永远等候她\\\\\\\\\\r\\n 让她坐到镜中常坐的地方\\\\\\\\\\r\\n 望着窗外,只要想起一生中后悔的事情\\\\\\\\\\r\\n 梅花便落满了南山\\\\\\\\\\r\\n\\\\end{center}\\r\\n\\r\\n\\\\end{document}\\r\\n```\\r\\n![_code_20171125165943](https://user-images.githubusercontent.com/16968934/33229051-22acaa0a-d202-11e7-9eb8-a7f9e9d1719f.png)\\r\\n\\r\\n\\r\\n## 参考链接\\r\\n1. [Linux下texLive的安装和配置](http://blog.csdn.net/matricer/article/details/52253658)\\r\\n2. [使用xelatex生成中文pdf](http://blog.jqian.net/post/xelatex.html)","content":"
\\n

系统:Ubuntu 17.04
\\n软件搭配:texLive2017 + VSCode

\\n
\\n

简述

\\n

导师要求我用英文写周报,而且强制要求用LaTex完成,于是乎就花了点时间配置了一下LaTex环境。目前有很多IDE形式的LaTex集成环境了,但是我喜欢用一个编辑器搞定所有事情,于是选用了TexLive + VSCode的配置,其他配置可以参考这个知乎答案

\\n

\\"2017-09-07

\\n

安装VSCode

\\n

VSCode是微软出的新一代编辑器,拥有很强大的插件市场支持,几乎可以胜任所有的开发工作。安装步骤如下:

\\n
    \\n
  1. 官网下载.deb安装包;
  2. \\n
  3. 在命令行中,切换到安装包所在目录,执行dpkg -i 安装包名字.deb
  4. \\n
  5. 安装安装包所需依赖,继续输入apt-get -f install
  6. \\n
\\n

至此VSCode就安装完成了,打开VSCode,按下快捷键CTRL+SHIFT+X,搜索插件LaTex Workshop,安装并重启编辑器。

\\n

至此,编辑器就装好了。

\\n

安装TexLive

\\n

TexLive是一个tex发行版,其他的介绍就不啰嗦了,直接开始安装。

\\n

安装过程很简单,执行以下命令:

\\n
sudo apt-get install texlive-xetex texlive-latex-extra texlive-science\\n
\\n

使用

\\n

打开VSCode,新建一个目录,然后在目录中新建一个.tex文件,即可使用tex进行写作了,如果要开启即时预览功能,需要先编译该tex文件,按下F1,输入命令build LaTex project并回车,你会发现目录下面生成了一堆乱七八糟的东西,不用管,再次按下F1,输入命令View PDF file in new tab,就可以开启即时预览了,每一次保存后,预览窗口就会自动刷新。

\\n

此外,为了支持中文,需要修改Latex Workshop的设置项,在VScode的设置文件中添加下面几行:

\\n
"latex-workshop.latex.recipes": [\\n    {\\n        "name": "xelatex",\\n        "tools": [\\n            "xelatex"\\n        ]\\n    },\\n    {\\n        "name": "bibtex->xelatex",\\n        "tools": [\\n            "xelatex",\\n            "bibtex",\\n            "xelatex",\\n            "xelatex"\\n        ]\\n    }\\n],\\n"latex-workshop.latex.tools": [\\n    {\\n        "name": "xelatex",\\n        "command": "xelatex",\\n        "args": [\\n            "%DOC%.tex"\\n        ]\\n    }, {\\n        "name": "bibtex",\\n        "command": "bibtex",\\n        "args": [\\n            "%DOCFILE%"\\n        ]\\n    }\\n]\\n
\\n

这里笔者提供了两条编译命令流,一个是不需要引用的纯xelatex,一般用于课程设计报告的书写;另一个是包含了bibtex的命令流,一般用于会议论文的书写。

\\n

然后新建一个文件测试一下:

\\n
\\\\documentclass[12pt]{article}\\n\\\\usepackage{fontspec}\\n\\\\setmainfont{宋体}\\n\\\\title{镜中}\\n\\\\author{张枣}\\n\\\\date{}\\n\\n\\\\begin{document}\\n\\\\begin{center}\\n    只要想起一生中后悔的事情\\\\\\\\\\n    梅花便落了下来\\\\\\\\\\n    比如看她游泳到河的另一岸\\\\\\\\\\n    比如登上一株松木梯子\\\\\\\\\\n    危险的事情固然美丽\\\\\\\\\\n    不如看她骑马归来\\\\\\\\\\n    面颊温暖,\\\\\\\\\\n    羞愧。低下头,回答着皇帝\\\\\\\\\\n    一面镜子永远等候她\\\\\\\\\\n    让她坐到镜中常坐的地方\\\\\\\\\\n    望着窗外,只要想起一生中后悔的事情\\\\\\\\\\n    梅花便落满了南山\\\\\\\\\\n\\\\end{center}\\n\\n\\\\end{document}\\n
\\n

\\"_code_20171125165943\\"

\\n

参考链接

\\n
    \\n
  1. Linux下texLive的安装和配置
  2. \\n
  3. 使用xelatex生成中文pdf
  4. \\n
\\n"},"6":{"id":6,"date":"2017/09/23","author":"Yidadaa","title":"如何优雅地使用服务器","mdContent":"最近师兄给我分配了一个实验室服务器地账号,自己就琢磨着怎么好好地折腾一下这个服务器,但无奈我的账号没有root权限,安装软件只能通过自己编译的方式完成,并且师兄只分配给我一个端口,我要想运行什么web服务,只能绑定到这一个端口上面,一头雾水之际,突然看到了一个神器:SSH端口转发。\\r\\n\\r\\n## 什么是SSH端口转发?\\r\\n\\r\\n让我们先来了解一下端口转发的概念吧。我们知道,SSH 会自动加密和解密所有 SSH 客户端与服务端之间的网络数据。但是,SSH 还同时提供了一个非常有用的功能,这就是端口转发。它能够将其他 TCP 端口的网络数据通过 SSH 链接来转发,并且自动提供了相应的加密及解密服务。这一过程有时也被叫做“隧道”(tunneling),这是因为 SSH 为其他 TCP 链接提供了一个安全的通道来进行传输而得名。例如,Telnet,SMTP,LDAP 这些 TCP 应用均能够从中得益,避免了用户名,密码以及隐私信息的明文传输。而与此同时,如果您工作环境中的防火墙限制了一些网络端口的使用,但是允许 SSH 的连接,那么也是能够通过将 TCP 端口转发来使用 SSH 进行通讯。总的来说 SSH 端口转发能够提供两大功能:\\r\\n- 加密 SSH Client 端至 SSH Server 端之间的通讯数据。\\r\\n- 突破防火墙的限制完成一些之前无法建立的 TCP 连接。\\r\\n\\r\\n上面是我复制粘贴[别人的](https://www.ibm.com/developerworks/cn/linux/l-cn-sshforward/),总而言之,端口转发就是将所有发送到server_1的m端口的请求转发到server_2的n端口上去,并且两个端口之间的转发,通过SSH加密完成,也就是说,只需要涉密服务器上的一个SSH端口,就可以通过端口转发功能来为我们提供各种各样的服务。\\r\\n\\r\\n## 如何使用端口转发?\\r\\n\\r\\n首先要确定你电脑装了ssh,命令行里输一下ssh即可,然后使用下面的命令:\\r\\n```bash\\r\\nssh -L :: \\r\\n```\\r\\n举个例子,我服务器的IP地址是`123.123.1.123`,SSH端口号是`88`,我在服务器的`8888`端口上开了一个`jupyter-notebook`的服务,那么我要想在我的笔记本上使用这个服务,就要将本地的`8888`端口给映射到服务器的`8888`端口去,那么我只需要输入命令`ssh -L 8888:localhost:8888 username@123.123.1.123 -p 88`,然后输入密码,映射就成功了,这时候访问本地的`8888`端口,就相当于访问服务器的`8888`端口了。\\r\\n\\r\\n## 还有其他的吗?\\r\\n当然有啦,为了全面模拟xshell的使用体验(大雾),当然还需要一个静态文件服务啦,可以用一行代码`python -m SimpleHTTPServer 100`在100端口开一个静态文件服务,然后用`ssh`转发100端口,这样就就可以在本地查看服务器的文件了,可以在调参的时候看生成的图片文件,当然了,我的野心不止于此,之后开始跑网络的时候,可以也用来扩展TensorBoard的功能。\\r\\n\\r\\n除此之外,还有其他的神奇的命令行可以使用,比如终端管理软件tmux,功能上与screen差不过,如果不不了解screen,可以查阅一下,tmux赋予了命令行的窗口化功能,可以分割、调整当前的终端窗口,经过配置之后可以获得非常不错的效果,当然啦,具体的配置还是去网上搜吧,一千人的电脑里有一千个终端配置,程序员还是要有自己的风格才行。","content":"

最近师兄给我分配了一个实验室服务器地账号,自己就琢磨着怎么好好地折腾一下这个服务器,但无奈我的账号没有root权限,安装软件只能通过自己编译的方式完成,并且师兄只分配给我一个端口,我要想运行什么web服务,只能绑定到这一个端口上面,一头雾水之际,突然看到了一个神器:SSH端口转发。

\\n

什么是SSH端口转发?

\\n

让我们先来了解一下端口转发的概念吧。我们知道,SSH 会自动加密和解密所有 SSH 客户端与服务端之间的网络数据。但是,SSH 还同时提供了一个非常有用的功能,这就是端口转发。它能够将其他 TCP 端口的网络数据通过 SSH 链接来转发,并且自动提供了相应的加密及解密服务。这一过程有时也被叫做“隧道”(tunneling),这是因为 SSH 为其他 TCP 链接提供了一个安全的通道来进行传输而得名。例如,Telnet,SMTP,LDAP 这些 TCP 应用均能够从中得益,避免了用户名,密码以及隐私信息的明文传输。而与此同时,如果您工作环境中的防火墙限制了一些网络端口的使用,但是允许 SSH 的连接,那么也是能够通过将 TCP 端口转发来使用 SSH 进行通讯。总的来说 SSH 端口转发能够提供两大功能:

\\n
    \\n
  • 加密 SSH Client 端至 SSH Server 端之间的通讯数据。
  • \\n
  • 突破防火墙的限制完成一些之前无法建立的 TCP 连接。
  • \\n
\\n

上面是我复制粘贴别人的,总而言之,端口转发就是将所有发送到server_1的m端口的请求转发到server_2的n端口上去,并且两个端口之间的转发,通过SSH加密完成,也就是说,只需要涉密服务器上的一个SSH端口,就可以通过端口转发功能来为我们提供各种各样的服务。

\\n

如何使用端口转发?

\\n

首先要确定你电脑装了ssh,命令行里输一下ssh即可,然后使用下面的命令:

\\n
ssh -L <local port>:<remote host>:<remote port> <SSH hostname>\\n
\\n

举个例子,我服务器的IP地址是123.123.1.123,SSH端口号是88,我在服务器的8888端口上开了一个jupyter-notebook的服务,那么我要想在我的笔记本上使用这个服务,就要将本地的8888端口给映射到服务器的8888端口去,那么我只需要输入命令ssh -L 8888:localhost:8888 username@123.123.1.123 -p 88,然后输入密码,映射就成功了,这时候访问本地的8888端口,就相当于访问服务器的8888端口了。

\\n

还有其他的吗?

\\n

当然有啦,为了全面模拟xshell的使用体验(大雾),当然还需要一个静态文件服务啦,可以用一行代码python -m SimpleHTTPServer 100在100端口开一个静态文件服务,然后用ssh转发100端口,这样就就可以在本地查看服务器的文件了,可以在调参的时候看生成的图片文件,当然了,我的野心不止于此,之后开始跑网络的时候,可以也用来扩展TensorBoard的功能。

\\n

除此之外,还有其他的神奇的命令行可以使用,比如终端管理软件tmux,功能上与screen差不过,如果不不了解screen,可以查阅一下,tmux赋予了命令行的窗口化功能,可以分割、调整当前的终端窗口,经过配置之后可以获得非常不错的效果,当然啦,具体的配置还是去网上搜吧,一千人的电脑里有一千个终端配置,程序员还是要有自己的风格才行。

\\n"},"8":{"id":8,"date":"2017/10/28","author":"Yidadaa","title":"如何有效地预估工作量","mdContent":"在实际开发过程中,难免要对自己手头的工作进行工作量预估,其实笔者一开始预估工作量的时候总是感到没谱,往往会得出过于乐观的结论,也就是所谓的“程序员的乐观”,老鸟程序员经常告诫我们说:“宁可多算一周,不可少估一天”。过于乐观地估计工作量,不仅会让自己疲于赶进度,还会连累其他的开发伙伴。所以本文就着重讲一下如何行之有效地做出正确的工作量预估。\\r\\n\\r\\n由于笔者能力有限,本文只讲单个开发者的情况,如果是团队leader进行规划的话,要考虑的东西只会更多,笔者也没有团队规划的经验,就不在此强答了。\\r\\n\\r\\n### 第一步:读懂需求,细分步骤。\\r\\n\\r\\n对需求一知半解就盲目地进行开发是**是非常危险**的行为,轻则遗漏细节,重则全盘皆错。很多开发者都不怎么重视这一阶段,觉得只有开始做之后才能逐渐地理解需求,其实不然,这一阶段要求对需求全貌有一个把握,把整个需求的难点和耗时的点给找出来,方便开发时认真对待。\\r\\n\\r\\n对于大部分需求,都能找出明确的步骤划分。比如我要做一个表格展示页,用来展示一些从后端取回的数据,那么就可以将之划分为编写页面和集成后端接口两个部分,而且根据自己以往的开发经验,还能大概知道哪里比较耗时,哪里还需要进一步地了解。\\r\\n\\r\\n### 第二步:把握轻重缓急,优先估计熟悉部分。\\r\\n\\r\\n无论项目复杂与否,肯定有些部分是自己最熟悉的,那么我们就优先从这些地方下手,根据以往的开发经验,确定一个时间点A,并将此确定为开发的第一阶段。\\r\\n\\r\\n### 第三步:肢解生疏需求,消灭项目盲点。\\r\\n\\r\\n除却熟悉的部分,下面就是真正的耗时耗力的阶段了,不熟悉的部分可以有三种形式:对技术细节不熟悉、对具体需求不熟悉和对团队协作过程不熟悉。\\r\\n\\r\\n1. **不熟悉的技术细节**。这时要单独估算学习新的技术知识所需的时间,根据未知知识与自己已有的储备知识的不同疏离程度,要分配不同的时间段B,比如对于前端来说,如果要学习的部分属于后端范畴或者是自己没有接触过的算法部分,那么就要相应的预估长一点。\\r\\n\\r\\n2. **不熟悉的需求细节**。如果拿到的需求文档有些部分语焉不详,自己就要长个心眼,把与制定需求的人沟通的过程也要估算在内,划定一个时间段C。\\r\\n\\r\\n3. **不熟悉的协调过程**。这种情况一般发生在刚接触新项目时,自己对项目的管理规范等等不熟悉,这时也要将沟通时间估算进去,划定时间段D。\\r\\n\\r\\n经过以上一番考虑,基本上就可以确定出一个比较具体的时间范围了。一定要切记,很多时候团队leader让成员估算开发时间时,最关心的往往并不是某个人能不能及时把手头的工作做完,而是如何分配,使得所有成员能够在指定的时间内完成一整个任务。所以错误的预估工作量和耗时,很可能会延误和其他人员的对接,造成整个项目完成时间的延后。\\r\\n\\r\\n*注:以上文字整理自笔者与某位年长十年的程序员的聊天记录,可能不适用于某些开发情况。*","content":"

在实际开发过程中,难免要对自己手头的工作进行工作量预估,其实笔者一开始预估工作量的时候总是感到没谱,往往会得出过于乐观的结论,也就是所谓的“程序员的乐观”,老鸟程序员经常告诫我们说:“宁可多算一周,不可少估一天”。过于乐观地估计工作量,不仅会让自己疲于赶进度,还会连累其他的开发伙伴。所以本文就着重讲一下如何行之有效地做出正确的工作量预估。

\\n

由于笔者能力有限,本文只讲单个开发者的情况,如果是团队leader进行规划的话,要考虑的东西只会更多,笔者也没有团队规划的经验,就不在此强答了。

\\n

第一步:读懂需求,细分步骤。

\\n

对需求一知半解就盲目地进行开发是是非常危险的行为,轻则遗漏细节,重则全盘皆错。很多开发者都不怎么重视这一阶段,觉得只有开始做之后才能逐渐地理解需求,其实不然,这一阶段要求对需求全貌有一个把握,把整个需求的难点和耗时的点给找出来,方便开发时认真对待。

\\n

对于大部分需求,都能找出明确的步骤划分。比如我要做一个表格展示页,用来展示一些从后端取回的数据,那么就可以将之划分为编写页面和集成后端接口两个部分,而且根据自己以往的开发经验,还能大概知道哪里比较耗时,哪里还需要进一步地了解。

\\n

第二步:把握轻重缓急,优先估计熟悉部分。

\\n

无论项目复杂与否,肯定有些部分是自己最熟悉的,那么我们就优先从这些地方下手,根据以往的开发经验,确定一个时间点A,并将此确定为开发的第一阶段。

\\n

第三步:肢解生疏需求,消灭项目盲点。

\\n

除却熟悉的部分,下面就是真正的耗时耗力的阶段了,不熟悉的部分可以有三种形式:对技术细节不熟悉、对具体需求不熟悉和对团队协作过程不熟悉。

\\n
    \\n
  1. \\n

    不熟悉的技术细节。这时要单独估算学习新的技术知识所需的时间,根据未知知识与自己已有的储备知识的不同疏离程度,要分配不同的时间段B,比如对于前端来说,如果要学习的部分属于后端范畴或者是自己没有接触过的算法部分,那么就要相应的预估长一点。

    \\n
  2. \\n
  3. \\n

    不熟悉的需求细节。如果拿到的需求文档有些部分语焉不详,自己就要长个心眼,把与制定需求的人沟通的过程也要估算在内,划定一个时间段C。

    \\n
  4. \\n
  5. \\n

    不熟悉的协调过程。这种情况一般发生在刚接触新项目时,自己对项目的管理规范等等不熟悉,这时也要将沟通时间估算进去,划定时间段D。

    \\n
  6. \\n
\\n

经过以上一番考虑,基本上就可以确定出一个比较具体的时间范围了。一定要切记,很多时候团队leader让成员估算开发时间时,最关心的往往并不是某个人能不能及时把手头的工作做完,而是如何分配,使得所有成员能够在指定的时间内完成一整个任务。所以错误的预估工作量和耗时,很可能会延误和其他人员的对接,造成整个项目完成时间的延后。

\\n

注:以上文字整理自笔者与某位年长十年的程序员的聊天记录,可能不适用于某些开发情况。

\\n"},"9":{"id":9,"date":"2017/11/02","author":"Yidadaa","title":"数据挖掘复习内容","mdContent":"# 数据挖掘期末复习\\r\\n## 第一章内容\\r\\n1. 什么是数据挖掘,数据挖掘与其他学科的联系。\\r\\n2. 知识发现的流程。数据挖掘是数据发现的核心。\\r\\n3. 数据挖掘的主要任务。关联规则挖掘、分类或回归、聚类和异常点检测。\\r\\n\\r\\n## 第二章 认识数据\\r\\n\\r\\n### 相似度计算\\r\\n计算欧氏距离以及另外一个算法。\\r\\n\\r\\n### 数据的统计描述\\r\\n包括数据的中心性描述(中位数、众数)和散度(极值、方差、百分位点)。\\r\\n\\r\\n### 数据预处理\\r\\n1. 数据清洗。噪声检测及缺失值处理。\\r\\n2. 数据集成。冗余分析和相关分析(卡方分析)。\\r\\n\\r\\n### 数据变换\\r\\n1. 最小最大归一化。\\r\\n2. (X-期望)/标准差\\r\\n\\r\\n## 第三章 数据仓库\\r\\n\\r\\n### 什么是数据仓库\\r\\n数据仓库是面向主题的、非易失的、随时间变化的、集成的。\\r\\n\\r\\n### 多维数据模型\\r\\n星型模型、雪花模型和事实星座模型。\\r\\n\\r\\n## 第四章 关联规则\\r\\n\\r\\n### 什么是频繁项集,如何从项集中获取关联规则\\r\\n数据中支持度大于最小支持度的的项集为频繁项集。\\r\\n\\r\\n### Apriori算法(重点必考)\\r\\n题型是从给定事务集合中计算关联规则。\\r\\n\\r\\n## 第五章 分类\\r\\n1. 监督学习和非监督学习\\r\\n2. 生成模型和判别模型。\\r\\n3. 分类和回归的异同。都是监督学习。一个是离散,一个是连续。\\r\\n\\r\\n### 决策树(重点)\\r\\n避免过拟合:增加数据量,降低模型复杂度。\\r\\n决策树通过剪枝来避免过拟合。\\r\\n\\r\\n### KNN(重点)\\r\\n属于懒惰学习,没有训练过程。 \\r\\n缺点:对K敏感 \\r\\n优点:无需训练 \\r\\n\\r\\n### 其他算法\\r\\n朴素贝叶斯、SVM、ANN和BP网络。\\r\\n\\r\\n### 评价指标\\r\\n准确率,召回率,敏感度,精度,F1\\r\\n\\r\\n## 第六章 聚类\\r\\n\\r\\n### 什么是聚类\\r\\n\\r\\n### 聚类的分类及相应算法\\r\\n1. 基于划分的算法。k-means,k中心\\r\\n2. 基于密度。DB scan\\r\\n3. 基于层次。层次聚类\\r\\n4. 基于网格。\\r\\n\\r\\n### k-means(重点)\\r\\nk-means的流程。\\r\\n\\r\\n## 第七章 异常检测\\r\\n\\r\\n### 什么是异常\\r\\n\\r\\n### 异常的类型\\r\\n全局、局部、集体、情景\\r\\n\\r\\n### LOF\\r\\n","content":"

数据挖掘期末复习

\\n

第一章内容

\\n
    \\n
  1. 什么是数据挖掘,数据挖掘与其他学科的联系。
  2. \\n
  3. 知识发现的流程。数据挖掘是数据发现的核心。
  4. \\n
  5. 数据挖掘的主要任务。关联规则挖掘、分类或回归、聚类和异常点检测。
  6. \\n
\\n

第二章 认识数据

\\n

相似度计算

\\n

计算欧氏距离以及另外一个算法。

\\n

数据的统计描述

\\n

包括数据的中心性描述(中位数、众数)和散度(极值、方差、百分位点)。

\\n

数据预处理

\\n
    \\n
  1. 数据清洗。噪声检测及缺失值处理。
  2. \\n
  3. 数据集成。冗余分析和相关分析(卡方分析)。
  4. \\n
\\n

数据变换

\\n
    \\n
  1. 最小最大归一化。
  2. \\n
  3. (X-期望)/标准差
  4. \\n
\\n

第三章 数据仓库

\\n

什么是数据仓库

\\n

数据仓库是面向主题的、非易失的、随时间变化的、集成的。

\\n

多维数据模型

\\n

星型模型、雪花模型和事实星座模型。

\\n

第四章 关联规则

\\n

什么是频繁项集,如何从项集中获取关联规则

\\n

数据中支持度大于最小支持度的的项集为频繁项集。

\\n

Apriori算法(重点必考)

\\n

题型是从给定事务集合中计算关联规则。

\\n

第五章 分类

\\n
    \\n
  1. 监督学习和非监督学习
  2. \\n
  3. 生成模型和判别模型。
  4. \\n
  5. 分类和回归的异同。都是监督学习。一个是离散,一个是连续。
  6. \\n
\\n

决策树(重点)

\\n

避免过拟合:增加数据量,降低模型复杂度。\\n决策树通过剪枝来避免过拟合。

\\n

KNN(重点)

\\n

属于懒惰学习,没有训练过程。
\\n缺点:对K敏感
\\n优点:无需训练

\\n

其他算法

\\n

朴素贝叶斯、SVM、ANN和BP网络。

\\n

评价指标

\\n

准确率,召回率,敏感度,精度,F1

\\n

第六章 聚类

\\n

什么是聚类

\\n

聚类的分类及相应算法

\\n
    \\n
  1. 基于划分的算法。k-means,k中心
  2. \\n
  3. 基于密度。DB scan
  4. \\n
  5. 基于层次。层次聚类
  6. \\n
  7. 基于网格。
  8. \\n
\\n

k-means(重点)

\\n

k-means的流程。

\\n

第七章 异常检测

\\n

什么是异常

\\n

异常的类型

\\n

全局、局部、集体、情景

\\n

LOF

\\n"},"11":{"id":11,"date":"2018/01/02","author":"Yidadaa","title":"2017年过去了,我很怀念它","mdContent":"> 你温柔如故,三月的柳絮纷飞,那副光景像一把红糖入水,初春的雪终于化了。\\r\\n\\r\\n### 前言\\r\\n\\r\\n今年注定是纪念意义非凡的一年:90一代的年轻人全部成年;属于很多人青春记忆的苍老师也结婚了;而对于我们来说,再过不到六个月,就要各奔东西,继续各自的人生。对于很多人,今年才是真正意义上的成年。\\r\\n\\r\\n不想以后的人生再浑浑噩噩下去,下定决心从今年开始有计划地生活起来,于是就有了这篇年终总结,而且以后每年都会做这样一个总结。\\r\\n\\r\\n### 摘要\\r\\n\\r\\n回望2017,许多事情如浮云飘过。\\r\\n\\r\\n年初的一次实习令我感触良多,不仅磨练了自己的技术,也促使我思考了很多形而上的问题。实习过后,我开始通过各种渠道接项目赚钱,其实后半年的主旋律就是接活-赚钱-接活-赚钱……\\r\\n\\r\\n那些形而上的问题就包括了未来的职业规划,我毕业后从事前端肯定是不行的,很快就会遇到天花板,工作内容也容易流于表面,所以个人倾向于从事高门槛的工作,比如算法工程师,但自己目前的算法能力还不够,于是在五月份决定留校读研。\\r\\n\\r\\n至于感情方面,没有进展,但已然列入2018的年度计划里,毕竟[孤独的人是可耻的](http://music.163.com/m/song?id=5279718)。\\r\\n\\r\\n此外就是健康和学业,这两个统统一蹶不振,后文会有详述。\\r\\n\\r\\n总而言之,本文总共包括五个部分,分别从财务、工程、学术、品格、感情、健康等方面对过去一年进行总结,并对未来一年做出期望。\\r\\n\\r\\n### 财务\\r\\n\\r\\n对于大多数大学生来说,所有烦恼的根源莫过于没钱。所幸自己有一些编程手艺,得以自己养活自己,还有余力支撑自己偶尔的挥霍。\\r\\n\\r\\n![2017](https://user-images.githubusercontent.com/16968934/34976947-bb6dd468-fad3-11e7-8d84-ff264097554a.JPG)\\r\\n\\r\\n从4月份开始,我养成了记账的习惯,刚开始的时候会很不适应,但是渐渐的就形成了条件反射,一开始觉得每一笔支出都记下来会特别繁琐,后来经过摸索,渐渐地形成了一些记账规则:\\r\\n\\r\\n1. 饭卡消费详情不记,只记充值金额。饭卡基本只能用来支付在食堂、浴室、洗衣和校内交通的消费,其中的大头是食堂花费,于是饭卡归到餐饮类中,每次只需要在充饭卡的时候记账即可;\\r\\n2. 朋友间借入借出不记。因为朋友间借账并没有产生实际的消费,记下来只会徒增流水,不利于年终分析,此类资金转换可以单开一个账本记录;\\r\\n3. 诸如日常使用支付宝或者微信支出的零食、饮料等消费,一般都会即刻记录下来,长久以来,可成习惯。\\r\\n\\r\\n回到2017全年消费状况。第四个月的入账金额,基本是实习期间发放的工资。其实实习工资给的很少,百度只给了150元/天的工资,然后每月按出勤天数发放工资,也就是说,每月实际入账不到3000,刚开始实习的三个月会有住房补助,金额是1650/月。当时我租的房子是2300/月,抛去房租和日常花费,工资所剩无几,所以说基本是存不下钱的,年初租房时还要靠父亲打钱资助。\\r\\n\\r\\n从五月份开始,我通过码市外包接活产生收入。五月份到七月份之间,替一个很豪爽的金主出钱开发浏览器,这段时间入账特别多,但好景不长,之后的金主都比较抠门,而且有一次与金主发生争执,项目不了了之,这一点其实是我自己的问题,在技术选型时固执己见,没有结合实际需求,导致项目流产。不过还好金主比较仁义,付了一部分钱。\\r\\n\\r\\n九月份开学之后,生活比较窘迫。有一个月没有接到什么活,很穷,不得已找老爸求助,老爸资助了3000块生活费,得以度过难关。\\r\\n\\r\\n10月份发生了两件事,一件是参加了小白健康这个众包组织,参与小程序开发,收入有限但相对来说比较稳定;另一件是接了一个算法项目,项目经历不可谓不坎坷,这件事放到下一章节详述,此项目分文未进,而且消耗了大量时间精力。\\r\\n\\r\\n11月份卖掉了性能孱弱的小米笔记本12.5寸低配版,购入华为Matebook X,所以这个月流水显得比较多。12月份和室友接了另外一个算法项目,产生了一些收入。\\r\\n\\r\\n2017各月收入大抵如上,再来看一看收支详情——我的钱从哪儿来,又到哪去了。\\r\\n\\r\\n![2017](https://user-images.githubusercontent.com/16968934/34977806-f2fc500a-fad6-11e7-895d-e44902703749.JPG)\\r\\n\\r\\n通过收支类目汇总可以看到,在自己“为什么这么穷”这个问题上,数码产品难辞其咎。这一年买了不少电子产品,更换了两台笔记本、一部手机,还买了手持稳定器和运动相机等各种小玩意儿。这些小玩意儿买来后不久就放在储物箱里吃灰了,实在是一大浪费。\\r\\n\\r\\n收入方面,来源比较单一,外包加工资占了总收入的70%,另外一些来自学校和家里的补助。\\r\\n\\r\\n**对于2018的展望**:\\r\\n1. 攒够自己和妹妹毕业旅行的钱;\\r\\n2. 保证自给自足。\\r\\n\\r\\n### 工程\\r\\n\\r\\n![3](https://user-images.githubusercontent.com/16968934/35086214-c374f6b6-fc66-11e7-8471-f53498671365.JPG)\\r\\n\\r\\n编程这项技能,如果不去用它,就会像刀刃一样生锈。目前的技术栈依然以前端为主,但慢慢的往算法方向转,要说有什么清晰的目标,就是争取能实现一个比较靠谱的工程项目。\\r\\n\\r\\n教务系统的插件是从大二下学期就开始做了,但是去年一整年进展都很慢,从 Github 的贡献频度图就可以看出,前三个月基本没有再动过代码,因为实习时很忙,工作上的内容总是掏空我的精力。返校后终于有时间写自己的代码了,但项目基本都托管在码市上,所以这部分的工作量也无法统计到底有多少。\\r\\n\\r\\n总体来说,虽然从数据上看,每周都能有代码产出,但是对比 Github 上其他热爱编程的用户,自己还有很大差距,个人理想的贡献频度是每周至少有5天产出,全年频度向1000进发。\\r\\n\\r\\n**对于2018的展望**:\\r\\n1. Coding 平均频度不低于 2/ 天,每周 Coding 记录不少于5天;\\r\\n2. 全年频度向1000进发;\\r\\n3. 4月份之前将教务系统插件完善放出,组织新生力量投入开发;\\r\\n4. lintCode 题库两遍以上。\\r\\n\\r\\n### 学术\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/35086717-970a4138-fc68-11e7-962f-0e72ee85faf0.png)\\r\\n\\r\\n说来惭愧,我在大学生里面,尤其是同届生里,算是不学无术的典范。\\r\\n\\r\\n先说成绩吧,大学四年一路“高歌猛降”,尤其是数学相关科目,基本都在60分线上徘徊,这对以后的转型是十分致命的。**如果想在研究生三年做出学术成果,一定要注意在数学这门课上花些功夫补救。**\\r\\n\\r\\n计算机基础课学得还算扎实,虽说成绩没有那么亮眼,但是很多概念都在写代码的时候重新实践了,也有了不一样的体会,自认为要比不少同龄人学得好些。\\r\\n\\r\\n未来一年的重点是科研能力,自己在出去实习之前基本没打算保研,出去实习之后,没有大学这个保护膜罩着,生活的锐利让我几近崩溃,最后不得不屁滚尿流地回了学校。同时也在大四上学期的关键时候决定保研,但是自己又对学校老师一无所知,最后投身于BMC门下,跟了一个专做工程的教授。但就个人而言,还是希望能在研究生水个几篇Paper。\\r\\n\\r\\n另外一个就是软技能的培养,做科研需要阅读大量外文文献,同时产出的也是英文文章,所以自己的英文水平也需要进行锻炼。大四上重修的《科技写作》让我认识到自己的写作已经退回到了初中生水平,实在汗颜。\\r\\n\\r\\n**对于2018的展望**:\\r\\n1. 系统地锻炼科研能力;\\r\\n2. 目标是产出一篇较高水平的论文;\\r\\n3. 保证每月足量的英语锻炼,包括背单词、阅读英文文献等。\\r\\n\\r\\n### 品格\\r\\n\\r\\n![35087661-f3e9c588-fc6b-11e7-8373-974891be6d94](https://user-images.githubusercontent.com/16968934/35088907-3ad9e438-fc70-11e7-91b1-6925b74478c7.jpg)\\r\\n\\r\\n我的书桌前的墙上贴着这样两张纸,一张是“九型人格测试表”,一张是“目光短浅、自以为是”。\\r\\n\\r\\n前者是《心理健康》课上做的一个测试,且不论可信度如何,其对我来讲还是有一些参考意义的。最下方一行是我的得分,我在“爱独特”、“爱知识”和“爱和平”三项上得到了很高的分数,这很符合我过去的习性,我一直奉行“技术至上”,避免与别人正面打交道,专心发展自己的技术水平,以便以后在职场上有足够的竞争力,我想这也是多数理工科学生的通性。而“爱权力”、“爱成功”和“爱秩序”的低分,则表示着我并没有太强的进取心,也就是没有太大的“野心”。这一点是表现得十分明显:大四上选择保研目标时,我草草决定了保本校,“事后诸葛亮”地想一下,当然是去外校更好一点。不过“事在人为”嘛,几个好朋友都留校了,只要刻苦努力,一定可以取得不错的成果。\\r\\n\\r\\n后者是一直以来用于警戒自己的警句,我视其为自己最大的两个缺点,在这上面吃过苦头,今后一定要努力避免。\\r\\n\\r\\n此外,在接外包项目的过程中,我接触到了形形色色的人,有的成熟老练,有的则平庸无趣。接触他人得同时也认识到了自身的不足。古语有云:“以铜为鉴,可以正衣冠;以人为鉴,可以明得失。”目前为止,自己对自身的品行有了一个大概了解,最大的缺点就是固执,六月份和何老师谈话时也被点出来过,她原话说:“你和张靖义虽然看起来截然不同,但有个最大的共性就是都比较固执。”我深以为然。\\r\\n\\r\\n一直以来,我有意识地控制自己,在对待问题时多考虑别人的感受,免得被自己一时鲁莽误事。同时也很感谢身边的朋友,每个人身上都有着值得学习的闪光之处,新的一年,不仅要保持谦逊,更要善待亲友,并向他们学习长处。\\r\\n\\r\\n**2018的展望**:\\r\\n1. 明确缺点:“自以为是,目光短浅”;\\r\\n2. 明确目标:“保持谦逊,锐意进取”;\\r\\n3. 小小的鸡汤:和老铁们一起努力成为群星中最闪耀的!\\r\\n\\r\\n### 感情\\r\\n\\r\\n[待完善]\\r\\n\\r\\n### 健康\\r\\n\\r\\n活着并不是一件容易的事情,尤其是健康地活着。\\r\\n\\r\\n由于作息持续紊乱,我身体整体的状况都下降得厉害,尤其是腰背部,稍不注意就会饱受腰肌劳损的折磨。另外精力也下降得厉害,所幸平常保持了一定的运动量,隔三差五就会去跑跑步,健康水平算是维持住了。\\r\\n\\r\\n这一章原本是有数据支撑的,但是小米手环的数据导出特别麻烦,就没有做。简单地概括一下,从五月份起,我的作息时间基本维持在上午10点到凌晨一点之间,有时会在这两个时间点前后浮动。这样的作息极其不健康,我这大半年吃早餐的次数屈指可数,体重也始终在60左右徘徊,我个人是希望体重能够达到65kg的,这样稍微练一下就可以练出比较漂亮的肌肉,比现在瘦如竹竿要强很多。\\r\\n\\r\\n**2018的展望**:\\r\\n1. 坚持跑步的好习惯,带动室友一起跑;\\r\\n2. 逐渐修正作息至00:00入睡,9:00起床;\\r\\n3. 体重向65kg进发。\\r\\n\\r\\n### 展望2018\\r\\n\\r\\n我是一个非常庸俗的人,这一点从本文的编排顺序就可以看出——我把“财务”放到了第一位,在平常的时候,如果连续一个月都没有进账,我会十分不开心。这样的表现与本人对自己的期望大相径庭,我理想中的自己是一个出世的居士,可以为院子里的花开了而高兴,也可以为满庭的落叶而哭泣,不以物喜,不以己悲,为享受生活而活,为成为自己而活。可如今自己如此痴迷金钱,我深感自责,但又一想,如果没有金钱,我可能连种花的院子都没有,到时候只能对着街头的垃圾堆而哭泣,想到这里,心里又释然了起来。\\r\\n\\r\\n所以2018年最大的愿望,就是**挣更多的钱**。所谓要想出世,必先入世,这是我为这个庸俗的愿望提出的歪理。\\r\\n\\r\\n其他的愿望都分散在文章各处了,这些是写出来的愿望,还有些不想写出的愿望,比如缺失的那个章节,因为我实在没有想到要写些什么,而且就算不写自己也会时刻惦记着,反而是那些对自己有督促作用而自己又不怎么上心的才需要写出来,并且在这里我还是要重复一遍:多多coding,多多运动,多多学习。\\r\\n\\r\\n2018年没有什么大不了的,大胆地活着吧。","content":"
\\n

你温柔如故,三月的柳絮纷飞,那副光景像一把红糖入水,初春的雪终于化了。

\\n
\\n

前言

\\n

今年注定是纪念意义非凡的一年:90一代的年轻人全部成年;属于很多人青春记忆的苍老师也结婚了;而对于我们来说,再过不到六个月,就要各奔东西,继续各自的人生。对于很多人,今年才是真正意义上的成年。

\\n

不想以后的人生再浑浑噩噩下去,下定决心从今年开始有计划地生活起来,于是就有了这篇年终总结,而且以后每年都会做这样一个总结。

\\n

摘要

\\n

回望2017,许多事情如浮云飘过。

\\n

年初的一次实习令我感触良多,不仅磨练了自己的技术,也促使我思考了很多形而上的问题。实习过后,我开始通过各种渠道接项目赚钱,其实后半年的主旋律就是接活-赚钱-接活-赚钱……

\\n

那些形而上的问题就包括了未来的职业规划,我毕业后从事前端肯定是不行的,很快就会遇到天花板,工作内容也容易流于表面,所以个人倾向于从事高门槛的工作,比如算法工程师,但自己目前的算法能力还不够,于是在五月份决定留校读研。

\\n

至于感情方面,没有进展,但已然列入2018的年度计划里,毕竟孤独的人是可耻的

\\n

此外就是健康和学业,这两个统统一蹶不振,后文会有详述。

\\n

总而言之,本文总共包括五个部分,分别从财务、工程、学术、品格、感情、健康等方面对过去一年进行总结,并对未来一年做出期望。

\\n

财务

\\n

对于大多数大学生来说,所有烦恼的根源莫过于没钱。所幸自己有一些编程手艺,得以自己养活自己,还有余力支撑自己偶尔的挥霍。

\\n

\\"2017\\"

\\n

从4月份开始,我养成了记账的习惯,刚开始的时候会很不适应,但是渐渐的就形成了条件反射,一开始觉得每一笔支出都记下来会特别繁琐,后来经过摸索,渐渐地形成了一些记账规则:

\\n
    \\n
  1. 饭卡消费详情不记,只记充值金额。饭卡基本只能用来支付在食堂、浴室、洗衣和校内交通的消费,其中的大头是食堂花费,于是饭卡归到餐饮类中,每次只需要在充饭卡的时候记账即可;
  2. \\n
  3. 朋友间借入借出不记。因为朋友间借账并没有产生实际的消费,记下来只会徒增流水,不利于年终分析,此类资金转换可以单开一个账本记录;
  4. \\n
  5. 诸如日常使用支付宝或者微信支出的零食、饮料等消费,一般都会即刻记录下来,长久以来,可成习惯。
  6. \\n
\\n

回到2017全年消费状况。第四个月的入账金额,基本是实习期间发放的工资。其实实习工资给的很少,百度只给了150元/天的工资,然后每月按出勤天数发放工资,也就是说,每月实际入账不到3000,刚开始实习的三个月会有住房补助,金额是1650/月。当时我租的房子是2300/月,抛去房租和日常花费,工资所剩无几,所以说基本是存不下钱的,年初租房时还要靠父亲打钱资助。

\\n

从五月份开始,我通过码市外包接活产生收入。五月份到七月份之间,替一个很豪爽的金主出钱开发浏览器,这段时间入账特别多,但好景不长,之后的金主都比较抠门,而且有一次与金主发生争执,项目不了了之,这一点其实是我自己的问题,在技术选型时固执己见,没有结合实际需求,导致项目流产。不过还好金主比较仁义,付了一部分钱。

\\n

九月份开学之后,生活比较窘迫。有一个月没有接到什么活,很穷,不得已找老爸求助,老爸资助了3000块生活费,得以度过难关。

\\n

10月份发生了两件事,一件是参加了小白健康这个众包组织,参与小程序开发,收入有限但相对来说比较稳定;另一件是接了一个算法项目,项目经历不可谓不坎坷,这件事放到下一章节详述,此项目分文未进,而且消耗了大量时间精力。

\\n

11月份卖掉了性能孱弱的小米笔记本12.5寸低配版,购入华为Matebook X,所以这个月流水显得比较多。12月份和室友接了另外一个算法项目,产生了一些收入。

\\n

2017各月收入大抵如上,再来看一看收支详情——我的钱从哪儿来,又到哪去了。

\\n

\\"2017\\"

\\n

通过收支类目汇总可以看到,在自己“为什么这么穷”这个问题上,数码产品难辞其咎。这一年买了不少电子产品,更换了两台笔记本、一部手机,还买了手持稳定器和运动相机等各种小玩意儿。这些小玩意儿买来后不久就放在储物箱里吃灰了,实在是一大浪费。

\\n

收入方面,来源比较单一,外包加工资占了总收入的70%,另外一些来自学校和家里的补助。

\\n

对于2018的展望

\\n
    \\n
  1. 攒够自己和妹妹毕业旅行的钱;
  2. \\n
  3. 保证自给自足。
  4. \\n
\\n

工程

\\n

\\"3\\"

\\n

编程这项技能,如果不去用它,就会像刀刃一样生锈。目前的技术栈依然以前端为主,但慢慢的往算法方向转,要说有什么清晰的目标,就是争取能实现一个比较靠谱的工程项目。

\\n

教务系统的插件是从大二下学期就开始做了,但是去年一整年进展都很慢,从 Github 的贡献频度图就可以看出,前三个月基本没有再动过代码,因为实习时很忙,工作上的内容总是掏空我的精力。返校后终于有时间写自己的代码了,但项目基本都托管在码市上,所以这部分的工作量也无法统计到底有多少。

\\n

总体来说,虽然从数据上看,每周都能有代码产出,但是对比 Github 上其他热爱编程的用户,自己还有很大差距,个人理想的贡献频度是每周至少有5天产出,全年频度向1000进发。

\\n

对于2018的展望

\\n
    \\n
  1. Coding 平均频度不低于 2/ 天,每周 Coding 记录不少于5天;
  2. \\n
  3. 全年频度向1000进发;
  4. \\n
  5. 4月份之前将教务系统插件完善放出,组织新生力量投入开发;
  6. \\n
  7. lintCode 题库两遍以上。
  8. \\n
\\n

学术

\\n

\\"image\\"

\\n

说来惭愧,我在大学生里面,尤其是同届生里,算是不学无术的典范。

\\n

先说成绩吧,大学四年一路“高歌猛降”,尤其是数学相关科目,基本都在60分线上徘徊,这对以后的转型是十分致命的。如果想在研究生三年做出学术成果,一定要注意在数学这门课上花些功夫补救。

\\n

计算机基础课学得还算扎实,虽说成绩没有那么亮眼,但是很多概念都在写代码的时候重新实践了,也有了不一样的体会,自认为要比不少同龄人学得好些。

\\n

未来一年的重点是科研能力,自己在出去实习之前基本没打算保研,出去实习之后,没有大学这个保护膜罩着,生活的锐利让我几近崩溃,最后不得不屁滚尿流地回了学校。同时也在大四上学期的关键时候决定保研,但是自己又对学校老师一无所知,最后投身于BMC门下,跟了一个专做工程的教授。但就个人而言,还是希望能在研究生水个几篇Paper。

\\n

另外一个就是软技能的培养,做科研需要阅读大量外文文献,同时产出的也是英文文章,所以自己的英文水平也需要进行锻炼。大四上重修的《科技写作》让我认识到自己的写作已经退回到了初中生水平,实在汗颜。

\\n

对于2018的展望

\\n
    \\n
  1. 系统地锻炼科研能力;
  2. \\n
  3. 目标是产出一篇较高水平的论文;
  4. \\n
  5. 保证每月足量的英语锻炼,包括背单词、阅读英文文献等。
  6. \\n
\\n

品格

\\n

\\"35087661-f3e9c588-fc6b-11e7-8373-974891be6d94\\"

\\n

我的书桌前的墙上贴着这样两张纸,一张是“九型人格测试表”,一张是“目光短浅、自以为是”。

\\n

前者是《心理健康》课上做的一个测试,且不论可信度如何,其对我来讲还是有一些参考意义的。最下方一行是我的得分,我在“爱独特”、“爱知识”和“爱和平”三项上得到了很高的分数,这很符合我过去的习性,我一直奉行“技术至上”,避免与别人正面打交道,专心发展自己的技术水平,以便以后在职场上有足够的竞争力,我想这也是多数理工科学生的通性。而“爱权力”、“爱成功”和“爱秩序”的低分,则表示着我并没有太强的进取心,也就是没有太大的“野心”。这一点是表现得十分明显:大四上选择保研目标时,我草草决定了保本校,“事后诸葛亮”地想一下,当然是去外校更好一点。不过“事在人为”嘛,几个好朋友都留校了,只要刻苦努力,一定可以取得不错的成果。

\\n

后者是一直以来用于警戒自己的警句,我视其为自己最大的两个缺点,在这上面吃过苦头,今后一定要努力避免。

\\n

此外,在接外包项目的过程中,我接触到了形形色色的人,有的成熟老练,有的则平庸无趣。接触他人得同时也认识到了自身的不足。古语有云:“以铜为鉴,可以正衣冠;以人为鉴,可以明得失。”目前为止,自己对自身的品行有了一个大概了解,最大的缺点就是固执,六月份和何老师谈话时也被点出来过,她原话说:“你和张靖义虽然看起来截然不同,但有个最大的共性就是都比较固执。”我深以为然。

\\n

一直以来,我有意识地控制自己,在对待问题时多考虑别人的感受,免得被自己一时鲁莽误事。同时也很感谢身边的朋友,每个人身上都有着值得学习的闪光之处,新的一年,不仅要保持谦逊,更要善待亲友,并向他们学习长处。

\\n

2018的展望

\\n
    \\n
  1. 明确缺点:“自以为是,目光短浅”;
  2. \\n
  3. 明确目标:“保持谦逊,锐意进取”;
  4. \\n
  5. 小小的鸡汤:和老铁们一起努力成为群星中最闪耀的!
  6. \\n
\\n

感情

\\n

[待完善]

\\n

健康

\\n

活着并不是一件容易的事情,尤其是健康地活着。

\\n

由于作息持续紊乱,我身体整体的状况都下降得厉害,尤其是腰背部,稍不注意就会饱受腰肌劳损的折磨。另外精力也下降得厉害,所幸平常保持了一定的运动量,隔三差五就会去跑跑步,健康水平算是维持住了。

\\n

这一章原本是有数据支撑的,但是小米手环的数据导出特别麻烦,就没有做。简单地概括一下,从五月份起,我的作息时间基本维持在上午10点到凌晨一点之间,有时会在这两个时间点前后浮动。这样的作息极其不健康,我这大半年吃早餐的次数屈指可数,体重也始终在60左右徘徊,我个人是希望体重能够达到65kg的,这样稍微练一下就可以练出比较漂亮的肌肉,比现在瘦如竹竿要强很多。

\\n

2018的展望

\\n
    \\n
  1. 坚持跑步的好习惯,带动室友一起跑;
  2. \\n
  3. 逐渐修正作息至00:00入睡,9:00起床;
  4. \\n
  5. 体重向65kg进发。
  6. \\n
\\n

展望2018

\\n

我是一个非常庸俗的人,这一点从本文的编排顺序就可以看出——我把“财务”放到了第一位,在平常的时候,如果连续一个月都没有进账,我会十分不开心。这样的表现与本人对自己的期望大相径庭,我理想中的自己是一个出世的居士,可以为院子里的花开了而高兴,也可以为满庭的落叶而哭泣,不以物喜,不以己悲,为享受生活而活,为成为自己而活。可如今自己如此痴迷金钱,我深感自责,但又一想,如果没有金钱,我可能连种花的院子都没有,到时候只能对着街头的垃圾堆而哭泣,想到这里,心里又释然了起来。

\\n

所以2018年最大的愿望,就是挣更多的钱。所谓要想出世,必先入世,这是我为这个庸俗的愿望提出的歪理。

\\n

其他的愿望都分散在文章各处了,这些是写出来的愿望,还有些不想写出的愿望,比如缺失的那个章节,因为我实在没有想到要写些什么,而且就算不写自己也会时刻惦记着,反而是那些对自己有督促作用而自己又不怎么上心的才需要写出来,并且在这里我还是要重复一遍:多多coding,多多运动,多多学习。

\\n

2018年没有什么大不了的,大胆地活着吧。

\\n"},"12":{"id":12,"date":"2018/02/11","author":"Yidadaa","title":"设计师如何与程序员进行有效沟通?","mdContent":"### 摘要\\r\\n需求方如何清楚地表达自己的新需求,设计师和程序员如何清楚地理解需求,程序员如何高效地将需求落地实现,是实际团队开发工作中每天都要面对的问题。本文描述一个需求从提出到落地的建议流程,旨在提高团队的沟通效率,提高总体生产力。\\r\\n\\r\\n### 需求的生命周期\\r\\n实际开发都是以需求为中心,所以需求是绝对的主角。一个完整的开发流程,往往包括**提出需求**、**理解需求**、**细化需求**、**实现需求**这样几个大的阶段,本文假设一个小型团队存在需求方、设计师和程序员三个主要角色,下面将对各个阶段以及各个角色的分工进行详解。\\r\\n\\r\\n**1. 提出需求** \\r\\n> 参与者:需求方 \\r\\n> 涉及材料:需求文档、原型图 \\r\\n> 建议使用工具:[墨刀](http://www.modao.cc/)、Microsoft PowerPoint \\r\\n\\r\\n此阶段,需求方应尽可能详尽并不失条理地描述出自己的需求,并形成文档以便后续使用。本文建议需求方在需求文档列出功能要点,并辅以原型图加以解释。\\r\\n\\r\\n![1](https://user-images.githubusercontent.com/16968934/36070792-86883648-0f3d-11e8-92d0-cc2b262845a7.JPG)\\r\\n\\r\\n原型图的作用仅仅是为了说明功能,应当尽可能地使用最简单直白的方式展现。如果提出的需求与现有项目无关,那么可以使用线框或者手绘;如果是在现有项目基础上提出,那么尽可能使用现有项目中的视觉元素来构建原型图。\\r\\n\\r\\n当涉及到多页面跳转时,应使用相应标识如连接线、跳转标注符号等来展示页面间的跳转关系。\\r\\n\\r\\n本文建议使用[墨刀](http://www.modao.cc/)或者PPT来绘制原型图。\\r\\n\\r\\n![default](https://user-images.githubusercontent.com/16968934/36070970-bb240770-0f41-11e8-8b6b-d856b695b504.jpg)\\r\\n\\r\\n**2. 理解需求** \\r\\n> 参与者:需求方、设计师、程序员 \\r\\n> 涉及材料:需求文档、原型图 \\r\\n> 建议使用工具:纸或者白板 \\r\\n\\r\\n此阶段,需求方组织设计师和程序员参与小型会议,需求方根据阶段一中的文档和原型图对需求进行详解,此时设计师和程序员应尽可能多地提出疑问并及时调整需求。\\r\\n\\r\\n当各方意见一致以后,进入需求细化阶段,设计师准备提供高保真设计图。\\r\\n\\r\\n**3. 细化需求** \\r\\n> 参与者:需求方、设计师、程序员 \\r\\n> 涉及材料:高保真设计稿 \\r\\n> 建议使用工具:Photoshop、Adobe Illustrator、Sketch \\r\\n\\r\\n此阶段,设计师着手制作高保真图像,高保真图像是最接近成品界面的设计稿,在绘制高保真图像过程中,设计师应及时就设计效果询问程序员是否有实现难度,比如某些动画效果可能会带来性能上的问题、某种阴影或者模糊效果无法用代码实现等等,这些风险应该在设计稿完成之前尽可能地消除掉,以免后续再次进行修改。\\r\\n\\r\\n高保真图像完成后,由需求方和程序员进行review,一定要保证各方无异议后再进入下一阶段。\\r\\n\\r\\n**4. 实现需求**\\r\\n> 参与者:设计师、程序员 \\r\\n> 涉及材料:标注图、DEMO\\r\\n> 建议使用工具:[PxCook像素大厨](http://www.fancynode.com.cn/pxcook)(Windows)、[Sketch Measure](http://sketch.im/plugins/1)(Mac) \\r\\n\\r\\n当高保真设计稿由各方review无异议后,应由设计师提供标注图给程序员,一份规范的标注图可以大幅提高程序员的编码效率,并且可以减少设计师与程序员的冗余沟通。\\r\\n\\r\\n一份规范的标注图应当清晰地展示出设计图中各个元素的**大小、位置、颜色、字号**等信息。\\r\\n\\r\\n![2](https://user-images.githubusercontent.com/16968934/36071511-42025d5c-0f4a-11e8-8399-8d83f90dbd21.JPG)\\r\\n\\r\\n程序员拿到标注图后,则立即投入开发。**为了提高效率,此阶段应尽量避免对标注图进行修改,即设计图中的所有的修改都尽量保证在此阶段之前完成。**\\r\\n\\r\\n### 附录\\r\\n- [在线原型工具墨刀快速上手指南](https://modao.cc/tutorials/%E5%BF%AB%E9%80%9F%E5%88%9B%E5%BB%BA%E5%BA%94%E7%94%A8)\\r\\n- [如何用科学的方法做出专业的原型图?](http://www.woshipm.com/rp/917962.html)\\r\\n- [移动端界面标注:如何理清标注的思路?](http://www.woshipm.com/pd/598672.html)\\r\\n- [UI设计师在跟程序员对接的时候,需要做到哪些能让沟通最高效的完成?](https://www.zhihu.com/question/30791244/answer/49523933)","content":"

摘要

\\n

需求方如何清楚地表达自己的新需求,设计师和程序员如何清楚地理解需求,程序员如何高效地将需求落地实现,是实际团队开发工作中每天都要面对的问题。本文描述一个需求从提出到落地的建议流程,旨在提高团队的沟通效率,提高总体生产力。

\\n

需求的生命周期

\\n

实际开发都是以需求为中心,所以需求是绝对的主角。一个完整的开发流程,往往包括提出需求理解需求细化需求实现需求这样几个大的阶段,本文假设一个小型团队存在需求方、设计师和程序员三个主要角色,下面将对各个阶段以及各个角色的分工进行详解。

\\n

1. 提出需求

\\n
\\n

参与者:需求方
\\n涉及材料:需求文档、原型图
\\n建议使用工具:墨刀、Microsoft PowerPoint

\\n
\\n

此阶段,需求方应尽可能详尽并不失条理地描述出自己的需求,并形成文档以便后续使用。本文建议需求方在需求文档列出功能要点,并辅以原型图加以解释。

\\n

\\"1\\"

\\n

原型图的作用仅仅是为了说明功能,应当尽可能地使用最简单直白的方式展现。如果提出的需求与现有项目无关,那么可以使用线框或者手绘;如果是在现有项目基础上提出,那么尽可能使用现有项目中的视觉元素来构建原型图。

\\n

当涉及到多页面跳转时,应使用相应标识如连接线、跳转标注符号等来展示页面间的跳转关系。

\\n

本文建议使用墨刀或者PPT来绘制原型图。

\\n

\\"default\\"

\\n

2. 理解需求

\\n
\\n

参与者:需求方、设计师、程序员
\\n涉及材料:需求文档、原型图
\\n建议使用工具:纸或者白板

\\n
\\n

此阶段,需求方组织设计师和程序员参与小型会议,需求方根据阶段一中的文档和原型图对需求进行详解,此时设计师和程序员应尽可能多地提出疑问并及时调整需求。

\\n

当各方意见一致以后,进入需求细化阶段,设计师准备提供高保真设计图。

\\n

3. 细化需求

\\n
\\n

参与者:需求方、设计师、程序员
\\n涉及材料:高保真设计稿
\\n建议使用工具:Photoshop、Adobe Illustrator、Sketch

\\n
\\n

此阶段,设计师着手制作高保真图像,高保真图像是最接近成品界面的设计稿,在绘制高保真图像过程中,设计师应及时就设计效果询问程序员是否有实现难度,比如某些动画效果可能会带来性能上的问题、某种阴影或者模糊效果无法用代码实现等等,这些风险应该在设计稿完成之前尽可能地消除掉,以免后续再次进行修改。

\\n

高保真图像完成后,由需求方和程序员进行review,一定要保证各方无异议后再进入下一阶段。

\\n

4. 实现需求

\\n
\\n

参与者:设计师、程序员
\\n涉及材料:标注图、DEMO\\n建议使用工具:PxCook像素大厨(Windows)、Sketch Measure(Mac)

\\n
\\n

当高保真设计稿由各方review无异议后,应由设计师提供标注图给程序员,一份规范的标注图可以大幅提高程序员的编码效率,并且可以减少设计师与程序员的冗余沟通。

\\n

一份规范的标注图应当清晰地展示出设计图中各个元素的大小、位置、颜色、字号等信息。

\\n

\\"2\\"

\\n

程序员拿到标注图后,则立即投入开发。为了提高效率,此阶段应尽量避免对标注图进行修改,即设计图中的所有的修改都尽量保证在此阶段之前完成。

\\n

附录

\\n\\n"},"13":{"id":13,"date":"2018/03/08","author":"Yidadaa","title":"品酒日记[长期更新]","mdContent":"> 本文用于记录笔者品尝的各种酒类的历程,纯娱乐。\\r\\n\\r\\n### 杜苏·阿斯蒂甜白低醇高泡葡萄酒\\r\\n**品种**:白葡萄酒 \\r\\n**口味** :甜型、果香 \\r\\n**产地**:意大利 \\r\\n**购买途径**:京东直采 \\r\\n**价格**:¥150 \\r\\n**品酒记录**:诚如酒名所言,入杯时泡沫四起,状如雪碧,片刻后泡沫消去,酒体为无色透明,有少许气泡着壁(其实这个酒真的挺像雪碧的)。初入口时有充气汽水的刺激感,同时伴有类似于原浆啤酒的香味,咽下时同样有轻微刺激感,其实到目前为止与RIO差不太多,但是咽下后口腔残留香味很浓,有点像蜜饯的味道,甜得有点腻那种感觉。总体来说,比较适合作为饮料来喝。 \\r\\n**记录日期**:2018-03-08\\r\\n\\r\\n----------\\r\\n\\r\\n### 桃乐丝特选公牛血干红葡萄酒 \\r\\n**品种**:红葡萄酒 \\r\\n**口味**:干型 \\r\\n**产地**:西班牙 \\r\\n**购买途径**:京东直采 \\r\\n**价格**:¥189 \\r\\n**品酒记录**:入杯的观感的确如血一样红,在灯光照耀下犹如宝石般闪耀,陶菊说能闻到香味,但是我只能闻到干红特有的刺鼻气味。第一口并无太多感觉,有些干,但并不涩,这一点要比涩如木屑的几十块的张裕解百纳好很多,入肚后一股温热涌上来,峙龙评价说像白酒,确实如此。西方人酿红酒时努力将糖分剔除,因为红酒是作为佐餐佳品,糖分过多会掩盖肉类的味道,很可惜中国人并没有这么讲究,喝酒只是为了一解忧愁,为了容易入口,才有下酒菜这一说,相对西方来说,中国的酒是主角,菜是配角,而西方人为了佐菜而喝酒,这点的确有些不同,这也造就了中国人喝红酒总是喜欢掺雪碧的“奇观”,不过转念一想,西方人喝茶还掺糖加奶呢!所以啊,干红还是得配雪碧才得以下咽,当然,有肉就另说啦! \\r\\n**记录日期**:2018-03-24 \\r\\n\\r\\n----------\\r\\n\\r\\n### 年华半甜葡萄酒 \\r\\n**品种**:白葡萄酒 \\r\\n**口味**:半甜型 \\r\\n**产地**:德国 \\r\\n**购买途径**:永辉超市 \\r\\n**价格**:¥59 \\r\\n**品酒记录**:酒体金黄,比较清澈,果香要比桃红酒淡上不少,刚入口有少许苦味,片刻后苦味散去,圆润的果香开始充斥口腔。总体来说跟桃红葡萄酒喝起来差不多。 \\r\\n**记录日期**:2018-04-21 \\r\\n\\r\\n----------\\r\\n\\r\\n### 甄爱五星白兰地\\r\\n**品种**:葡萄蒸馏酒 \\r\\n**口味**:N/A \\r\\n**产地**:青岛 \\r\\n**购买途径**:永辉超市 \\r\\n**价格**:¥48 \\r\\n**品酒记录**:好久没有喝过酒了,最近失去了经济来源,手头属实紧张,趁助学金发下来,去永辉整了瓶国产白兰地,这是我第一次喝国外的高度酒,实不相瞒,国内的白酒和各种梅子酒以及清酒完全喝不来,酒精味令我难以下咽,然而这瓶国产白兰地度数虽然高达40度,口感却格外清爽。开瓶后可以闻到葡萄酒特有的醇类和有机酸混合而成的味道,入口后会首先品尝到单宁多酚的苦味,随后混合着未蒸馏完全的单糖的甜味,酒体流过喉咙,乙醇的辛辣随之涌进鼻腔,然而却不似中国白酒那样猛烈,这种恰到好处的温柔让人回想起高中时代前桌女生的巴掌和脸庞接触后那种微微涨红的感觉,然后酒体穿肠过肚,一股暖流升腾而起,寒夜里升腾而起的火焰,窜到夜空里> 本文用于记录笔者品尝的各种酒类的历程,纯娱乐。\\r\\n\\r\\n### 杜苏·阿斯蒂甜白低醇高泡葡萄酒\\r\\n**品种**:白葡萄酒 \\r\\n**口味** :甜型、果香 \\r\\n**产地**:意大利 \\r\\n**购买途径**:京东直采 \\r\\n**价格**:¥150 \\r\\n**品酒记录**:诚如酒名所言,入杯时泡沫四起,状如雪碧,片刻后泡沫消去,酒体为无色透明,有少许气泡着壁(其实这个酒真的挺像雪碧的)。初入口时有充气汽水的刺激感,同时伴有类似于原浆啤酒的香味,咽下时同样有轻微刺激感,其实到目前为止与RIO差不太多,但是咽下后口腔残留香味很浓,有点像蜜饯的味道,甜得有点腻那种感觉。总体来说,比较适合作为饮料来喝。 \\r\\n**记录日期**:2018-03-08\\r\\n\\r\\n----------\\r\\n\\r\\n### 桃乐丝特选公牛血干红葡萄酒 \\r\\n**品种**:红葡萄酒 \\r\\n**口味**:干型 \\r\\n**产地**:西班牙 \\r\\n**购买途径**:京东直采 \\r\\n**价格**:¥189 \\r\\n**品酒记录**:入杯的观感的确如血一样红,在灯光照耀下犹如宝石般闪耀,陶菊说能闻到香味,但是我只能闻到干红特有的刺鼻气味。第一口并无太多感觉,有些干,但并不涩,这一点要比涩如木屑的几十块的张裕解百纳好很多,入肚后一股温热涌上来,峙龙评价说像白酒,确实如此。西方人酿红酒时努力将糖分剔除,因为红酒是作为佐餐佳品,糖分过多会掩盖肉类的味道,很可惜中国人并没有这么讲究,喝酒只是为了一解忧愁,为了容易入口,才有下酒菜这一说,相对西方来说,中国的酒是主角,菜是配角,而西方人为了佐菜而喝酒,这点的确有些不同,这也造就了中国人喝红酒总是喜欢掺雪碧的“奇观”,不过转念一想,西方人喝茶还掺糖加奶呢!所以啊,干红还是得配雪碧才得以下咽,当然,有肉就另说啦! \\r\\n**记录日期**:2018-03-24 \\r\\n\\r\\n----------\\r\\n\\r\\n### 年华半甜葡萄酒 \\r\\n**品种**:白葡萄酒 \\r\\n**口味**:半甜型 \\r\\n**产地**:德国 \\r\\n**购买途径**:永辉超市 \\r\\n**价格**:¥59 \\r\\n**品酒记录**:酒体金黄,比较清澈,果香要比桃红酒淡上不少,刚入口有少许苦味,片刻后苦味散去,圆润的果香开始充斥口腔。总体来说跟桃红葡萄酒喝起来差不多。 \\r\\n**记录日期**:2018-04-21 \\r\\n\\r\\n----------\\r\\n\\r\\n### 甄爱五星白兰地\\r\\n**品种**:葡萄蒸馏酒 \\r\\n**口味**:N/A \\r\\n**产地**:青岛 \\r\\n**购买途径**:永辉超市 \\r\\n**价格**:¥48 \\r\\n**品酒记录**:好久没有喝过酒了,最近失去了经济来源,手头属实紧张,趁助学金发下来,去永辉整了瓶国产白兰地,这是我第一次喝国外的高度酒,实不相瞒,国内的白酒和各种梅子酒以及清酒完全喝不来,酒精味令我难以下咽,然而这瓶国产白兰地度数虽然高达40度,口感却格外清爽。开瓶后可以闻到葡萄酒特有的醇类和有机酸混合而成的味道,入口后会首先品尝到单宁多酚的苦味,随后混合着未蒸馏完全的单糖的甜味,酒体流过喉咙,乙醇的辛辣随之涌进鼻腔,然而却不似中国白酒那样猛烈,这种恰到好处的温柔让人回想起高中时代前桌女生的巴掌和脸庞接触后那种微微涨红的感觉,然后酒体穿肠过肚,一股暖流涌来,寒夜里升腾而起的火焰,窜到夜空里攸尔消失不见。 \\r\\n**饮酒提示**: 适合独饮,若用于朋友聚会,可与果味饮料一同调制饮用,目前试过的最佳选择有水溶C100、果粒橙、柠檬味雪碧。\\r\\n**记录日期**:2019-10-11 ","content":"
\\n

本文用于记录笔者品尝的各种酒类的历程,纯娱乐。

\\n
\\n

杜苏·阿斯蒂甜白低醇高泡葡萄酒

\\n

品种:白葡萄酒
\\n口味 :甜型、果香
\\n产地:意大利
\\n购买途径:京东直采
\\n价格:¥150
\\n品酒记录:诚如酒名所言,入杯时泡沫四起,状如雪碧,片刻后泡沫消去,酒体为无色透明,有少许气泡着壁(其实这个酒真的挺像雪碧的)。初入口时有充气汽水的刺激感,同时伴有类似于原浆啤酒的香味,咽下时同样有轻微刺激感,其实到目前为止与RIO差不太多,但是咽下后口腔残留香味很浓,有点像蜜饯的味道,甜得有点腻那种感觉。总体来说,比较适合作为饮料来喝。
\\n记录日期:2018-03-08

\\n
\\n

桃乐丝特选公牛血干红葡萄酒

\\n

品种:红葡萄酒
\\n口味:干型
\\n产地:西班牙
\\n购买途径:京东直采
\\n价格:¥189
\\n品酒记录:入杯的观感的确如血一样红,在灯光照耀下犹如宝石般闪耀,陶菊说能闻到香味,但是我只能闻到干红特有的刺鼻气味。第一口并无太多感觉,有些干,但并不涩,这一点要比涩如木屑的几十块的张裕解百纳好很多,入肚后一股温热涌上来,峙龙评价说像白酒,确实如此。西方人酿红酒时努力将糖分剔除,因为红酒是作为佐餐佳品,糖分过多会掩盖肉类的味道,很可惜中国人并没有这么讲究,喝酒只是为了一解忧愁,为了容易入口,才有下酒菜这一说,相对西方来说,中国的酒是主角,菜是配角,而西方人为了佐菜而喝酒,这点的确有些不同,这也造就了中国人喝红酒总是喜欢掺雪碧的“奇观”,不过转念一想,西方人喝茶还掺糖加奶呢!所以啊,干红还是得配雪碧才得以下咽,当然,有肉就另说啦!
\\n记录日期:2018-03-24

\\n
\\n

年华半甜葡萄酒

\\n

品种:白葡萄酒
\\n口味:半甜型
\\n产地:德国
\\n购买途径:永辉超市
\\n价格:¥59
\\n品酒记录:酒体金黄,比较清澈,果香要比桃红酒淡上不少,刚入口有少许苦味,片刻后苦味散去,圆润的果香开始充斥口腔。总体来说跟桃红葡萄酒喝起来差不多。
\\n记录日期:2018-04-21

\\n
\\n

甄爱五星白兰地

\\n

品种:葡萄蒸馏酒
\\n口味:N/A
\\n产地:青岛
\\n购买途径:永辉超市
\\n价格:¥48
\\n品酒记录:好久没有喝过酒了,最近失去了经济来源,手头属实紧张,趁助学金发下来,去永辉整了瓶国产白兰地,这是我第一次喝国外的高度酒,实不相瞒,国内的白酒和各种梅子酒以及清酒完全喝不来,酒精味令我难以下咽,然而这瓶国产白兰地度数虽然高达40度,口感却格外清爽。开瓶后可以闻到葡萄酒特有的醇类和有机酸混合而成的味道,入口后会首先品尝到单宁多酚的苦味,随后混合着未蒸馏完全的单糖的甜味,酒体流过喉咙,乙醇的辛辣随之涌进鼻腔,然而却不似中国白酒那样猛烈,这种恰到好处的温柔让人回想起高中时代前桌女生的巴掌和脸庞接触后那种微微涨红的感觉,然后酒体穿肠过肚,一股暖流升腾而起,寒夜里升腾而起的火焰,窜到夜空里> 本文用于记录笔者品尝的各种酒类的历程,纯娱乐。

\\n

杜苏·阿斯蒂甜白低醇高泡葡萄酒

\\n

品种:白葡萄酒
\\n口味 :甜型、果香
\\n产地:意大利
\\n购买途径:京东直采
\\n价格:¥150
\\n品酒记录:诚如酒名所言,入杯时泡沫四起,状如雪碧,片刻后泡沫消去,酒体为无色透明,有少许气泡着壁(其实这个酒真的挺像雪碧的)。初入口时有充气汽水的刺激感,同时伴有类似于原浆啤酒的香味,咽下时同样有轻微刺激感,其实到目前为止与RIO差不太多,但是咽下后口腔残留香味很浓,有点像蜜饯的味道,甜得有点腻那种感觉。总体来说,比较适合作为饮料来喝。
\\n记录日期:2018-03-08

\\n
\\n

桃乐丝特选公牛血干红葡萄酒

\\n

品种:红葡萄酒
\\n口味:干型
\\n产地:西班牙
\\n购买途径:京东直采
\\n价格:¥189
\\n品酒记录:入杯的观感的确如血一样红,在灯光照耀下犹如宝石般闪耀,陶菊说能闻到香味,但是我只能闻到干红特有的刺鼻气味。第一口并无太多感觉,有些干,但并不涩,这一点要比涩如木屑的几十块的张裕解百纳好很多,入肚后一股温热涌上来,峙龙评价说像白酒,确实如此。西方人酿红酒时努力将糖分剔除,因为红酒是作为佐餐佳品,糖分过多会掩盖肉类的味道,很可惜中国人并没有这么讲究,喝酒只是为了一解忧愁,为了容易入口,才有下酒菜这一说,相对西方来说,中国的酒是主角,菜是配角,而西方人为了佐菜而喝酒,这点的确有些不同,这也造就了中国人喝红酒总是喜欢掺雪碧的“奇观”,不过转念一想,西方人喝茶还掺糖加奶呢!所以啊,干红还是得配雪碧才得以下咽,当然,有肉就另说啦!
\\n记录日期:2018-03-24

\\n
\\n

年华半甜葡萄酒

\\n

品种:白葡萄酒
\\n口味:半甜型
\\n产地:德国
\\n购买途径:永辉超市
\\n价格:¥59
\\n品酒记录:酒体金黄,比较清澈,果香要比桃红酒淡上不少,刚入口有少许苦味,片刻后苦味散去,圆润的果香开始充斥口腔。总体来说跟桃红葡萄酒喝起来差不多。
\\n记录日期:2018-04-21

\\n
\\n

甄爱五星白兰地

\\n

品种:葡萄蒸馏酒
\\n口味:N/A
\\n产地:青岛
\\n购买途径:永辉超市
\\n价格:¥48
\\n品酒记录:好久没有喝过酒了,最近失去了经济来源,手头属实紧张,趁助学金发下来,去永辉整了瓶国产白兰地,这是我第一次喝国外的高度酒,实不相瞒,国内的白酒和各种梅子酒以及清酒完全喝不来,酒精味令我难以下咽,然而这瓶国产白兰地度数虽然高达40度,口感却格外清爽。开瓶后可以闻到葡萄酒特有的醇类和有机酸混合而成的味道,入口后会首先品尝到单宁多酚的苦味,随后混合着未蒸馏完全的单糖的甜味,酒体流过喉咙,乙醇的辛辣随之涌进鼻腔,然而却不似中国白酒那样猛烈,这种恰到好处的温柔让人回想起高中时代前桌女生的巴掌和脸庞接触后那种微微涨红的感觉,然后酒体穿肠过肚,一股暖流涌来,寒夜里升腾而起的火焰,窜到夜空里攸尔消失不见。
\\n饮酒提示: 适合独饮,若用于朋友聚会,可与果味饮料一同调制饮用,目前试过的最佳选择有水溶C100、果粒橙、柠檬味雪碧。\\n记录日期:2019-10-11

\\n"},"14":{"id":14,"date":"2018/05/11","author":"Yidadaa","title":"另一个我","mdContent":"> 我知道你的痛苦,所以请你尊重我选择快乐的权力。\\r\\n\\r\\n每隔一段时间,我就感觉自己的脑中好像被什么东西堵住了一样,觉得自己活得很不透彻。\\r\\n\\r\\n具体点说,就是注意力与敏感度全面下降,时常陷入呆滞,时常情绪低落,时常喜怒无常,对任何事情都提不起兴趣,遑论正常学习与工作。尽管这些描述与“抑郁症”患者的表现如此相似,我也不同意给自己贴上“抑郁症”的标签,一是由于现在这个标签基本被广泛滥用,二是这样会给自己造成心理暗示,加重负面情绪。\\r\\n\\r\\n我将其称为另一个我,我的对立面:他。\\r\\n\\r\\n他沉静、偏执,并多愁善感。他日常表现高冷,一切常人津津乐道之事,统统勾不起他的半点兴趣,这令他十分格格不入。他脾气古怪,在任何时候都可能会陷入呆滞,对外界刺激响应度降低,同时社交欲望大幅下降。他不喜欢做任何有意义的事情,他甚至否定意义本身,有时候他认为,生命是宇宙间最可笑的笑话,因为一切终归热寂,一切都将毫无意义,包括意义本身。所以,他还很中二。\\r\\n\\r\\n他的到来悄无声息,可能是在正午,也可能是在半夜。我不喜欢他,他在时我什么事也做不了,但我对他的到来束手无策。并且,他何时回来,会待到何时,我统统无从得知。\\r\\n\\r\\n所以这一次我采取的对策,是躺在床上什么都不做。这让我想起了实习的日子,这段日子之所以令我无法忘怀,是因为彼时他几乎时时刻刻和我形影不离,我躺在床上的时候,仿佛回到了那段日子里。我躺在床上,什么也不做,什么也做不了,没法写代码,没法做毕设,没法与喜欢的妹子聊天,没法与朋友开黑打游戏,真是糟糕透了。\\r\\n\\r\\n我想和他聊聊,他到底是谁,在想些什么,想让我做些什么,但这似乎不可能,我与他几乎无法交流。在某种程度上,我就是他,他就是我,当我是我时,他就不是我,当他是我时,我就不是我。我们无法同时存在,也无从谈起平等交流。\\r\\n\\r\\n因此,我趁我还是我时,写下这篇文章,作为一个媒介,希望当他来时可以看到,并知道我的真实想法,也让我知道他的真实想法。","content":"
\\n

我知道你的痛苦,所以请你尊重我选择快乐的权力。

\\n
\\n

每隔一段时间,我就感觉自己的脑中好像被什么东西堵住了一样,觉得自己活得很不透彻。

\\n

具体点说,就是注意力与敏感度全面下降,时常陷入呆滞,时常情绪低落,时常喜怒无常,对任何事情都提不起兴趣,遑论正常学习与工作。尽管这些描述与“抑郁症”患者的表现如此相似,我也不同意给自己贴上“抑郁症”的标签,一是由于现在这个标签基本被广泛滥用,二是这样会给自己造成心理暗示,加重负面情绪。

\\n

我将其称为另一个我,我的对立面:他。

\\n

他沉静、偏执,并多愁善感。他日常表现高冷,一切常人津津乐道之事,统统勾不起他的半点兴趣,这令他十分格格不入。他脾气古怪,在任何时候都可能会陷入呆滞,对外界刺激响应度降低,同时社交欲望大幅下降。他不喜欢做任何有意义的事情,他甚至否定意义本身,有时候他认为,生命是宇宙间最可笑的笑话,因为一切终归热寂,一切都将毫无意义,包括意义本身。所以,他还很中二。

\\n

他的到来悄无声息,可能是在正午,也可能是在半夜。我不喜欢他,他在时我什么事也做不了,但我对他的到来束手无策。并且,他何时回来,会待到何时,我统统无从得知。

\\n

所以这一次我采取的对策,是躺在床上什么都不做。这让我想起了实习的日子,这段日子之所以令我无法忘怀,是因为彼时他几乎时时刻刻和我形影不离,我躺在床上的时候,仿佛回到了那段日子里。我躺在床上,什么也不做,什么也做不了,没法写代码,没法做毕设,没法与喜欢的妹子聊天,没法与朋友开黑打游戏,真是糟糕透了。

\\n

我想和他聊聊,他到底是谁,在想些什么,想让我做些什么,但这似乎不可能,我与他几乎无法交流。在某种程度上,我就是他,他就是我,当我是我时,他就不是我,当他是我时,我就不是我。我们无法同时存在,也无从谈起平等交流。

\\n

因此,我趁我还是我时,写下这篇文章,作为一个媒介,希望当他来时可以看到,并知道我的真实想法,也让我知道他的真实想法。

\\n"},"15":{"id":15,"date":"2018/08/07","author":"Yidadaa","title":"某算法竞赛初试题解 - 3K问题","mdContent":"### 题目\\r\\n给定包含N个正整数的**非递减**数组A,假设该数组以以下形式保存了某个正整数K的值,即:\\r\\n$$K = \\\\sum_{i=0}^N 2^{A[i]}$$\\r\\n\\r\\n例如给定$A=[1,4,5]$,则$K=2^1+2^4+2^5=50$。\\r\\n\\r\\n则给出一个算法,计算出$3K$的二进制表示中为`1`的比特个数。\\r\\n例如$3K=150=10010110_2$,程序返回值为`4`。\\r\\n\\r\\n**要求**:时间复杂度控制在$O(N)$;空间复杂度控制在$O(1)$。\\r\\n\\r\\n### 题解\\r\\n对于二进制的题,一般都要从二进制本质出发,从数组A的定义可以得到:\\r\\n\\r\\n$$3K=3\\\\sum^N_{i=0}2^{A[i]}=(2^0+2^1)\\\\sum^N_{i=0}2^{A[i]}=\\\\sum^N_{i=0}2^{A[i]}+\\\\sum^N_{i=0}2^{A[i+1]}$$\\r\\n\\r\\n然后我们对数组中的每一位都加上1,然后合并回到数组A中,得到新的数组B,那么问题就转化为计算数组B代表的K的二进制中比特`1`的个数,为了表述方便,我们下文依然用数组A来举例表述。\\r\\n\\r\\n让我们再次回到二进制的基本概念上来,对于一个二进制数:\\r\\n$$110_2=0\\\\times 2^0 + 1\\\\times 2^1 + 1\\\\times 2^2$$\\r\\n\\r\\n那么数组A所代表的数字就变成了:\\r\\n$$K=2^1+2^4+2^5=1\\\\times2^1+0\\\\times2^2+0\\\\times2^3+1\\\\times2^4+1\\\\times2^5=11001_2$$\\r\\n\\r\\n也就是说,数组A中的每一位数字`A[i]`,都代表了K的二进制中低`A[i]`位的比特位为`1`。\\r\\n\\r\\n于是问题就简化为,将数组B表示的每个比特位加起来,最后的结果中`1`的个数,就是我们想要的结果。\\r\\n\\r\\n代码实现如下:\\r\\n```python\\r\\ndef solution(A):\\r\\n bits = {} # 使用哈希表来存储每一个比特位,可以节省空间\\r\\n # 如果键值k存在于bits中,那么代表低k位为1\\r\\n for i in A:\\r\\n for j in range(i, i + 2):\\r\\n t = j\\r\\n while t in bits:\\r\\n del bits[t] // 逐比特执行加法\\r\\n t += 1\\r\\n bits[t] = 1\\r\\n # 最后输出bits中的键值个数即可\\r\\n return len(bits.keys())\\r\\n```\\r\\n\\r\\n### 题外话\\r\\n然而,这道题是我在比赛结束一个小时之后才解出来的,认清了自己是菜鸡的事实。","content":"

题目

\\n

给定包含N个正整数的非递减数组A,假设该数组以以下形式保存了某个正整数K的值,即:

\\n

K=i=0N2A[i]K = \\\\sum_{i=0}^N 2^{A[i]}\\nK=i=0N2A[i]

\\n

例如给定A=[1,4,5]A=[1,4,5]A=[1,4,5],则K=21+24+25=50K=2^1+2^4+2^5=50K=21+24+25=50

\\n

则给出一个算法,计算出3K3K3K的二进制表示中为1的比特个数。\\n例如3K=150=1001011023K=150=10010110_23K=150=100101102,程序返回值为4

\\n

要求:时间复杂度控制在O(N)O(N)O(N);空间复杂度控制在O(1)O(1)O(1)

\\n

题解

\\n

对于二进制的题,一般都要从二进制本质出发,从数组A的定义可以得到:

\\n

3K=3i=0N2A[i]=(20+21)i=0N2A[i]=i=0N2A[i]+i=0N2A[i+1]3K=3\\\\sum^N_{i=0}2^{A[i]}=(2^0+2^1)\\\\sum^N_{i=0}2^{A[i]}=\\\\sum^N_{i=0}2^{A[i]}+\\\\sum^N_{i=0}2^{A[i+1]}\\n3K=3i=0N2A[i]=(20+21)i=0N2A[i]=i=0N2A[i]+i=0N2A[i+1]

\\n

然后我们对数组中的每一位都加上1,然后合并回到数组A中,得到新的数组B,那么问题就转化为计算数组B代表的K的二进制中比特1的个数,为了表述方便,我们下文依然用数组A来举例表述。

\\n

让我们再次回到二进制的基本概念上来,对于一个二进制数:

\\n

1102=0×20+1×21+1×22110_2=0\\\\times 2^0 + 1\\\\times 2^1 + 1\\\\times 2^2\\n1102=0×20+1×21+1×22

\\n

那么数组A所代表的数字就变成了:

\\n

K=21+24+25=1×21+0×22+0×23+1×24+1×25=110012K=2^1+2^4+2^5=1\\\\times2^1+0\\\\times2^2+0\\\\times2^3+1\\\\times2^4+1\\\\times2^5=11001_2\\nK=21+24+25=1×21+0×22+0×23+1×24+1×25=110012

\\n

也就是说,数组A中的每一位数字A[i],都代表了K的二进制中低A[i]位的比特位为1

\\n

于是问题就简化为,将数组B表示的每个比特位加起来,最后的结果中1的个数,就是我们想要的结果。

\\n

代码实现如下:

\\n
def solution(A):\\n    bits = {} # 使用哈希表来存储每一个比特位,可以节省空间\\n    # 如果键值k存在于bits中,那么代表低k位为1\\n    for i in A:\\n        for j in range(i, i + 2):\\n            t = j\\n            while t in bits:\\n                del bits[t] // 逐比特执行加法\\n                t += 1\\n            bits[t] = 1\\n    # 最后输出bits中的键值个数即可\\n    return len(bits.keys())\\n
\\n

题外话

\\n

然而,这道题是我在比赛结束一个小时之后才解出来的,认清了自己是菜鸡的事实。

\\n"},"16":{"id":16,"date":"2018/08/09","author":"Yidadaa","title":"LintCode 困难题赏 - 103.寻找带环链表入口","mdContent":"### 题目\\r\\n给定一个链表,如果链表中存在环,则返回到链表中环的起始节点,如果没有环,返回null。\\r\\n\\r\\n**要求**:不使用额外空间。 \\r\\n**例子**:带环链表`1->5->3->4->6->(index-2)`,返回值为`index-2`对应的节点,即值为`5`的那个节点。\\r\\n\\r\\n### 题解\\r\\n这道题可以说是链表题目中的经典题目了。解这道题之前,还有道题(LintCode No.102)是判断一个链表是否有环,使用了快慢指针的方法,具体思路是使用两根指针以不同的速度遍历链表,快指针一次走两个节点,慢指针一次走一个节点,如果两根指针中途相遇了,那么这个链表就是有环的。\\r\\n\\r\\n本题的思路依然是使用快慢指针的方法,但前提要先利用快慢指针的特性,找出快慢指针走过的路程、环入口、相遇点之间的数学关系。\\r\\n\\r\\n设起点到入口走了`x`步,入口到两根指针第一次相遇处走了`y`步,环的长度为`c`,则快慢指针第一次相遇时,快指针走的距离是慢指针的两倍,即`x+Nc+y=2(x+y)`(其中N为自然数),变换一下得到`x+y=Nc`,于是我们就可以得到入口的索引`x=Nc-y`,这个式子意味着,如果我们用另外两根指针去遍历该链表,其中一根从起点开始走,另外一根从之前快慢指针相遇的地方开始走,并且两根指针每次都只走一步,那么当第一根指针走了`x`步到达环入口时,恰好等于另一根指针从相遇点出发然后绕环N圈后到达环入口,即两根指针在环的入口处相遇。\\r\\n\\r\\n![default](https://user-images.githubusercontent.com/16968934/43889515-f8fdcf44-9bf6-11e8-83a0-53b50e8bb635.jpg)\\r\\n\\r\\n\\r\\n### 代码\\r\\n```python\\r\\n\\"\\"\\"\\r\\nDefinition of ListNode\\r\\nclass ListNode(object):\\r\\n\\r\\n def __init__(self, val, next=None):\\r\\n self.val = val\\r\\n self.next = next\\r\\n\\"\\"\\"\\r\\n\\r\\n\\r\\nclass Solution:\\r\\n \\"\\"\\"\\r\\n @param: head: The first node of linked list.\\r\\n @return: The node where the cycle begins. if there is no cycle, return null\\r\\n \\"\\"\\"\\r\\n def detectCycle(self, head):\\r\\n slow = head\\r\\n fast = head\\r\\n meet = None\\r\\n while fast and fast.next:\\r\\n slow = slow.next\\r\\n fast = fast.next.next\\r\\n if slow == fast:\\r\\n meet = fast\\r\\n break\\r\\n if not fast or not fast.next:\\r\\n return None\\r\\n slow = head\\r\\n while slow != meet:\\r\\n slow = slow.next\\r\\n meet = meet.next\\r\\n return slow\\r\\n```","content":"

题目

\\n

给定一个链表,如果链表中存在环,则返回到链表中环的起始节点,如果没有环,返回null。

\\n

要求:不使用额外空间。
\\n例子:带环链表1->5->3->4->6->(index-2),返回值为index-2对应的节点,即值为5的那个节点。

\\n

题解

\\n

这道题可以说是链表题目中的经典题目了。解这道题之前,还有道题(LintCode No.102)是判断一个链表是否有环,使用了快慢指针的方法,具体思路是使用两根指针以不同的速度遍历链表,快指针一次走两个节点,慢指针一次走一个节点,如果两根指针中途相遇了,那么这个链表就是有环的。

\\n

本题的思路依然是使用快慢指针的方法,但前提要先利用快慢指针的特性,找出快慢指针走过的路程、环入口、相遇点之间的数学关系。

\\n

设起点到入口走了x步,入口到两根指针第一次相遇处走了y步,环的长度为c,则快慢指针第一次相遇时,快指针走的距离是慢指针的两倍,即x+Nc+y=2(x+y)(其中N为自然数),变换一下得到x+y=Nc,于是我们就可以得到入口的索引x=Nc-y,这个式子意味着,如果我们用另外两根指针去遍历该链表,其中一根从起点开始走,另外一根从之前快慢指针相遇的地方开始走,并且两根指针每次都只走一步,那么当第一根指针走了x步到达环入口时,恰好等于另一根指针从相遇点出发然后绕环N圈后到达环入口,即两根指针在环的入口处相遇。

\\n

\\"default\\"

\\n

代码

\\n
"""\\nDefinition of ListNode\\nclass ListNode(object):\\n\\n    def __init__(self, val, next=None):\\n        self.val = val\\n        self.next = next\\n"""\\n\\n\\nclass Solution:\\n    """\\n    @param: head: The first node of linked list.\\n    @return: The node where the cycle begins. if there is no cycle, return null\\n    """\\n    def detectCycle(self, head):\\n        slow = head\\n        fast = head\\n        meet = None\\n        while fast and fast.next:\\n            slow = slow.next\\n            fast = fast.next.next\\n            if slow == fast:\\n                meet = fast\\n                break\\n        if not fast or not fast.next:\\n            return None\\n        slow = head\\n        while slow != meet:\\n            slow = slow.next\\n            meet = meet.next\\n        return slow\\n
\\n"},"17":{"id":17,"date":"2018/12/20","author":"Yidadaa","title":"2018,山与水的分界线","mdContent":"> **No Fear In My Heart(节选) - 朴树** \\r\\n你也曾经追问,然后沉默 \\r\\n渐渐习惯谎言,并以此为荣 \\r\\n因为没有草原,就忘了你是马 \\r\\n你卑微的人生,从不曾犯错的,无聊的人生 \\r\\n\\r\\n### 前言\\r\\n2018 年发生了很多注定写进历史书的大事,霍金、金庸、李咏这些耳熟能详的名人逝世的消息,无不昭示着某段旧时代的淡去。对于我而言,2018 年发生的大事莫过于毕业,本科最后的日子里与同学重温了青春最后的激情,毕业晚会上一曲合唱过后,生活泛起的最后一朵浪花消失在海面上,以后的日子仿佛便会一直沉浸在鱼入大海的逍遥自在和平淡无奇中,此时回味起那时的念头,觉得这预言怕是应验了。\\r\\n\\r\\n本文拖更了大半年,文章的标题在 18 年 12 月份就已经拟好,那时我踌躇满志,心里满怀期待,而时至今日动笔,这标题让我感慨万千。\\r\\n\\r\\n*注:本文全长 9861 字,读完预计耗时 10 分钟。*\\r\\n\\r\\n### 摘要\\r\\n简短地回顾 2018,前半年毕业在即,工作重心基本在毕业设计和毕业旅行的筹划上,全年更加深入地参与到小白健康的开发工作中去,主导了两个重要页面的协调和开发;而后毕业季来袭,匆匆赶完毕业答辩后,和大晗哥他们筹备了毕业晚会,虽然排面不大,但大家的热情都很高涨,很有热血的感觉。\\r\\n\\r\\n毕业晚会后,和党员、峙龙、陶菊以及狗哥去川西自驾,好好玩了一周,算是为本科时光划上了一个句号。下半年研究生生活开始,除了陶菊去华为工作之外,几个好友都重新回到课堂,拿起课本学习的时光无比充实,但焦虑也无处不在,大家都纷纷发了论文,而自己仍在工程和科研之间摇摆不定,这难题始终难以解决。\\r\\n\\r\\n2018 年至少是满怀希望的一年,我怀着对她的期望和热爱生活着,尽管没有取得什么璀璨的成绩,但总体上来说自己充满干劲,年中暑假我冒昧地邀请她去看五月天的演唱会,不出所料地遭到了拒绝,没有做好前期调研和预热的空想主义果然还是行不通啊,以后要好好吸取这个宝贵的教训才好。\\r\\n\\r\\n身体状况在暑假的时候迎来了最坏的时期,8 月份颈椎疼痛不已,甚至只能躺在床上写代码,幸好随后使用了各种手段,勉强让颈椎恢复了工作,一直到年末都没有遇到什么大问题,健康问题依然不能忽视。\\r\\n\\r\\n本文依然会延续去年的设定,从财务、编程、学习、品格、感情、健康这几个方面对 2018 做出总结,并对未来做出展望。\\r\\n\\r\\n\\r\\n### 财务\\r\\n\\r\\n我从去年的四月份开始,养成了记账的习惯,所以 2018 年全年的收支明细都一清二楚,去年的年终总结我很详细地列出了收支明细,现在觉得有隐私泄露的风险,而且说实话相对于已经工作的同学来说,这个数值也没有什么参考价值,所以今年就不给出详细数据了。\\r\\n\\r\\n先说收入方面,相对于 2017 年,2018 年的收入基本翻了一番,因为大四下学期时间比较充裕,我一直都在为毕业旅行攒钱,所以就花了很多时间和精力在小白健康的开发上,从记账软件中也可以看出全年有60%的收入都来自于小白健康的远程兼职,占比上与去年持平,但金额上翻了大概一倍,另外有17%的收入来自研究生的奖助学金,相当于把交的学费又给还回来了,这个其实没什么好说的,除此之外,也会零星地接一些小项目,剩下的收入基本都产生于此。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63689961-017ce280-c83e-11e9-92b1-a7711523d85b.png)\\r\\n\\r\\n然后说一下支出,当习惯了做外包恰烂钱之后,兜里有了点积蓄,但我又没有存钱的习惯,所以花钱一直大手大脚,2018 年有 35% 的钱都花在了数码设备上,比餐饮支出(23.8%)还要多,期间买了一台 matebook x 笔记本、一台索尼的 a6300 微单以及一个定焦镜头,手机从小米 6X 到红米 note5 再一路换到小米 8,也产生了不少花销,年底购入任天堂 switch 游戏机以及若干游戏,然后其他体脂秤、显示器拍立得之类的小玩意儿不一而足,买来基本属于吃灰状态,都算作是玩具。\\r\\n\\r\\n之后是电影话剧以及旅游等娱乐支出,年初的时候受陶菊邀请,和狗哥一起在云南自驾游了一番,玩得很爽;暑假时候五月天的演唱会门票花了些钱,然后毕业旅行以及暑假过渡期租了两个月的房子。在学校的时候在电影上花了很多钱,2018 年上映的 7 分以上的电影基本都去电影院贡献了票房。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63689498-b7dfc800-c83c-11e9-87ef-b9621ea455f2.png)\\r\\n\\r\\n现在想一想,用宝贵的大学时光去做外包挣小钱其实是一件得不偿失的事情,如果能把这些时间精力用来做比赛或者搞科研,其收益肯定要比做外包更高,而且出去实习工资要比自己接活更稳定,还能找工作时作为加分项。**如果有大学生在读这篇文章,我强烈建议你把时间投入到比赛 / 科研 / 学习 / 实习上去,学生时代挣的这点小钱跟学到的知识比实在是微不足道。**\\r\\n\\r\\n**2018年目标完成情况:**\\r\\n- [x] 攒够自己和妹妹毕业旅行的钱;\\r\\n- [x] 保证自给自足。\\r\\n\\r\\n其实第一项只完成了前半部分,妹妹第一年高考因成绩不理想而复读了,而且我也没能攒够两个人出去玩的钱,没有兑现承诺,内心有些愧疚。\\r\\n\\r\\n**对于2019的展望:**\\r\\n- [ ] 自给自足到出去实习。\\r\\n\\r\\n我认为没必要在学校里攒很多钱,如果能找到实习,一直到工作都不必担心钱的问题,所以 2019 最要紧的就是收缩自己的物欲,控制支出。\\r\\n\\r\\n\\r\\n### 编程\\r\\n![image](https://user-images.githubusercontent.com/16968934/63692006-4ce5bf80-c843-11e9-8d5c-a18034947773.png)\\r\\n\\r\\n我是 Github 的忠实用户,所有的代码都会托管到这个网站上,所以 Github 的频率图可以在很大程度上代表编程频率,从提交 Commit 的频率上来看,相比去年提升了 130%,与去年同期相比有了长足的进步,这是一个很好的迹象,证明我对编程的热爱有增无减,并且我确实认为这种热爱会一直保持下去。\\r\\n\\r\\n但值得一提的是,这里面有相当一部分的提交都是为了开发小白健康,也就是为了远程兼职恰饭,纯粹的自己的项目提交应该只占了 50% 左右。\\r\\n\\r\\n稍微盘点一下 2018 年写过的个人项目吧,先说说勉强算是成品的,第一个是能在浏览器里玩NES游戏的模拟器,可以在[此处体验](http://blog.simplenaive.cn/Web-NES/dist/index.html)。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63693445-af8c8a80-c846-11e9-876d-5be465952727.png)\\r\\n\\r\\n然后是自己写的博客框架,完全依赖 Github 的 Issue 功能,一键部署,响应式设计,兼容 PC / 手机 / 平板,不出意料的话,你看到的这篇文章就是通过这个项目展现的,可以在此处体验:[Yidadaa的个人博客](https://blog.simplenaive.cn/)。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63693772-802a4d80-c847-11e9-8861-e15cdd1b618e.png)\\r\\n\\r\\n再之就是和一个 18 级的学弟一起搞的小程序,相当于把 2017 年挖的坑给填了一下,顺便带学弟体验一下软件开发的流程,由于学校不允许这类小程序出现,所以没有正式上线,属于自用阶段,开源地址:[成电Life小程序](https://github.com/UESTC-Miniapp/UESTC-Miniapp-FrontEnd)。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63694091-3d1caa00-c848-11e9-8651-2afa4681bddb.png)\\r\\n\\r\\n然后罗列几个挖了坑没填上的项目,首先是分布式系统的课设,一个 P2P 的聊天程序,使用 Flutter 开发,由于在界面上花了太多功夫,导致没有按时完成,这门课最后也被我放弃了,项目自然也不了了之:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63694849-edd77900-c849-11e9-8d4a-ae3126cb2c09.png)\\r\\n\\r\\n然后是一个简易投票软件以及时间记录程序,前者本来是在毕业季的时候发放问卷用的,结果时间太紧没有完成,搞了界面就没有继续再做了;后者是我挖的另外一个惊天巨坑,还有另外两个相关的项目在计划中,以后会慢慢填上。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63695075-71916580-c84a-11e9-95a4-f8c832f52812.png)\\r\\n\\r\\n以上就是 2018 年的个人项目汇总,突然发现还挺多的,2018 过得还挺充实的嘛。\\r\\n\\r\\n**2018 目标完成情况**\\r\\n- [x] Coding 平均频度不低于 2 / 天,每周 Coding 记录不少于 5 天;\\r\\n- [ ] 全年频度向 1000 进发;\\r\\n- [x] 4月份之前将教务系统插件完善放出,组织新生力量投入开发;\\r\\n- [ ] lintCode 题库两遍以上。\\r\\n\\r\\n第二个目标没有完成,有点意外,可能是因为中间放假的时候太久没写代码了,希望2019能达成这个目标;第四个目标就纯粹是自己太懒了,三天打鱼两天筛网,2019 年应该拿出充足的时间进行算法训练,方便以后找工作。\\r\\n\\r\\n**对 2019 的展望**\\r\\n- [ ] 平均频度不小于 2 次/天,全年频度向 1000 次进发;\\r\\n- [ ] 完成至少 4 个个人项目;\\r\\n- [ ] 算法训练平均时间不少于 2h / 周。\\r\\n\\r\\n\\r\\n### 学习\\r\\n2018 年的成绩总体来说还算可以,表现要比本科平均水平强些,上半年的毕业设计期末总评拿到了 92 分,主要是工作量到位了,其实并没有多少自己的东西,对比同期的大佬同学,自己的工作实在是不够看。\\r\\n\\r\\n下半年研究生入学,几门学位课学的还算可以,研究生阶段的课程确实要比本科更偏计算机理论一些,收获比较大的几门课当数有限自动机、算法设计与分析以及高级计算机系统结构。高系这门课我自认为学的很透彻了,然而最后期末考试还是扑街了,回想起来应该还是有个知识点没有记牢,导致丢了很多分,但这门课依然十分重要,弥补了本科阶段知识的空白,把编译器到硬件这段之间的空白填补上了。\\r\\n\\r\\n然后就不得不说一说科研能力,2018 年结束了,自己的科研产出依然是零,说到底还是自己不上心,身边的同学都能稳稳地坐在实验室搬砖,而我总是自视甚高,不肯听导师的安排,导致科研迟迟没有进展,这实在是急需改进的地方,我希望剩下的两年能沉得住气,把基础的东西沉淀下来,这样以后才能走得更远。\\r\\n\\r\\n**2018 目标完成情况**\\r\\n- [ ] 系统地锻炼科研能力;\\r\\n- [ ] 目标是产出一篇较高水平的论文;\\r\\n- [ ] 保证每月的英语锻炼,包括词汇量以及文献阅读等。\\r\\n\\r\\n写到这里感觉自己脸上火辣辣得疼,浪潮退去之后,原来自己才是那个没穿裤衩裸泳的人……不过胜不骄,败不馁,打起精神来总能走出一片路来,继续向前走吧。\\r\\n\\r\\n**2019 的展望**\\r\\n- [ ] 在至少两场竞赛中夺得名次;\\r\\n- [ ] 完成至少一篇论文的写作。\\r\\n\\r\\n从今以后,论文和比赛就是我的命。\\r\\n\\r\\n\\r\\n### 品格\\r\\n对于自己性格中的优点和缺点,自己在去年的总结中已经有所提及,我认为 2018 年,我的品格上面没有出现特别大的问题,经历了毕业以后,自己越来越珍惜身边的朋友,他们身上的闪光点就是引导我前进的梯度,三人行必有我师,此话情真意切。\\r\\n\\r\\n但缺点仍不容忽视。我认为自己目前严肃有余而认真不足,表面上是在一本正经的做事,但只要稍稍仔细观察下,就能看出我总是做足了表面功夫,看见什么有趣的东西都想去试一试,然后却四处发力,拳头全打在了空气上。人类社会发展至今,已经不再需要亚里士多德或者达芬奇这样的博学者,而是更需要在某个细分领域专精的博士,更何况现在自己还只是个小硕,如果不能在某个方向立稳脚跟,我想这个研究生必定会读的很失败。\\r\\n\\r\\n**2019 的展望**\\r\\n- [ ] Focus on one thing at any time.\\r\\n\\r\\n### 情感\\r\\n个人情感大体上可以分成亲情、友情和爱情。去年这段是留空的,我当时觉得把这么私密的、敏感的情感公开出来,心理上会感到羞耻从而难以启齿,这也跟自己接受的教育有关,总认为沉静、含蓄和内敛才是好的。\\r\\n\\r\\n但我现在觉得,适当地显露自己的情绪也是好的,人始终是群居动物,本性里渴望交流,渴望寻求认同,所以必然会有分享欲,人类的喜乐或许并不相通,但身边的人多多能感受到一二分自己的真实心声,向身边的人袒露心扉,并不失为一种坦荡,所以今年我会花很大的篇幅来写一写这些事情。\\r\\n\\r\\n从家庭关系上来看,我这个儿子当得还算可以,到目前为止,我的表现已经大大超出了我父母的期待,虽然放到其他社会阶层去看,我只能算是勉强达到了及格线,但在农民段位里,我父母绝对能在养儿子这一项任务上拿到 SS 的评价了。\\r\\n\\r\\n然而,农民家庭出个知识分子,有喜也有忧。农村人没有特别高的追求,不会像城市里一些控制欲很强的父母那样要求子女一直往上走,他们抱着一种很朴实的心态,觉得读书学到了知识,够养家糊口娶媳妇就好,尤其是我母亲这样的农村妇女,能抱上孙子孙女已经算是人生一大乐事了。所以我这样读了大学的人,在家族里显得格格不入,这种格格不入从小便显山露水,我父亲一度指着鼻子骂我,说我从小受了亲戚邻居的恩惠,长大了却连漂亮话都说不囫囵,简直白眼狼一样,这时母亲往往还会附上我的若干堂兄弟在酒桌上挥斥方遒的英勇事例,我只能哑口无言,退避三舍。\\r\\n\\r\\n所以在亲情上,我表现得有些冷漠,如果有人能在时间尺度上纵览我的过去,他应该并不会对这种冷漠感到惊讶,他会打趣道:嗐,这浑小子小时候读说明书都能读得津津有味,您就别指望这死理性派长大后能八面玲珑巧舌如簧啦!\\r\\n\\r\\n这种死理性派带来的不良体验,不仅亲人们能感受出来,我的朋友们也能感受到。我是一个技术狂热者,认为编程可以解决生活中遇到的任何问题,甚至在跟喜欢的妹子聊天时都在想着能不能从聊天记录中挖掘出什么有用的信息来,后来的确也建了一个某人的信息库,但显然这种用直男思维解决情感问题的行为,跟用键盘控制挖掘机做菜一样荒诞。\\r\\n\\r\\n我能感受到朋友们对我的评价,他们或许会说我敲代码很熟练,这也会一点,那也会一点,但绝不会说我多么善解人意,或者会与人相处地很舒服,但现在总归要比刚上大学时强了很多,那时我似乎总是在别人的底线上走来走去,室友们能包容我到现在还没有动手,就已经非常值得在心底奏出一段土耳其进行曲了。\\r\\n\\r\\n我是个不解风情的人,而且懦弱,加上瘦弱的体型,看上去没有什么男子气概。这些都是客观存在的特点,并在一次深夜会酒时从异性那里得到了验证。不得不说,我很羡慕那些情商高异性缘也好的人,他们总是能在正确的时间说出合适的话,活跃气氛的同时收获一片好感,我就做不来,而且越怕越抵触,越抵触越得不到锻炼,那大脑里的一些神经元就永远沉睡。\\r\\n\\r\\n这种特质当然来源于我品格里死理性派的那部分,凡事都讲究个道理和缘由,哪怕是不讲道理的事情也试图去讲讲道理,自然讨不到什么好处。我的感情经历十分贫乏,没有什么人教过我怎么跟异性相处,而且这也不是什么自学就能解决的事情,若抱着自学的目的和很多女生接触,整个人就会陷进蛛网里,想想就觉得难以接受。\\r\\n\\r\\n既然决定了写这一部分,就力求搞清楚问题所在,然后对症下药,所以我就索性多写些过去的事情。\\r\\n\\r\\n我真正的情感萌芽生长于高考完的暑假,和女孩约定一起去青岛看海,那是一段刻骨铭心的经历,但不知怎么我就搞砸了,其实这也没什么,没有人不加练习就能在考试中取得好成绩,和女生相处也应该是这样,如果能摆正心态,事情总能不至于无法挽回,但那时我脑子里与异性打交道的回路似乎还没有就绪,觉得搞砸了就是搞砸了,然后就一砸到底。\\r\\n\\r\\n当时我的眼里没有海全是她,相机里一千多张照片里有九百张她的游客照,含她率高达90%,如果拿这些照片做训练集训个二分类器出来,绝对称得上是个极端不均衡样本问题。\\r\\n\\r\\n欢乐的时光很是短暂,在青岛玩了两三天就坐上了返程的火车,那时我们是真正的穷酸书生,只能坐二十多小时的硬座回家,夜深时,她靠在我的肩膀上睡着了,车窗外辽阔的华北平原灯火阑珊,而我的心里却是锣鼓喧天鞭炮齐鸣,头顶似乎正有一千颗黑洞盘旋,花上好一阵才能平静下来,我觉得时机到了,是时候升华一下这炽热的革命友谊了,然后便吻了上去。\\r\\n\\r\\n后来我看过很多场恋爱电影,也读过很多爱情故事,但都没能再有过那一秒钟的感受,只有在某个燃烧着绚丽彩云的傍晚,从不知名角落吹来的风偷偷拂过脸颊,我才能偶尔捕捉到那醉人的柔软感觉。\\r\\n\\r\\n但就算极尽辞藻之奢华,也不能令时光倒流,也不能抹去看到她头也不回地下车之后发来的那条短信后的失落感与罪恶感,那条短信简明扼要地说“没想到你是这样的人”,“怎样的人?”,这个问题我想了很多年,我一度将其解读为一种判决,她似乎把我与那些平日里不务正业的地痞流氓划为了一类,而这与我自身的认知产生了激烈的冲突,尽管我小时候顽劣不堪,但从初中开始我就努力地成为一个正直的人。\\r\\n\\r\\n而且我对她的热爱从高二那个有着刺眼阳光的午后便缓缓燃起,从拍下那张她在阳光下吃盒饭的照片时便已颇具规模,而现在再去解读那个瞬间,几乎可以百分之百地将其定义为心动的感觉,而且这种感觉在随后两年如野火般蔓延:刻意养成记错题本的习惯然后借给她,把她拉到围观我做好的班级画报的人群前然后尽量装作平常地说“这是我做的”,在她做物理题的时候偷偷地拍下她认真做题的样子,她和班里男生打篮球的时候我在隔壁场里变成一条酸菜鱼……我把对她的热爱小心翼翼地藏在这些小事后面,并把这些回忆在心中精心雕琢成她的样子,但那天过后,她直接用一条短信在这座雕像后刻上了“大清乾隆年间制”,这让我在做那些充满小心思的小事时,就跟初中时跟在女生后面挤眉弄眼的小混混一样惹人厌烦,然后那些炽烈的热爱瞬间就变为烈火吞噬了所有信心,连之后再去找她辩解的勇气也没能拿出来。\\r\\n\\r\\n以现在的视角去分析那件往事,意义并不大,她之后几年很快结识了新的男孩,男友也换了好几任,这场风波对她影响似乎并不大,我觉得也没有必要再去问她那条短信真正的意图,毕竟过去了好几年,许多事情都应该翻篇了,而当我意识到这一点的时候,时间已经是 2018 年,我决定重振旗鼓告别单身。文章开头说,2018 年我满怀期待,就是这个原因。\\r\\n\\r\\n[ 此处1076字已做加密处理 ]\\r\\n\\r\\n### 健康\\r\\n活着并不是意见容易的事情,尤其是健康地活着。这句话在去年的年终总结中就已经写过,但现在还应该添一句注解,不仅要身体健康,还要心理健康。\\r\\n\\r\\n由于身体状况在年中迎来了断崖式下降,我现在不得不比以前任何时候都更注意自己的健康状况。三月份、八月份和九月份都保持了一定的户外运动量,每周的两三天都要出去跑上两公里,此外颈椎部分的舒缓动作也研究了很多,基本已经能让不堪重负的颈椎更好受一些。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/63859433-ed6de800-c9d9-11e9-930d-1eee4d42678e.png)\\r\\n\\r\\n然后便是睡眠情况,上图的数据来源自小米手环,我做了一张图来展示睡眠趋势。其实从睡眠参数上来看,2018 年的睡眠还是蛮正常的,深水比例 20%,平均每天睡觉 8 个小时,简直就是写在百科的睡眠参数,但是入睡时间和起床时间就有点偏离大众了,我可能更偏向于夜猫子型的作息。而且从上图中还能看到一个有趣的现象,那就是冬天的深睡比例和时长都明显要比春夏更长,可能寒冷的天气更加适合人类睡眠吧。\\r\\n\\r\\n然后就是体重,2018 年体重很稳定地保持在 57.7kg 附近,每次称体重都会提示我低于 85% 的同体型人士,看来以后还是要多吃多练,早日达到 65kg 的正常体重。\\r\\n\\r\\n**2018 目标完成情况**\\r\\n- [ ] 坚持跑步的好习惯\\r\\n- [ ] 修正作息至 00:00 - 09:00\\r\\n- [ ] 体重向 65kg 出发\\r\\n\\r\\n轻轻地叹上一口气,希望胃口能早点好起来,这个目标继续留到 2019 年。\\r\\n\\r\\n### 展望2019\\r\\n去年的展望中说到,我是一个非常庸俗的人,今年也不例外,但是随着三观的逐渐完善,很多事情的意义都在发生变化,比如学生时代,金钱对我来说真的那么重要吗?现在觉得未必,读书的时候,我想要追求的东西还是在形而上的层面,我希望 2019 年能把自己的一切都量化起来,只有量化成数据,自己才能更加清醒的活着。\\r\\n\\r\\n所以 2019 最大的愿望,就是**把自己的生活数字化**,然后在拥有基准线的基础上不断进步。这是一个美好的愿望,我希望它能在 2019 结束时变成现实。\\r\\n\\r\\n当然并不是所有事情都能量化,比如情感,它游离在我们身边的空气中,既不能抓住它,又不能任它离开,我能驾驭它吗?可能时间会给我答案。\\r\\n\\r\\n2019 年没有什么大不了的,认真地活着吧。","content":"
\\n

No Fear In My Heart(节选) - 朴树
\\n你也曾经追问,然后沉默
\\n渐渐习惯谎言,并以此为荣
\\n因为没有草原,就忘了你是马
\\n你卑微的人生,从不曾犯错的,无聊的人生

\\n
\\n

前言

\\n

2018 年发生了很多注定写进历史书的大事,霍金、金庸、李咏这些耳熟能详的名人逝世的消息,无不昭示着某段旧时代的淡去。对于我而言,2018 年发生的大事莫过于毕业,本科最后的日子里与同学重温了青春最后的激情,毕业晚会上一曲合唱过后,生活泛起的最后一朵浪花消失在海面上,以后的日子仿佛便会一直沉浸在鱼入大海的逍遥自在和平淡无奇中,此时回味起那时的念头,觉得这预言怕是应验了。

\\n

本文拖更了大半年,文章的标题在 18 年 12 月份就已经拟好,那时我踌躇满志,心里满怀期待,而时至今日动笔,这标题让我感慨万千。

\\n

注:本文全长 9861 字,读完预计耗时 10 分钟。

\\n

摘要

\\n

简短地回顾 2018,前半年毕业在即,工作重心基本在毕业设计和毕业旅行的筹划上,全年更加深入地参与到小白健康的开发工作中去,主导了两个重要页面的协调和开发;而后毕业季来袭,匆匆赶完毕业答辩后,和大晗哥他们筹备了毕业晚会,虽然排面不大,但大家的热情都很高涨,很有热血的感觉。

\\n

毕业晚会后,和党员、峙龙、陶菊以及狗哥去川西自驾,好好玩了一周,算是为本科时光划上了一个句号。下半年研究生生活开始,除了陶菊去华为工作之外,几个好友都重新回到课堂,拿起课本学习的时光无比充实,但焦虑也无处不在,大家都纷纷发了论文,而自己仍在工程和科研之间摇摆不定,这难题始终难以解决。

\\n

2018 年至少是满怀希望的一年,我怀着对她的期望和热爱生活着,尽管没有取得什么璀璨的成绩,但总体上来说自己充满干劲,年中暑假我冒昧地邀请她去看五月天的演唱会,不出所料地遭到了拒绝,没有做好前期调研和预热的空想主义果然还是行不通啊,以后要好好吸取这个宝贵的教训才好。

\\n

身体状况在暑假的时候迎来了最坏的时期,8 月份颈椎疼痛不已,甚至只能躺在床上写代码,幸好随后使用了各种手段,勉强让颈椎恢复了工作,一直到年末都没有遇到什么大问题,健康问题依然不能忽视。

\\n

本文依然会延续去年的设定,从财务、编程、学习、品格、感情、健康这几个方面对 2018 做出总结,并对未来做出展望。

\\n

财务

\\n

我从去年的四月份开始,养成了记账的习惯,所以 2018 年全年的收支明细都一清二楚,去年的年终总结我很详细地列出了收支明细,现在觉得有隐私泄露的风险,而且说实话相对于已经工作的同学来说,这个数值也没有什么参考价值,所以今年就不给出详细数据了。

\\n

先说收入方面,相对于 2017 年,2018 年的收入基本翻了一番,因为大四下学期时间比较充裕,我一直都在为毕业旅行攒钱,所以就花了很多时间和精力在小白健康的开发上,从记账软件中也可以看出全年有60%的收入都来自于小白健康的远程兼职,占比上与去年持平,但金额上翻了大概一倍,另外有17%的收入来自研究生的奖助学金,相当于把交的学费又给还回来了,这个其实没什么好说的,除此之外,也会零星地接一些小项目,剩下的收入基本都产生于此。

\\n

\\"image\\"

\\n

然后说一下支出,当习惯了做外包恰烂钱之后,兜里有了点积蓄,但我又没有存钱的习惯,所以花钱一直大手大脚,2018 年有 35% 的钱都花在了数码设备上,比餐饮支出(23.8%)还要多,期间买了一台 matebook x 笔记本、一台索尼的 a6300 微单以及一个定焦镜头,手机从小米 6X 到红米 note5 再一路换到小米 8,也产生了不少花销,年底购入任天堂 switch 游戏机以及若干游戏,然后其他体脂秤、显示器拍立得之类的小玩意儿不一而足,买来基本属于吃灰状态,都算作是玩具。

\\n

之后是电影话剧以及旅游等娱乐支出,年初的时候受陶菊邀请,和狗哥一起在云南自驾游了一番,玩得很爽;暑假时候五月天的演唱会门票花了些钱,然后毕业旅行以及暑假过渡期租了两个月的房子。在学校的时候在电影上花了很多钱,2018 年上映的 7 分以上的电影基本都去电影院贡献了票房。

\\n

\\"image\\"

\\n

现在想一想,用宝贵的大学时光去做外包挣小钱其实是一件得不偿失的事情,如果能把这些时间精力用来做比赛或者搞科研,其收益肯定要比做外包更高,而且出去实习工资要比自己接活更稳定,还能找工作时作为加分项。如果有大学生在读这篇文章,我强烈建议你把时间投入到比赛 / 科研 / 学习 / 实习上去,学生时代挣的这点小钱跟学到的知识比实在是微不足道。

\\n

2018年目标完成情况:

\\n
    \\n
  • \\n
  • \\n
\\n

其实第一项只完成了前半部分,妹妹第一年高考因成绩不理想而复读了,而且我也没能攒够两个人出去玩的钱,没有兑现承诺,内心有些愧疚。

\\n

对于2019的展望:

\\n
    \\n
  • \\n
\\n

我认为没必要在学校里攒很多钱,如果能找到实习,一直到工作都不必担心钱的问题,所以 2019 最要紧的就是收缩自己的物欲,控制支出。

\\n

编程

\\n

\\"image\\"

\\n

我是 Github 的忠实用户,所有的代码都会托管到这个网站上,所以 Github 的频率图可以在很大程度上代表编程频率,从提交 Commit 的频率上来看,相比去年提升了 130%,与去年同期相比有了长足的进步,这是一个很好的迹象,证明我对编程的热爱有增无减,并且我确实认为这种热爱会一直保持下去。

\\n

但值得一提的是,这里面有相当一部分的提交都是为了开发小白健康,也就是为了远程兼职恰饭,纯粹的自己的项目提交应该只占了 50% 左右。

\\n

稍微盘点一下 2018 年写过的个人项目吧,先说说勉强算是成品的,第一个是能在浏览器里玩NES游戏的模拟器,可以在此处体验

\\n

\\"image\\"

\\n

然后是自己写的博客框架,完全依赖 Github 的 Issue 功能,一键部署,响应式设计,兼容 PC / 手机 / 平板,不出意料的话,你看到的这篇文章就是通过这个项目展现的,可以在此处体验:Yidadaa的个人博客

\\n

\\"image\\"

\\n

再之就是和一个 18 级的学弟一起搞的小程序,相当于把 2017 年挖的坑给填了一下,顺便带学弟体验一下软件开发的流程,由于学校不允许这类小程序出现,所以没有正式上线,属于自用阶段,开源地址:成电Life小程序

\\n

\\"image\\"

\\n

然后罗列几个挖了坑没填上的项目,首先是分布式系统的课设,一个 P2P 的聊天程序,使用 Flutter 开发,由于在界面上花了太多功夫,导致没有按时完成,这门课最后也被我放弃了,项目自然也不了了之:

\\n

\\"image\\"

\\n

然后是一个简易投票软件以及时间记录程序,前者本来是在毕业季的时候发放问卷用的,结果时间太紧没有完成,搞了界面就没有继续再做了;后者是我挖的另外一个惊天巨坑,还有另外两个相关的项目在计划中,以后会慢慢填上。

\\n

\\"image\\"

\\n

以上就是 2018 年的个人项目汇总,突然发现还挺多的,2018 过得还挺充实的嘛。

\\n

2018 目标完成情况

\\n
    \\n
  • \\n
  • \\n
  • \\n
  • \\n
\\n

第二个目标没有完成,有点意外,可能是因为中间放假的时候太久没写代码了,希望2019能达成这个目标;第四个目标就纯粹是自己太懒了,三天打鱼两天筛网,2019 年应该拿出充足的时间进行算法训练,方便以后找工作。

\\n

对 2019 的展望

\\n
    \\n
  • \\n
  • \\n
  • \\n
\\n

学习

\\n

2018 年的成绩总体来说还算可以,表现要比本科平均水平强些,上半年的毕业设计期末总评拿到了 92 分,主要是工作量到位了,其实并没有多少自己的东西,对比同期的大佬同学,自己的工作实在是不够看。

\\n

下半年研究生入学,几门学位课学的还算可以,研究生阶段的课程确实要比本科更偏计算机理论一些,收获比较大的几门课当数有限自动机、算法设计与分析以及高级计算机系统结构。高系这门课我自认为学的很透彻了,然而最后期末考试还是扑街了,回想起来应该还是有个知识点没有记牢,导致丢了很多分,但这门课依然十分重要,弥补了本科阶段知识的空白,把编译器到硬件这段之间的空白填补上了。

\\n

然后就不得不说一说科研能力,2018 年结束了,自己的科研产出依然是零,说到底还是自己不上心,身边的同学都能稳稳地坐在实验室搬砖,而我总是自视甚高,不肯听导师的安排,导致科研迟迟没有进展,这实在是急需改进的地方,我希望剩下的两年能沉得住气,把基础的东西沉淀下来,这样以后才能走得更远。

\\n

2018 目标完成情况

\\n
    \\n
  • \\n
  • \\n
  • \\n
\\n

写到这里感觉自己脸上火辣辣得疼,浪潮退去之后,原来自己才是那个没穿裤衩裸泳的人……不过胜不骄,败不馁,打起精神来总能走出一片路来,继续向前走吧。

\\n

2019 的展望

\\n
    \\n
  • \\n
  • \\n
\\n

从今以后,论文和比赛就是我的命。

\\n

品格

\\n

对于自己性格中的优点和缺点,自己在去年的总结中已经有所提及,我认为 2018 年,我的品格上面没有出现特别大的问题,经历了毕业以后,自己越来越珍惜身边的朋友,他们身上的闪光点就是引导我前进的梯度,三人行必有我师,此话情真意切。

\\n

但缺点仍不容忽视。我认为自己目前严肃有余而认真不足,表面上是在一本正经的做事,但只要稍稍仔细观察下,就能看出我总是做足了表面功夫,看见什么有趣的东西都想去试一试,然后却四处发力,拳头全打在了空气上。人类社会发展至今,已经不再需要亚里士多德或者达芬奇这样的博学者,而是更需要在某个细分领域专精的博士,更何况现在自己还只是个小硕,如果不能在某个方向立稳脚跟,我想这个研究生必定会读的很失败。

\\n

2019 的展望

\\n
    \\n
  • \\n
\\n

情感

\\n

个人情感大体上可以分成亲情、友情和爱情。去年这段是留空的,我当时觉得把这么私密的、敏感的情感公开出来,心理上会感到羞耻从而难以启齿,这也跟自己接受的教育有关,总认为沉静、含蓄和内敛才是好的。

\\n

但我现在觉得,适当地显露自己的情绪也是好的,人始终是群居动物,本性里渴望交流,渴望寻求认同,所以必然会有分享欲,人类的喜乐或许并不相通,但身边的人多多能感受到一二分自己的真实心声,向身边的人袒露心扉,并不失为一种坦荡,所以今年我会花很大的篇幅来写一写这些事情。

\\n

从家庭关系上来看,我这个儿子当得还算可以,到目前为止,我的表现已经大大超出了我父母的期待,虽然放到其他社会阶层去看,我只能算是勉强达到了及格线,但在农民段位里,我父母绝对能在养儿子这一项任务上拿到 SS 的评价了。

\\n

然而,农民家庭出个知识分子,有喜也有忧。农村人没有特别高的追求,不会像城市里一些控制欲很强的父母那样要求子女一直往上走,他们抱着一种很朴实的心态,觉得读书学到了知识,够养家糊口娶媳妇就好,尤其是我母亲这样的农村妇女,能抱上孙子孙女已经算是人生一大乐事了。所以我这样读了大学的人,在家族里显得格格不入,这种格格不入从小便显山露水,我父亲一度指着鼻子骂我,说我从小受了亲戚邻居的恩惠,长大了却连漂亮话都说不囫囵,简直白眼狼一样,这时母亲往往还会附上我的若干堂兄弟在酒桌上挥斥方遒的英勇事例,我只能哑口无言,退避三舍。

\\n

所以在亲情上,我表现得有些冷漠,如果有人能在时间尺度上纵览我的过去,他应该并不会对这种冷漠感到惊讶,他会打趣道:嗐,这浑小子小时候读说明书都能读得津津有味,您就别指望这死理性派长大后能八面玲珑巧舌如簧啦!

\\n

这种死理性派带来的不良体验,不仅亲人们能感受出来,我的朋友们也能感受到。我是一个技术狂热者,认为编程可以解决生活中遇到的任何问题,甚至在跟喜欢的妹子聊天时都在想着能不能从聊天记录中挖掘出什么有用的信息来,后来的确也建了一个某人的信息库,但显然这种用直男思维解决情感问题的行为,跟用键盘控制挖掘机做菜一样荒诞。

\\n

我能感受到朋友们对我的评价,他们或许会说我敲代码很熟练,这也会一点,那也会一点,但绝不会说我多么善解人意,或者会与人相处地很舒服,但现在总归要比刚上大学时强了很多,那时我似乎总是在别人的底线上走来走去,室友们能包容我到现在还没有动手,就已经非常值得在心底奏出一段土耳其进行曲了。

\\n

我是个不解风情的人,而且懦弱,加上瘦弱的体型,看上去没有什么男子气概。这些都是客观存在的特点,并在一次深夜会酒时从异性那里得到了验证。不得不说,我很羡慕那些情商高异性缘也好的人,他们总是能在正确的时间说出合适的话,活跃气氛的同时收获一片好感,我就做不来,而且越怕越抵触,越抵触越得不到锻炼,那大脑里的一些神经元就永远沉睡。

\\n

这种特质当然来源于我品格里死理性派的那部分,凡事都讲究个道理和缘由,哪怕是不讲道理的事情也试图去讲讲道理,自然讨不到什么好处。我的感情经历十分贫乏,没有什么人教过我怎么跟异性相处,而且这也不是什么自学就能解决的事情,若抱着自学的目的和很多女生接触,整个人就会陷进蛛网里,想想就觉得难以接受。

\\n

既然决定了写这一部分,就力求搞清楚问题所在,然后对症下药,所以我就索性多写些过去的事情。

\\n

我真正的情感萌芽生长于高考完的暑假,和女孩约定一起去青岛看海,那是一段刻骨铭心的经历,但不知怎么我就搞砸了,其实这也没什么,没有人不加练习就能在考试中取得好成绩,和女生相处也应该是这样,如果能摆正心态,事情总能不至于无法挽回,但那时我脑子里与异性打交道的回路似乎还没有就绪,觉得搞砸了就是搞砸了,然后就一砸到底。

\\n

当时我的眼里没有海全是她,相机里一千多张照片里有九百张她的游客照,含她率高达90%,如果拿这些照片做训练集训个二分类器出来,绝对称得上是个极端不均衡样本问题。

\\n

欢乐的时光很是短暂,在青岛玩了两三天就坐上了返程的火车,那时我们是真正的穷酸书生,只能坐二十多小时的硬座回家,夜深时,她靠在我的肩膀上睡着了,车窗外辽阔的华北平原灯火阑珊,而我的心里却是锣鼓喧天鞭炮齐鸣,头顶似乎正有一千颗黑洞盘旋,花上好一阵才能平静下来,我觉得时机到了,是时候升华一下这炽热的革命友谊了,然后便吻了上去。

\\n

后来我看过很多场恋爱电影,也读过很多爱情故事,但都没能再有过那一秒钟的感受,只有在某个燃烧着绚丽彩云的傍晚,从不知名角落吹来的风偷偷拂过脸颊,我才能偶尔捕捉到那醉人的柔软感觉。

\\n

但就算极尽辞藻之奢华,也不能令时光倒流,也不能抹去看到她头也不回地下车之后发来的那条短信后的失落感与罪恶感,那条短信简明扼要地说“没想到你是这样的人”,“怎样的人?”,这个问题我想了很多年,我一度将其解读为一种判决,她似乎把我与那些平日里不务正业的地痞流氓划为了一类,而这与我自身的认知产生了激烈的冲突,尽管我小时候顽劣不堪,但从初中开始我就努力地成为一个正直的人。

\\n

而且我对她的热爱从高二那个有着刺眼阳光的午后便缓缓燃起,从拍下那张她在阳光下吃盒饭的照片时便已颇具规模,而现在再去解读那个瞬间,几乎可以百分之百地将其定义为心动的感觉,而且这种感觉在随后两年如野火般蔓延:刻意养成记错题本的习惯然后借给她,把她拉到围观我做好的班级画报的人群前然后尽量装作平常地说“这是我做的”,在她做物理题的时候偷偷地拍下她认真做题的样子,她和班里男生打篮球的时候我在隔壁场里变成一条酸菜鱼……我把对她的热爱小心翼翼地藏在这些小事后面,并把这些回忆在心中精心雕琢成她的样子,但那天过后,她直接用一条短信在这座雕像后刻上了“大清乾隆年间制”,这让我在做那些充满小心思的小事时,就跟初中时跟在女生后面挤眉弄眼的小混混一样惹人厌烦,然后那些炽烈的热爱瞬间就变为烈火吞噬了所有信心,连之后再去找她辩解的勇气也没能拿出来。

\\n

以现在的视角去分析那件往事,意义并不大,她之后几年很快结识了新的男孩,男友也换了好几任,这场风波对她影响似乎并不大,我觉得也没有必要再去问她那条短信真正的意图,毕竟过去了好几年,许多事情都应该翻篇了,而当我意识到这一点的时候,时间已经是 2018 年,我决定重振旗鼓告别单身。文章开头说,2018 年我满怀期待,就是这个原因。

\\n

[ 此处1076字已做加密处理 ]

\\n

健康

\\n

活着并不是意见容易的事情,尤其是健康地活着。这句话在去年的年终总结中就已经写过,但现在还应该添一句注解,不仅要身体健康,还要心理健康。

\\n

由于身体状况在年中迎来了断崖式下降,我现在不得不比以前任何时候都更注意自己的健康状况。三月份、八月份和九月份都保持了一定的户外运动量,每周的两三天都要出去跑上两公里,此外颈椎部分的舒缓动作也研究了很多,基本已经能让不堪重负的颈椎更好受一些。

\\n

\\"image\\"

\\n

然后便是睡眠情况,上图的数据来源自小米手环,我做了一张图来展示睡眠趋势。其实从睡眠参数上来看,2018 年的睡眠还是蛮正常的,深水比例 20%,平均每天睡觉 8 个小时,简直就是写在百科的睡眠参数,但是入睡时间和起床时间就有点偏离大众了,我可能更偏向于夜猫子型的作息。而且从上图中还能看到一个有趣的现象,那就是冬天的深睡比例和时长都明显要比春夏更长,可能寒冷的天气更加适合人类睡眠吧。

\\n

然后就是体重,2018 年体重很稳定地保持在 57.7kg 附近,每次称体重都会提示我低于 85% 的同体型人士,看来以后还是要多吃多练,早日达到 65kg 的正常体重。

\\n

2018 目标完成情况

\\n
    \\n
  • \\n
  • \\n
  • \\n
\\n

轻轻地叹上一口气,希望胃口能早点好起来,这个目标继续留到 2019 年。

\\n

展望2019

\\n

去年的展望中说到,我是一个非常庸俗的人,今年也不例外,但是随着三观的逐渐完善,很多事情的意义都在发生变化,比如学生时代,金钱对我来说真的那么重要吗?现在觉得未必,读书的时候,我想要追求的东西还是在形而上的层面,我希望 2019 年能把自己的一切都量化起来,只有量化成数据,自己才能更加清醒的活着。

\\n

所以 2019 最大的愿望,就是把自己的生活数字化,然后在拥有基准线的基础上不断进步。这是一个美好的愿望,我希望它能在 2019 结束时变成现实。

\\n

当然并不是所有事情都能量化,比如情感,它游离在我们身边的空气中,既不能抓住它,又不能任它离开,我能驾驭它吗?可能时间会给我答案。

\\n

2019 年没有什么大不了的,认真地活着吧。

\\n"},"20":{"id":20,"date":"2019/06/24","author":"Yidadaa","title":"LeetCode困难题赏 - 887.扔鸡蛋","mdContent":"### 题目\\r\\n假设有$K$个鸡蛋和$N$层楼,每个蛋的性质完全相同,而且如果某个蛋已经碎了,就没法再次使用。假如存在楼层$F, F\\\\in[0, n]$,且鸡蛋从任何高于$F$的楼层扔下都会碎掉,但从低于或等于$F$的楼层扔下则不会碎。\\r\\n\\r\\n每次移动,都可以使用一个鸡蛋从某个楼层扔下,如果你想准确地测得$F$的值,那么在最坏的情况下,最少需要移动几次?\\r\\n\\r\\n原题链接:[Leetcode 887](https://leetcode-cn.com/problems/super-egg-drop/)。\\r\\n\\r\\n### 题解\\r\\n刚看到这道题时,很容易一头雾水,不知道题目在说些什么,因为扔鸡蛋的结果我们是无法在做题时实时获得的,比如我们在第$x$层楼扔第$i$个蛋时,是不知道这个蛋是否会碎,而且对于给定楼层总数和鸡蛋总数,我们知道存在楼层$F$会令鸡蛋碎掉,但是我们却并不知道这个数值到底是几。\\r\\n\\r\\n那么这道题到底在说些什么呢?经过分析可以发现,这道题之所以难理解,是因为题目将真正考察的地方隐藏起来了,题目的核心就在于理解最后一句话:**在最坏的情况下,最少移动几次**。这句话意味着,我们在扔鸡蛋时是可以采取多种策略的,每种策略都对应一种最坏情况,比如:\\r\\n1. 采用策略$a$:从第一层逐层向上扔,那么最坏情况是$min(K, N, F)$;\\r\\n2. 采用策略$b$:从顶楼逐层向下扔,那么最坏情况是$min(K, N, F)$,即与第一种策略类似;\\r\\n3. 采用最优策略$\\\\hat{f}$,我们现在可能不知道这个策略具体是什么,但是可以确定移动次数是$x=f(K, N, F)$。\\r\\n\\r\\n针对上面列出的几种情况,不难发现,移动次数$x$是与$K, N, F$有关的函数,即$f(K, N, F)$,那么函数$f$就可以代表我们选择的策略,由于具体的策略我们是不知道的,所以将策略函数$f$也看作一个变量,那么最终我们要求得的$x$表达式可以写作:\\r\\n$$x=g(K, N, F, f)$$\\r\\n到了这一步,我们才算是真正的理解题意了,我们需要找到一个算法$g$,在策略空间$f\\\\in\\\\Psi$中找到最优策略$\\\\hat{f}$,并且针对这种策略$\\\\hat{f}$,找到最坏情况$F\\\\in\\\\[0,N]$对应的$x$。\\r\\n\\r\\n绕了这么久,可以发现,我们遍历所有可能的策略,并且在每个策略中,都假设楼层$F$总是出现在使当前策略$f$移动次数最大的楼层处,题目并没有给出如何遍历楼层,也没有给出$F$的值,这两个值都需要我们在算法$g$中自行遍历。\\r\\n\\r\\n对于这个问题,一个自然的思路就是使用动态规划的思想来处理。我们使用状态表$dp[K][N]$来表示我们拥有$K$个鸡蛋和$N$层楼时的最小移动次数,那么考虑初始情况,拥有$i\\\\in[0,K]$个蛋,$j\\\\in[0,N]$层楼时:\\r\\n1. 若$i=0 or j=0$,毋庸置疑,不需要进行测试,因为没有鸡蛋和楼层,$dp[0][1]=dp[1][0]=dp[0][0]=0$;\\r\\n2. 若$i=1, j\\\\in[1,N]$,此时我们只有一个蛋,那么只能使用前面提到的策略$a$来试探,那么最坏情况就是$F=j$,即蛋碎的楼层在最大楼层处,那么我们最少尝试次数就是$dp[1][j]=j, j\\\\in[1,N]$;\\r\\n3. 若$i\\\\in(1,K], j=1$,我们有不止一个鸡蛋,但是只有一层楼,那么毫无疑问,只需要测试一次就行了,得到$dp[i][1]=1, i\\\\in(1, K)$;\\r\\n4. 若$i\\\\in(1,K], j\\\\in(1,N)$,即有不止一个鸡蛋,且不止一层楼,那么我们就需要使用最优策略$\\\\hat{f}$来确定最小移动次数,\\r\\n\\r\\n写成代码形式:\\r\\n```python\\r\\ndef superEggDrop(self, K: int, N: int) -> int:\\r\\n dp = [[0] * (K + 1) for i in range(N + 1)]\\r\\n for i in range(1, N + 1): dp[i][1] = i\\r\\n for i in range(1, K + 1): dp[1][i] = 1\\r\\n\\r\\n for i in range(2, N + 1):\\r\\n for j in range(2, K + 1):\\r\\n dp[i][j] = dp[i][j - 1]\\r\\n for k in range(1, i + 1):\\r\\n dp[i][j] = min(dp[i][j], 1 + max(dp[k - 1][j - 1], dp[i - k][j]))\\r\\n\\r\\n return dp[N][K]\\r\\n```\\r\\n**算法复杂度:**$O(KN^2)$\\r\\n\\r\\n> 查看带有$\\\\LaTeX$公式渲染的博客内容:[https://blog.simplenaive.cn/#/post/20](https://blog.simplenaive.cn/#/post/20)","content":"

题目

\\n

假设有KKK个鸡蛋和NNN层楼,每个蛋的性质完全相同,而且如果某个蛋已经碎了,就没法再次使用。假如存在楼层F,F[0,n]F, F\\\\in[0, n]F,F[0,n],且鸡蛋从任何高于FFF的楼层扔下都会碎掉,但从低于或等于FFF的楼层扔下则不会碎。

\\n

每次移动,都可以使用一个鸡蛋从某个楼层扔下,如果你想准确地测得FFF的值,那么在最坏的情况下,最少需要移动几次?

\\n

原题链接:Leetcode 887

\\n

题解

\\n

刚看到这道题时,很容易一头雾水,不知道题目在说些什么,因为扔鸡蛋的结果我们是无法在做题时实时获得的,比如我们在第xxx层楼扔第iii个蛋时,是不知道这个蛋是否会碎,而且对于给定楼层总数和鸡蛋总数,我们知道存在楼层FFF会令鸡蛋碎掉,但是我们却并不知道这个数值到底是几。

\\n

那么这道题到底在说些什么呢?经过分析可以发现,这道题之所以难理解,是因为题目将真正考察的地方隐藏起来了,题目的核心就在于理解最后一句话:在最坏的情况下,最少移动几次。这句话意味着,我们在扔鸡蛋时是可以采取多种策略的,每种策略都对应一种最坏情况,比如:

\\n
    \\n
  1. 采用策略aaa:从第一层逐层向上扔,那么最坏情况是min(K,N,F)min(K, N, F)min(K,N,F)
  2. \\n
  3. 采用策略bbb:从顶楼逐层向下扔,那么最坏情况是min(K,N,F)min(K, N, F)min(K,N,F),即与第一种策略类似;
  4. \\n
  5. 采用最优策略f^\\\\hat{f}f^,我们现在可能不知道这个策略具体是什么,但是可以确定移动次数是x=f(K,N,F)x=f(K, N, F)x=f(K,N,F)
  6. \\n
\\n

针对上面列出的几种情况,不难发现,移动次数xxx是与K,N,FK, N, FK,N,F有关的函数,即f(K,N,F)f(K, N, F)f(K,N,F),那么函数fff就可以代表我们选择的策略,由于具体的策略我们是不知道的,所以将策略函数fff也看作一个变量,那么最终我们要求得的xxx表达式可以写作:

\\n

x=g(K,N,F,f)x=g(K, N, F, f)\\nx=g(K,N,F,f)

\\n

到了这一步,我们才算是真正的理解题意了,我们需要找到一个算法ggg,在策略空间fΨf\\\\in\\\\PsifΨ中找到最优策略f^\\\\hat{f}f^,并且针对这种策略f^\\\\hat{f}f^,找到最坏情况F\\\\in\\\\[0,N]对应的xxx

\\n

绕了这么久,可以发现,我们遍历所有可能的策略,并且在每个策略中,都假设楼层FFF总是出现在使当前策略fff移动次数最大的楼层处,题目并没有给出如何遍历楼层,也没有给出FFF的值,这两个值都需要我们在算法ggg中自行遍历。

\\n

对于这个问题,一个自然的思路就是使用动态规划的思想来处理。我们使用状态表dp[K][N]dp[K][N]dp[K][N]来表示我们拥有KKK个鸡蛋和NNN层楼时的最小移动次数,那么考虑初始情况,拥有i[0,K]i\\\\in[0,K]i[0,K]个蛋,j[0,N]j\\\\in[0,N]j[0,N]层楼时:

\\n
    \\n
  1. i=0orj=0i=0 or j=0i=0orj=0,毋庸置疑,不需要进行测试,因为没有鸡蛋和楼层,dp[0][1]=dp[1][0]=dp[0][0]=0dp[0][1]=dp[1][0]=dp[0][0]=0dp[0][1]=dp[1][0]=dp[0][0]=0
  2. \\n
  3. i=1,j[1,N]i=1, j\\\\in[1,N]i=1,j[1,N],此时我们只有一个蛋,那么只能使用前面提到的策略aaa来试探,那么最坏情况就是F=jF=jF=j,即蛋碎的楼层在最大楼层处,那么我们最少尝试次数就是dp[1][j]=j,j[1,N]dp[1][j]=j, j\\\\in[1,N]dp[1][j]=j,j[1,N]
  4. \\n
  5. i(1,K],j=1i\\\\in(1,K], j=1i(1,K],j=1,我们有不止一个鸡蛋,但是只有一层楼,那么毫无疑问,只需要测试一次就行了,得到dp[i][1]=1,i(1,K)dp[i][1]=1, i\\\\in(1, K)dp[i][1]=1,i(1,K)
  6. \\n
  7. i(1,K],j(1,N)i\\\\in(1,K], j\\\\in(1,N)i(1,K],j(1,N),即有不止一个鸡蛋,且不止一层楼,那么我们就需要使用最优策略f^\\\\hat{f}f^来确定最小移动次数,
  8. \\n
\\n

写成代码形式:

\\n
def superEggDrop(self, K: int, N: int) -> int:\\n    dp = [[0] * (K + 1) for i in range(N + 1)]\\n    for i in range(1, N + 1): dp[i][1] = i\\n    for i in range(1, K + 1): dp[1][i] = 1\\n\\n    for i in range(2, N + 1):\\n        for j in range(2, K + 1):\\n            dp[i][j] = dp[i][j - 1]\\n            for k in range(1, i + 1):\\n                dp[i][j] = min(dp[i][j], 1 + max(dp[k - 1][j - 1], dp[i - k][j]))\\n\\n    return dp[N][K]\\n
\\n

算法复杂度:O(KN2)O(KN^2)O(KN2)

\\n
\\n

查看带有LaTeX\\\\LaTeXLATEX公式渲染的博客内容:https://blog.simplenaive.cn/#/post/20

\\n
\\n"},"21":{"id":21,"date":"2019/06/29","author":"Yidadaa","title":"LeetCode困难题赏 - 600.不含连续1的非负整数","mdContent":"> 本文试从文法角度给出此题的解题思路。\\r\\n\\r\\n### 题目\\r\\n给定一个正整数 n,找出小于或等于 n 的非负整数中,其二进制表示不包含 连续的1的个数,其中$1\\\\leq n \\\\leq 10^9$。\\r\\n\\r\\n**示例:**\\r\\n```\\r\\n输入: 5\\r\\n输出: 5\\r\\n解释: \\r\\n下面是带有相应二进制表示的非负整数<= 5:\\r\\n0 : 0\\r\\n1 : 1\\r\\n2 : 10\\r\\n3 : 11\\r\\n4 : 100\\r\\n5 : 101\\r\\n其中,只有整数3违反规则(有两个连续的1),其他5个满足规则。\\r\\n```\\r\\n来源:[600. 不含连续1的非负整数](https://leetcode-cn.com/problems/non-negative-integers-without-consecutive-ones/)\\r\\n\\r\\n### 解析\\r\\n对于这种涉及到二进制字符串的题目,一般都会采用动态规划的思想来解决,观察本题所规定的二进制字符串,发现可以由以下文法$[1]$给出:\\r\\n\\r\\n$$S\\\\rightarrow 10S | 0S | 0 | 1 | \\\\epsilon$$\\r\\n\\r\\n那么根据该文法,我们自然而然地想到构造一个基于字符串长度的状态表$dp$,其中$dp[k]$表示长度为$k$的不含连续1的二进制字符串的个数,然后根据文法写出状态表的初始值:\\r\\n\\r\\n```python\\r\\ndp[0] = 1 # 由 S -> ε 语句给出,表示长度为0的字符串只有一个,即空字符串\\r\\ndp[1] = 2 # 由 S -> 0 | 1 语句给出,表示长度为1的字符串有两个,即0和1\\r\\n```\\r\\n\\r\\n然后根据文法语句$S\\\\rightarrow 10S | 0S$可知,任何长度为$k$的符合条件的字符串,都是由以下两种规则得到的:\\r\\n1. 在长度为$k-1$的字符串左侧添加$0$;\\r\\n2. 在长度为$k-2$的字符串左侧添加$10$。\\r\\n\\r\\n从而得到状态表的递推式:`dp[k] = dp[k - 1] + dp[k - 2]`,可以发现,该递推式就是斐波那契的生成式。\\r\\n\\r\\n到此为止,我们只能得到指定长度的二进制字符串的个数,题目中还有个限定条件是$\\\\leq n$,我们分析一个十进制转化为二进制字符串后的构成:\\r\\n$$5 \\\\rightarrow 101, 16 \\\\rightarrow 1000$$\\r\\n即首个字符肯定为1,也就是文法$[1]$中给出的形如$10S$的字符串,我们直接把十进制上界$n$转化为二进制后,可能会得到两种形式的字符串:\\r\\n1. $10S_{suffix}$,即第二位为0;\\r\\n2. $11S_{suffix}$,即第二位为1。\\r\\n\\r\\n对于第二种形式的正整数$n$,我们只需要计算小于$n$的形如$10S$的字符串对应的解即可,设求解程序为$f(S)$,则有如下的分解方式:\\r\\n$$f(S)=dp[L(g(S))]+f(g(S)_{suffix})$$\\r\\n其中$g(S)$表示不大于$S$的形如$10S$的字符串,$L(S)$表示字符串$S$的长度。\\r\\n\\r\\n### 代码\\r\\n[TODO]\\r\\n\\r\\n> 查看带有$\\\\LaTeX$公式渲染的博客内容:[https://blog.simplenaive.cn/#/post/21](https://blog.simplenaive.cn/#/post/21)","content":"
\\n

本文试从文法角度给出此题的解题思路。

\\n
\\n

题目

\\n

给定一个正整数 n,找出小于或等于 n 的非负整数中,其二进制表示不包含 连续的1的个数,其中1n1091\\\\leq n \\\\leq 10^91n109

\\n

示例:

\\n
输入: 5\\n输出: 5\\n解释: \\n下面是带有相应二进制表示的非负整数<= 5:\\n0 : 0\\n1 : 1\\n2 : 10\\n3 : 11\\n4 : 100\\n5 : 101\\n其中,只有整数3违反规则(有两个连续的1),其他5个满足规则。\\n
\\n

来源:600. 不含连续1的非负整数

\\n

解析

\\n

对于这种涉及到二进制字符串的题目,一般都会采用动态规划的思想来解决,观察本题所规定的二进制字符串,发现可以由以下文法[1][1][1]给出:

\\n

S10S0S01ϵS\\\\rightarrow 10S | 0S | 0 | 1 | \\\\epsilon\\nS10S0S01ϵ

\\n

那么根据该文法,我们自然而然地想到构造一个基于字符串长度的状态表dpdpdp,其中dp[k]dp[k]dp[k]表示长度为kkk的不含连续1的二进制字符串的个数,然后根据文法写出状态表的初始值:

\\n
dp[0] = 1 # 由 S -> ε 语句给出,表示长度为0的字符串只有一个,即空字符串\\ndp[1] = 2 # 由 S -> 0 | 1 语句给出,表示长度为1的字符串有两个,即0和1\\n
\\n

然后根据文法语句S10S0SS\\\\rightarrow 10S | 0SS10S0S可知,任何长度为kkk的符合条件的字符串,都是由以下两种规则得到的:

\\n
    \\n
  1. 在长度为k1k-1k1的字符串左侧添加000
  2. \\n
  3. 在长度为k2k-2k2的字符串左侧添加101010
  4. \\n
\\n

从而得到状态表的递推式:dp[k] = dp[k - 1] + dp[k - 2],可以发现,该递推式就是斐波那契的生成式。

\\n

到此为止,我们只能得到指定长度的二进制字符串的个数,题目中还有个限定条件是n\\\\leq nn,我们分析一个十进制转化为二进制字符串后的构成:

\\n

5101,1610005 \\\\rightarrow 101, 16 \\\\rightarrow 1000\\n5101,161000

\\n

即首个字符肯定为1,也就是文法[1][1][1]中给出的形如10S10S10S的字符串,我们直接把十进制上界nnn转化为二进制后,可能会得到两种形式的字符串:

\\n
    \\n
  1. 10Ssuffix10S_{suffix}10Ssuffix,即第二位为0;
  2. \\n
  3. 11Ssuffix11S_{suffix}11Ssuffix,即第二位为1。
  4. \\n
\\n

对于第二种形式的正整数nnn,我们只需要计算小于nnn的形如10S10S10S的字符串对应的解即可,设求解程序为f(S)f(S)f(S),则有如下的分解方式:

\\n

f(S)=dp[L(g(S))]+f(g(S)suffix)f(S)=dp[L(g(S))]+f(g(S)_{suffix})\\nf(S)=dp[L(g(S))]+f(g(S)suffix)

\\n

其中g(S)g(S)g(S)表示不大于SSS的形如10S10S10S的字符串,L(S)L(S)L(S)表示字符串SSS的长度。

\\n

代码

\\n

[TODO]

\\n
\\n

查看带有LaTeX\\\\LaTeXLATEX公式渲染的博客内容:https://blog.simplenaive.cn/#/post/21

\\n
\\n"},"22":{"id":22,"date":"2020/01/02","author":"Yidadaa","title":"2019,天际线","mdContent":"> **没有理想的人不伤心(节选)- 新裤子**\\r\\n> 你曾热爱的那个人\\r\\n> 这一生也不会再见面\\r\\n> 你等在这文化的废墟上\\r\\n> 已没人觉得你狂野\\r\\n> 那些让人敬仰的神殿\\r\\n> 只在无知的人心中灵验\\r\\n> 我住在属于我的猪圈\\r\\n> 这一夜无眠\\r\\n\\r\\n### 前言\\r\\n今年的总结不想再扯什么宏大叙事,因为我发现了人人都有的一种幻觉,对于同一件事情,自己得到的感触仿佛总要比别人的更强烈些,自己的人格仿佛就因此比别人显得更加独特,而不幸的是,这些独特的灵魂们最后往往都成了过江之鲫。\\r\\n\\r\\n所以笔者深知本文难逃窠臼,不过过去一年,有幸从身边人身上听到了一些关于自己的真实评价,这些只言片语对我而言,可谓是于无声处听惊雷,像是醉酒的人挨了当头一棒,偶尔清醒看到自己的满身狼藉,心中羞愧是有的,但是更多的还是感激,我为有这些说真话的亲人和朋友而感到庆幸。\\r\\n\\r\\n### 摘要\\r\\n简短地回顾 2019,前半年和室友参加了一个不怎么光彩的比赛,看到了许多成年人的利益纠纷,不由得感慨人活着实在太累。五月份参加了华为的软挑赛,中间和室友帮某大学的实验室讲了一些机器学习和深度学习的基础课程,恰了些烂钱。\\r\\n\\r\\n之后的半年几乎都在实验室度过,每天踩着滑板从宿舍到主楼通勤,上午九点钟到,下午六点钟走,晚上就在南门体育馆门前的大停车场练滑板。而且那时尤其喜欢吃朝阳的煲仔饭,午饭晚饭天天吃,后来到深圳吃了正宗的煲仔饭后才发现,食堂那个煲仔饭顶多叫盖浇饭 Pro,不过综合来看,还是盖浇饭 Pro 要更好吃一些(呲牙)。\\r\\n\\r\\n暑期结束后,室友和同学纷纷出去实习,我也有些心痒,所以在十一月初通过峙龙的推荐到腾讯的机器人实验室实习,开启了第二段实习经历。\\r\\n\\r\\n本文延续年终总结系列的传统设定,从各个方面盘点一下 2019 年的生活,这次会额外增加一些章节,并削减数据分析部分的内容,因为很多数据太过私人,没有展示出来的必要。\\r\\n\\r\\n### 消费\\r\\n2019 年全年都比较拮据,由于没有再四处接私活来产生稳定收入,只能靠不定期的比赛奖金和额外补贴来度日。\\r\\n\\r\\n2019 全年,我在 Switch 和 Steam 上买了接近两千块的游戏,这个支出既在意料之中,又令我觉得有点意外,因为自己称不上是个游戏狂热者,只是相对于几个朋友来说更喜欢玩单机游戏,像开源掌机这些小玩意儿,也从不吝惜钱财购买。很多游戏给我带来了很多欢乐,Steam 上的魔能、人类一败涂地、方块战斗剧场等,Switch 上的塞尔达传说、九张羊皮纸等,有些游戏和朋友一起玩欢乐得一塌糊涂,而有些游戏则是自己不知不觉就度过了几百个小时的孤独时光。\\r\\n\\r\\n游戏是第九艺术,我在 B 站上关注了很多做独立游戏的 UP 主,深知每款游戏都需要音乐、美术、剧情和程序的无间配合,才能与玩家见面,所以游戏算是消费品中的集大成者,兼有艺术美学和技术美学,这也是我对它们痴迷的原因,有时候购买一款游戏,可能仅仅是觉得开发者们为之付出的努力和激情值得赞赏。当然在大多数情况下,自己只是巴甫洛夫的那条狗罢了,期待从玩游戏这个曾经带来了很多欢乐的动作中再次获得快乐。\\r\\n\\r\\n 其次便是吃喝玩乐。\\r\\n\\r\\n### 编程\\r\\n[TODO]\\r\\n\\r\\n### 学习\\r\\n[TODO]\\r\\n\\r\\n### 品格\\r\\n[TODO]\\r\\n\\r\\n### 三观\\r\\n[TODO]\\r\\n\\r\\n### 健康\\r\\n[TODO]\\r\\n\\r\\n### 展望 2020\\r\\n[TODO]","content":"
\\n

没有理想的人不伤心(节选)- 新裤子\\n你曾热爱的那个人\\n这一生也不会再见面\\n你等在这文化的废墟上\\n已没人觉得你狂野\\n那些让人敬仰的神殿\\n只在无知的人心中灵验\\n我住在属于我的猪圈\\n这一夜无眠

\\n
\\n

前言

\\n

今年的总结不想再扯什么宏大叙事,因为我发现了人人都有的一种幻觉,对于同一件事情,自己得到的感触仿佛总要比别人的更强烈些,自己的人格仿佛就因此比别人显得更加独特,而不幸的是,这些独特的灵魂们最后往往都成了过江之鲫。

\\n

所以笔者深知本文难逃窠臼,不过过去一年,有幸从身边人身上听到了一些关于自己的真实评价,这些只言片语对我而言,可谓是于无声处听惊雷,像是醉酒的人挨了当头一棒,偶尔清醒看到自己的满身狼藉,心中羞愧是有的,但是更多的还是感激,我为有这些说真话的亲人和朋友而感到庆幸。

\\n

摘要

\\n

简短地回顾 2019,前半年和室友参加了一个不怎么光彩的比赛,看到了许多成年人的利益纠纷,不由得感慨人活着实在太累。五月份参加了华为的软挑赛,中间和室友帮某大学的实验室讲了一些机器学习和深度学习的基础课程,恰了些烂钱。

\\n

之后的半年几乎都在实验室度过,每天踩着滑板从宿舍到主楼通勤,上午九点钟到,下午六点钟走,晚上就在南门体育馆门前的大停车场练滑板。而且那时尤其喜欢吃朝阳的煲仔饭,午饭晚饭天天吃,后来到深圳吃了正宗的煲仔饭后才发现,食堂那个煲仔饭顶多叫盖浇饭 Pro,不过综合来看,还是盖浇饭 Pro 要更好吃一些(呲牙)。

\\n

暑期结束后,室友和同学纷纷出去实习,我也有些心痒,所以在十一月初通过峙龙的推荐到腾讯的机器人实验室实习,开启了第二段实习经历。

\\n

本文延续年终总结系列的传统设定,从各个方面盘点一下 2019 年的生活,这次会额外增加一些章节,并削减数据分析部分的内容,因为很多数据太过私人,没有展示出来的必要。

\\n

消费

\\n

2019 年全年都比较拮据,由于没有再四处接私活来产生稳定收入,只能靠不定期的比赛奖金和额外补贴来度日。

\\n

2019 全年,我在 Switch 和 Steam 上买了接近两千块的游戏,这个支出既在意料之中,又令我觉得有点意外,因为自己称不上是个游戏狂热者,只是相对于几个朋友来说更喜欢玩单机游戏,像开源掌机这些小玩意儿,也从不吝惜钱财购买。很多游戏给我带来了很多欢乐,Steam 上的魔能、人类一败涂地、方块战斗剧场等,Switch 上的塞尔达传说、九张羊皮纸等,有些游戏和朋友一起玩欢乐得一塌糊涂,而有些游戏则是自己不知不觉就度过了几百个小时的孤独时光。

\\n

游戏是第九艺术,我在 B 站上关注了很多做独立游戏的 UP 主,深知每款游戏都需要音乐、美术、剧情和程序的无间配合,才能与玩家见面,所以游戏算是消费品中的集大成者,兼有艺术美学和技术美学,这也是我对它们痴迷的原因,有时候购买一款游戏,可能仅仅是觉得开发者们为之付出的努力和激情值得赞赏。当然在大多数情况下,自己只是巴甫洛夫的那条狗罢了,期待从玩游戏这个曾经带来了很多欢乐的动作中再次获得快乐。

\\n

其次便是吃喝玩乐。

\\n

编程

\\n

[TODO]

\\n

学习

\\n

[TODO]

\\n

品格

\\n

[TODO]

\\n

三观

\\n

[TODO]

\\n

健康

\\n

[TODO]

\\n

展望 2020

\\n

[TODO]

\\n"},"25":{"id":25,"date":"2020/02/12","author":"Yidadaa","title":"LeetCode 趣题赏析 - 448. 找到数组中消失的数字","mdContent":"> 这是一道简单题,但是题目中的附加条件使得这道题别具趣味性。\\r\\n\\r\\n## 题目\\r\\n给定一个范围在 `1 ≤ a[i] ≤ n` ( `n` = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。\\r\\n\\r\\n找到所有在 `[1, n]` 范围之间没有出现在数组中的数字。\\r\\n\\r\\n您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。\\r\\n\\r\\n示例:\\r\\n```\\r\\n输入:\\r\\n[4,3,2,7,8,2,3,1]\\r\\n\\r\\n输出:\\r\\n[5,6]\\r\\n```\\r\\n来源:[力扣(LeetCode)](https://leetcode-cn.com/problems/find-all-numbers-disappeared-in-an-array)\\r\\n\\r\\n## 解读\\r\\n从题意出发,数组中重复元素会挤占消失元素的位置,我们举个比较简单的例子:\\r\\n```python\\r\\n[1, 2, 3, 4, 5, 1, 7]\\r\\n```\\r\\n可以看到元素 `1` 挤占了元素 `6` 的空间,我们可以很轻松地通过判断 `nums[i] == i` 来找到答案。\\r\\n\\r\\n这意味着,如果我们能对输入数组进行某种变换,使得数组中的元素满足某种特定地排列方式,那么我们只需要找出不等于索引值的元素所在的索引,就找到了消失的数字。\\r\\n\\r\\n举个稍微复杂点的例子,比如示例输入:\\r\\n```python\\r\\n[4, 3, 2, 7, 8, 2, 3, 1]\\r\\n```\\r\\n为了方便理解,我们对其排个序:\\r\\n```python\\r\\n[1, 2, 2, 3, 3, 4, 7, 8]\\r\\n```\\r\\n手动变换为我们想要的数组形式:\\r\\n```python\\r\\n[1, 2, 3, 4, 2, 3, 7, 8]\\r\\n```\\r\\n可以看到,索引 `5` 和 `6` 处(为方便表述,本文索引是从 `1` 开始)的元素满足了 `nums[i] != i`,所以 `[5, 6]` 就是我们要的答案。\\r\\n\\r\\n那么该如何进行这种变换呢?可以看到题中特地指出 `n == len(nums)`,这提示我们可以用元素值和索引值之间的映射关系来解决问题,过程如下所示,其中 `^` 表示当前索引:\\r\\n```python\\r\\n# 原始数组\\r\\n[4, 3, 2, 7, 8, 2, 3, 1]\\r\\n ^\\r\\n```\\r\\n先判断当前索引指向的数字是否满足 `nums[i] == i` 以及 `nums[nums[i]] == nums[i]`:\\r\\n- 如果已经满足则跳向下一个索引;\\r\\n- 如果不满足,则进行交换,即 `swap(nums[i], nums[nums[i]])`。\\r\\n\\r\\n逐次进行,我们得到以下步骤:\\r\\n```python\\r\\n[7, 3, 2, 4, 8, 2, 3, 1]\\r\\n ^\\r\\n[3, 3, 2, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[2, 3, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 8, 2, 7, 1]\\r\\n ^\\r\\n[3, 2, 3, 4, 1, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n ^\\r\\n[1, 2, 3, 4, 3, 2, 7, 8]\\r\\n```\\r\\n可以看到,数组逐渐变得有序,而且这种变换只需要 $O(N)$ 的时间复杂度,并且无需额外空间。\\r\\n\\r\\n## 代码\\r\\n```python\\r\\nclass Solution:\\r\\n def findDisappearedNumbers(self, nums: List[int]) -> List[int]:\\r\\n nums, i = [x - 1 for x in nums], 0 # 预处理\\r\\n while i < len(nums):\\r\\n if nums[i] == i and nums[nums[i]] == nums[i]\\r\\n tmp, nums[i] = nums[i], nums[nums[i]]\\r\\n nums[tmp] = tmp # 执行交换\\r\\n else: i += 1\\r\\n ret = [] # 找出消失的数字\\r\\n for i in range(len(nums)):\\r\\n if i != nums[i]: ret.append(i + 1)\\r\\n return ret\\r\\n```","content":"
\\n

这是一道简单题,但是题目中的附加条件使得这道题别具趣味性。

\\n
\\n

题目

\\n

给定一个范围在 1 ≤ a[i] ≤ nn = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。

\\n

找到所有在 [1, n] 范围之间没有出现在数组中的数字。

\\n

您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。

\\n

示例:

\\n
输入:\\n[4,3,2,7,8,2,3,1]\\n\\n输出:\\n[5,6]\\n
\\n

来源:力扣(LeetCode)

\\n

解读

\\n

从题意出发,数组中重复元素会挤占消失元素的位置,我们举个比较简单的例子:

\\n
[1, 2, 3, 4, 5, 1, 7]\\n
\\n

可以看到元素 1 挤占了元素 6 的空间,我们可以很轻松地通过判断 nums[i] == i 来找到答案。

\\n

这意味着,如果我们能对输入数组进行某种变换,使得数组中的元素满足某种特定地排列方式,那么我们只需要找出不等于索引值的元素所在的索引,就找到了消失的数字。

\\n

举个稍微复杂点的例子,比如示例输入:

\\n
[4, 3, 2, 7, 8, 2, 3, 1]\\n
\\n

为了方便理解,我们对其排个序:

\\n
[1, 2, 2, 3, 3, 4, 7, 8]\\n
\\n

手动变换为我们想要的数组形式:

\\n
[1, 2, 3, 4, 2, 3, 7, 8]\\n
\\n

可以看到,索引 56 处(为方便表述,本文索引是从 1 开始)的元素满足了 nums[i] != i,所以 [5, 6] 就是我们要的答案。

\\n

那么该如何进行这种变换呢?可以看到题中特地指出 n == len(nums),这提示我们可以用元素值和索引值之间的映射关系来解决问题,过程如下所示,其中 ^ 表示当前索引:

\\n
# 原始数组\\n[4, 3, 2, 7, 8, 2, 3, 1]\\n ^\\n
\\n

先判断当前索引指向的数字是否满足 nums[i] == i 以及 nums[nums[i]] == nums[i]

\\n
    \\n
  • 如果已经满足则跳向下一个索引;
  • \\n
  • 如果不满足,则进行交换,即 swap(nums[i], nums[nums[i]])
  • \\n
\\n

逐次进行,我们得到以下步骤:

\\n
[7, 3, 2, 4, 8, 2, 3, 1]\\n ^\\n[3, 3, 2, 4, 8, 2, 7, 1]\\n ^\\n[2, 3, 3, 4, 8, 2, 7, 1]\\n ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n    ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n       ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n          ^\\n[3, 2, 3, 4, 8, 2, 7, 1]\\n             ^\\n[3, 2, 3, 4, 1, 2, 7, 8]\\n             ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n             ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n                ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n                   ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n                      ^\\n[1, 2, 3, 4, 3, 2, 7, 8]\\n
\\n

可以看到,数组逐渐变得有序,而且这种变换只需要 O(N)O(N)O(N) 的时间复杂度,并且无需额外空间。

\\n

代码

\\n
class Solution:\\n    def findDisappearedNumbers(self, nums: List[int]) -> List[int]:\\n        nums, i = [x - 1 for x in nums], 0 # 预处理\\n        while i < len(nums):\\n            if nums[i] == i and nums[nums[i]] == nums[i]\\n                tmp, nums[i] = nums[i], nums[nums[i]]\\n                nums[tmp] = tmp # 执行交换\\n            else: i += 1\\n        ret = [] # 找出消失的数字\\n        for i in range(len(nums)):\\n            if i != nums[i]: ret.append(i + 1)\\n        return ret\\n
\\n"},"26":{"id":26,"date":"2020/02/27","author":"Yidadaa","title":"花式遍历二叉树","mdContent":"> 众所周知,递归是解决问题的良药,但是却不利于装逼,今天教你如何在最简单的问题上装逼。\\r\\n\\r\\n一般来说,基本所有的二叉树的题目都需要遍历树的每一个节点来找到解,在本科阶段学习数据结构时,所有的教材都会教给我们如何使用递归来执行各种遍历,诚然,递归形式的遍历不可谓不简洁:\\r\\n```python\\r\\nclass Node():\\r\\n def __init__(self, val, left, right):\\r\\n self.val = val\\r\\n self.left = left\\r\\n self.right = right\\r\\n\\r\\ndef traverse(node):\\r\\n if not node: return\\r\\n for next_node in [node, node.left, node.right]:\\r\\n if next_node == node: print(node.val)\\r\\n else: traverse(next_node)\\r\\n```\\r\\n我们只需要简单地调整 `[node, node.left, node.right]` 的顺序,就可以轻松地实现前序遍历、中序遍历和后序遍历,但是如果我们想要实现层次遍历,递归就无能为力了,而且由于递归本身自带的爆栈特性,在处理大规模数据时非常容易由于空间不足而报错[2]。\\r\\n\\r\\n那么有没有一种有效的遍历方法,只需要稍加变形就能实现以上四种遍历呢?这个时候就要请出树和图搜索中最常用的非递归遍历方式了,只需使用一个栈或者队列来辅助,就可以轻松实现各种遍历方式。\\r\\n\\r\\n我们先用一个简单的深度优先遍历(Depth First Search, DFS)算法作为例子,在前面定义好的二叉树结构的基础上,使用以下代码进行深度优先遍历:\\r\\n```python\\r\\ndef dfs(node):\\r\\n if not node: return\\r\\n print(node.val)\\r\\n for next_node in [node.left, node.right]:\\r\\n dfs(next_node)\\r\\n```\\r\\n这是一个小学生都会写的递归版本 DFS,显然无法满足我们的装逼需求,我们略施小计将其改造成非递归形式:\\r\\n```python\\r\\ndef dfs(node):\\r\\n stack = [node]\\r\\n while len(stack) > 0:\\r\\n node = stack.pop()\\r\\n if not node: continue\\r\\n print(node.val)\\r\\n for child in [node.left, node.right]:\\r\\n stack.append(child)\\r\\n```\\r\\n可以看到,代码虽然变长了,但是其解决思路却十分优雅,如果你碰巧没有在《编译原理》课上逃课、摸鱼或者睡觉,你应该能一眼看出这里的栈(stack)其实就是在模拟编译器(对于 Python 来说是解释器)在运行递归函数时的行为,回想一下之前的递归代码,每次调用自身函数时,都相当于往栈的末尾压入一个新的函数调用点,而很多编译器或者解释器(比如 Python 或者 Javascript)都没有开启尾递归优化[1],从而引入了栈溢出的风险,所以在实际写代码的时候,非递归形式的代码往往能在性能和效率上更胜一筹,这也是除了能用来装逼之外的更重要的优点。\\r\\n\\r\\n说了半天,让我们切入到文章的主题,如何对二叉树进行非递归形式的遍历呢?刚刚的 DFS 代码虽然很靓仔,但是有很多时候我要进行前中后序遍历或者层次遍历,该怎么写呢?其实只需要稍加变形即可。\\r\\n\\r\\n我们再次略施小计,把 DFS 变成广度优先遍历(Breadth First Search, BFS),而且只需要改动一处即可:\\r\\n```python\\r\\ndef dfs(node):\\r\\n queue= [node]\\r\\n while len(queue) > 0:\\r\\n node = queue.pop(0)\\r\\n if not node: continue\\r\\n print(node.val)\\r\\n for child in [node.left, node.right]:\\r\\n queue.append(child)\\r\\n```\\r\\n可以看到,我们只将栈改成了队列(queue),对于 Python 来说,只需要把 `pop` 函数的参数设置为 `0` 即可,为什么把栈改成队列就能将 DFS 改成 BFS 呢?回想一下栈和队列的特性:\\r\\n1. 栈的特性是先进后出,即所有元素只能从栈的尾部进入,尾部弹出;\\r\\n2. 队列的特性是先进先出,即所有元素从队列的尾部进入,头部弹出。\\r\\n\\r\\n基于队列的这种先进先出的特性,我们的函数在遍历第 `i + 1` 层节点时,其第 `i` 层的节点总能保证是已经被遍历过的,从而实现了广度优先遍历,而**二叉树的层次遍历其实就是广度优先遍历**。\\r\\n\\r\\n最后我们回到开头的引例,把上面的非递归形式的代码改造一下,从而完成二叉树的前中后序遍历,以二叉树的前序遍历为例子:\\r\\n```python\\r\\ndef pre_order(node):\\r\\n stack = [node]\\r\\n while len(stack) > 0:\\r\\n node = stack.pop()\\r\\n if not node: continue\\r\\n print(node.val)\\r\\n for child in [node.right, node.left]:\\r\\n stack.append(child)\\r\\n```\\r\\n可以看到,相对于原始的 DFS 代码,我们只调整了左右子树的入栈顺序,先将右子树入栈,再将左子树入栈,这样保证了出栈时左子树总是先于右子树出栈,从而保证了左子树一定会在右子树之前被遍历到,从而满足了前序遍历的“根左右”的遍历顺序。\\r\\n\\r\\n然后再看中序遍历,思考一下中序遍历的要求:每个节点只能在其左子树遍历完成后遍历,然后再遍历其右节点,即“左根右”的顺序,也就是说,我们要控制根节点被遍历的时机,但是基于栈的方式又不可避免地要先遍历根节点才能访问到其左右孩子,那么怎么才能判定根节点被访问的时机呢?其实我们只需要略施小计,让访问过的节点“二进宫”即可,即给每个节点多设置一个状态:\\r\\n```python\\r\\ndef pre_order(node):\\r\\n stack = [(node, False)]\\r\\n while len(stack) > 0:\\r\\n node, shuold_traverse = stack.pop()\\r\\n if not node: continue\\r\\n if should_traverse:\\r\\n print(node.val)\\r\\n continue\\r\\n for child in [node.right, node, node.left]:\\r\\n stack.append((child, child == node))\\r\\n```\\r\\n可以看到,我们用 `should_traverse` 来控制遍历的时机,每个节点并不会在第一时间被输出,而是赋予一个遍历状态,注意到二次入栈时,每个节点都会严格按照“右根左“的顺序入栈,这样出栈时顺序正好为”左根右“,从而保证了每个根节点只会在其左子树之后输出,满足了中序遍历的条件。\\r\\n\\r\\n有了前两个的铺垫,我们只需故技重施,就可以把中序遍历改造成后序遍历,即入栈时只需要按照”根右左“的顺序入栈:\\r\\n```python\\r\\ndef pre_order(node):\\r\\n stack = [(node, False)]\\r\\n while len(stack) > 0:\\r\\n node, shuold_traverse = stack.pop()\\r\\n if not node: continue\\r\\n if should_traverse:\\r\\n print(node.val)\\r\\n continue\\r\\n for child in [node, node.right, node.left]:\\r\\n stack.append((child, child == node))\\r\\n```\\r\\n这样根节点只会在其左右子树都遍历完成之后再输出,从而满足了后序遍历的条件。\\r\\n\\r\\n到此为止,花式遍历二叉树的方法就介绍完毕了,其实并没有什么高深的技巧,核心思想就是用队列或者栈来模拟递归函数的行为,为所有的遍历行为提供了一个统一的行为,从而更方便记忆,在面试的时候写出来也会有奇效。\\r\\n\\r\\n思考题:二叉树其实是一种有向无环图,可以想一下如何用非递归方法对一个图进行深度优先和广度优先遍历。\\r\\n\\r\\n### 附录\\r\\n1. [维基百科:尾调用](https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8)\\r\\n2. [维基百科:堆栈溢出](https://zh.wikipedia.org/wiki/%E5%A0%86%E7%96%8A%E6%BA%A2%E4%BD%8D)","content":"
\\n

众所周知,递归是解决问题的良药,但是却不利于装逼,今天教你如何在最简单的问题上装逼。

\\n
\\n

一般来说,基本所有的二叉树的题目都需要遍历树的每一个节点来找到解,在本科阶段学习数据结构时,所有的教材都会教给我们如何使用递归来执行各种遍历,诚然,递归形式的遍历不可谓不简洁:

\\n
class Node():\\n  def __init__(self, val, left, right):\\n    self.val = val\\n    self.left = left\\n    self.right = right\\n\\ndef traverse(node):\\n  if not node: return\\n  for next_node in [node, node.left, node.right]:\\n    if next_node == node: print(node.val)\\n    else: traverse(next_node)\\n
\\n

我们只需要简单地调整 [node, node.left, node.right] 的顺序,就可以轻松地实现前序遍历、中序遍历和后序遍历,但是如果我们想要实现层次遍历,递归就无能为力了,而且由于递归本身自带的爆栈特性,在处理大规模数据时非常容易由于空间不足而报错[2]。

\\n

那么有没有一种有效的遍历方法,只需要稍加变形就能实现以上四种遍历呢?这个时候就要请出树和图搜索中最常用的非递归遍历方式了,只需使用一个栈或者队列来辅助,就可以轻松实现各种遍历方式。

\\n

我们先用一个简单的深度优先遍历(Depth First Search, DFS)算法作为例子,在前面定义好的二叉树结构的基础上,使用以下代码进行深度优先遍历:

\\n
def dfs(node):\\n  if not node: return\\n  print(node.val)\\n  for next_node in [node.left, node.right]:\\n    dfs(next_node)\\n
\\n

这是一个小学生都会写的递归版本 DFS,显然无法满足我们的装逼需求,我们略施小计将其改造成非递归形式:

\\n
def dfs(node):\\n  stack = [node]\\n  while len(stack) > 0:\\n    node = stack.pop()\\n    if not node: continue\\n    print(node.val)\\n    for child in [node.left, node.right]:\\n      stack.append(child)\\n
\\n

可以看到,代码虽然变长了,但是其解决思路却十分优雅,如果你碰巧没有在《编译原理》课上逃课、摸鱼或者睡觉,你应该能一眼看出这里的栈(stack)其实就是在模拟编译器(对于 Python 来说是解释器)在运行递归函数时的行为,回想一下之前的递归代码,每次调用自身函数时,都相当于往栈的末尾压入一个新的函数调用点,而很多编译器或者解释器(比如 Python 或者 Javascript)都没有开启尾递归优化[1],从而引入了栈溢出的风险,所以在实际写代码的时候,非递归形式的代码往往能在性能和效率上更胜一筹,这也是除了能用来装逼之外的更重要的优点。

\\n

说了半天,让我们切入到文章的主题,如何对二叉树进行非递归形式的遍历呢?刚刚的 DFS 代码虽然很靓仔,但是有很多时候我要进行前中后序遍历或者层次遍历,该怎么写呢?其实只需要稍加变形即可。

\\n

我们再次略施小计,把 DFS 变成广度优先遍历(Breadth First Search, BFS),而且只需要改动一处即可:

\\n
def dfs(node):\\n  queue= [node]\\n  while len(queue) > 0:\\n    node = queue.pop(0)\\n    if not node: continue\\n    print(node.val)\\n    for child in [node.left, node.right]:\\n      queue.append(child)\\n
\\n

可以看到,我们只将栈改成了队列(queue),对于 Python 来说,只需要把 pop 函数的参数设置为 0 即可,为什么把栈改成队列就能将 DFS 改成 BFS 呢?回想一下栈和队列的特性:

\\n
    \\n
  1. 栈的特性是先进后出,即所有元素只能从栈的尾部进入,尾部弹出;
  2. \\n
  3. 队列的特性是先进先出,即所有元素从队列的尾部进入,头部弹出。
  4. \\n
\\n

基于队列的这种先进先出的特性,我们的函数在遍历第 i + 1 层节点时,其第 i 层的节点总能保证是已经被遍历过的,从而实现了广度优先遍历,而二叉树的层次遍历其实就是广度优先遍历

\\n

最后我们回到开头的引例,把上面的非递归形式的代码改造一下,从而完成二叉树的前中后序遍历,以二叉树的前序遍历为例子:

\\n
def pre_order(node):\\n  stack = [node]\\n  while len(stack) > 0:\\n    node = stack.pop()\\n    if not node: continue\\n    print(node.val)\\n    for child in [node.right, node.left]:\\n      stack.append(child)\\n
\\n

可以看到,相对于原始的 DFS 代码,我们只调整了左右子树的入栈顺序,先将右子树入栈,再将左子树入栈,这样保证了出栈时左子树总是先于右子树出栈,从而保证了左子树一定会在右子树之前被遍历到,从而满足了前序遍历的“根左右”的遍历顺序。

\\n

然后再看中序遍历,思考一下中序遍历的要求:每个节点只能在其左子树遍历完成后遍历,然后再遍历其右节点,即“左根右”的顺序,也就是说,我们要控制根节点被遍历的时机,但是基于栈的方式又不可避免地要先遍历根节点才能访问到其左右孩子,那么怎么才能判定根节点被访问的时机呢?其实我们只需要略施小计,让访问过的节点“二进宫”即可,即给每个节点多设置一个状态:

\\n
def pre_order(node):\\n  stack = [(node, False)]\\n  while len(stack) > 0:\\n    node, shuold_traverse = stack.pop()\\n    if not node: continue\\n    if should_traverse:\\n        print(node.val)\\n        continue\\n    for child in [node.right, node, node.left]:\\n      stack.append((child, child == node))\\n
\\n

可以看到,我们用 should_traverse 来控制遍历的时机,每个节点并不会在第一时间被输出,而是赋予一个遍历状态,注意到二次入栈时,每个节点都会严格按照“右根左“的顺序入栈,这样出栈时顺序正好为”左根右“,从而保证了每个根节点只会在其左子树之后输出,满足了中序遍历的条件。

\\n

有了前两个的铺垫,我们只需故技重施,就可以把中序遍历改造成后序遍历,即入栈时只需要按照”根右左“的顺序入栈:

\\n
def pre_order(node):\\n  stack = [(node, False)]\\n  while len(stack) > 0:\\n    node, shuold_traverse = stack.pop()\\n    if not node: continue\\n    if should_traverse:\\n        print(node.val)\\n        continue\\n    for child in [node, node.right, node.left]:\\n      stack.append((child, child == node))\\n
\\n

这样根节点只会在其左右子树都遍历完成之后再输出,从而满足了后序遍历的条件。

\\n

到此为止,花式遍历二叉树的方法就介绍完毕了,其实并没有什么高深的技巧,核心思想就是用队列或者栈来模拟递归函数的行为,为所有的遍历行为提供了一个统一的行为,从而更方便记忆,在面试的时候写出来也会有奇效。

\\n

思考题:二叉树其实是一种有向无环图,可以想一下如何用非递归方法对一个图进行深度优先和广度优先遍历。

\\n

附录

\\n
    \\n
  1. 维基百科:尾调用
  2. \\n
  3. 维基百科:堆栈溢出
  4. \\n
\\n"},"27":{"id":27,"date":"2020/02/28","author":"Yidadaa","title":"Gettysburg Address","mdContent":"> Abraham Lincoln, Gettysburg, Pennsylvania, 1863\\r\\n\\r\\nFour score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.\\r\\n\\r\\nNow we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.\\r\\n\\r\\nBut, in a larger sense, we can not dedicate, we can not consecrate, we can not hallow, this ground. The brave men, living and dead, who struggled here, have consecrated it, far above out poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living rather to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us, that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion, that we here highly resolve that these dead shall not have died in vain, that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.\\r\\n\\r\\nLink: [Video](https://www.bilibili.com/video/av45620254?from=search&seid=1967124183545885008)","content":"
\\n

Abraham Lincoln, Gettysburg, Pennsylvania, 1863

\\n
\\n

Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.

\\n

Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.

\\n

But, in a larger sense, we can not dedicate, we can not consecrate, we can not hallow, this ground. The brave men, living and dead, who struggled here, have consecrated it, far above out poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living rather to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us, that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion, that we here highly resolve that these dead shall not have died in vain, that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.

\\n

Link: Video

\\n"},"28":{"id":28,"date":"2020/03/18","author":"Yidadaa","title":"树和数组的千层套路","mdContent":"> 今天教你怎么在线性数组和二叉树间反复横跳。\\r\\n\\r\\n目录:\\r\\n- [x] 二叉堆\\r\\n- [ ] 树状数组\\r\\n- [x] 线段树\\r\\n\\r\\n在最简单的一类数据结构中,有两种最为基础:线性数组和树,它们在实际的应用中十分常用,比如线性数组用来存储结构化数据,树用来存储一些有层级归属关系的数据。一个最典型的使用树的场景就是浏览器中的 GUI 渲染策略,在你阅读本篇文章的时候,浏览器就已经从 HTML 文件中解析出当前界面上的所有元素,并按照从属关系依次渲染出来,你只需要按下键盘上的 `F12` 键,就可以看到页面中所有元素的从属关系。\\r\\n\\r\\n然而,一般意义上的树并不是高度结构化的数据,虽然一颗树的所有信息都可以由根节点遍历出来,但是在没有提前建立索引的情况下,你很难轻易地直接获取树中任意节点的数据。所以人们为了更高的随机索引速度,会更倾向于使用结构稳定的二叉树,尤其是完全二叉树,这是由于完全二叉树的左右子树高度差不超过 1,其每层的节点数量都是 $2^n$ ,所以完全二叉树可以很轻松地存储在线性数组里,然后通过 `child = tree[parent_index * 2]` 的方式去递推任意孩子节点的索引值。\\r\\n\\r\\n二叉树的这种特性使得树状结构可以很方便地存储在线性数组中,而本文就将阐述那些和线性数组关系紧密的树状数据结构们。\\r\\n\\r\\n## 二叉堆\\r\\n在使用二叉堆之前,你需要知道什么是堆,以及为什么要有堆。\\r\\n\\r\\n根据[维基百科](https://zh.wikipedia.org/wiki/%E5%A0%86%E7%A9%8D)的描述,堆是一种具有特殊顺序的树,也就是说,堆就是一种树,就像二叉搜索树那样,堆的节点间也有一些大小关系,最常用的堆是**最大堆**和**最小堆**,又称大(小)顶堆、大(小)根堆等,以最大堆为例,最大堆中的任意一个节点都比它的子节点的值更大,也就是说,堆的根节点的元素是整个堆的最大值,值得注意的是,这种大小约束只对父节点和子节点生效,而相同层级的节点不需要有大小关系上的约束。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/77818922-a1bb2280-7111-11ea-8eef-a4c8ab7e5783.png)\\r\\n\\r\\n除此之外,堆还必须是一颗完全二叉树,这可以保证对堆的各种操作的均摊复杂度最低。\\r\\n\\r\\n但是你可能会依然很疑惑,就像我在数据结构课上听到一个全新的数据结构的时候,心里会想:“wow, awesome, 然后呢?”,人们造出来拥有很多复杂结构的事物,并不是完全为了消磨时间,而是因为为了解决问题而不得不让这些工具变得这么复杂,本文中提到的三种数据结构也是如此,它们本身的特性使得它们在解决某些问题时异常好用。\\r\\n\\r\\n而堆的最常用例子就是优先队列,你可以看到堆的根节点始终是最大值或者最小值,这种最值可以以优先级的形式体现出来,而且这种极值顺序会在动态增减的过程中始终以 $O(log N)$ 的时间复杂度保持着,回想一下,如果你要对一个普通的线性数组取极大值,你就不得不付出 $O(N)$ 的时间复杂度。堆的特性可以大大降低很多需要动态维护优先级的算法的复杂度,比如原始的[迪杰斯特拉最短路径算法](https://zh.wikipedia.org/wiki/%E6%88%B4%E5%85%8B%E6%96%AF%E7%89%B9%E6%8B%89%E7%AE%97%E6%B3%95)的时间复杂度是 $O(N^2)$,而使用堆优化后可以达到 $O(N logN)$。\\r\\n\\r\\n现在你知道了什么是堆,以及堆用来解决那些问题,下面就介绍一下堆的常用操作,本文所述的堆均代表二叉堆。\\r\\n\\r\\n二叉堆通常存储在一个数组中,而堆的插入(insert)、弹出(pop)、取堆顶(top)、删除(remove)操作均可以通过不断交换数组元素来完成,而这些操作均有两个最基础的操作组成:向上调整(up)和向下调整(down),顾名思义,向上调整就是从当前节点开始向根节点遍历,确认该路径上所有节点是否满足“子节点小于(大于)父节点”的顺序,如果不满足,则进行调整;而向下调整则是向叶子节点进行遍历,然后执行同样的操作,写成代码形式(以最大堆为例):\\r\\n\\r\\n```python\\r\\nclass Heap:\\r\\n def __init__(self):\\r\\n self.heap = []\\r\\n\\r\\n def swap(self, i, j):\\r\\n self.heap[i], self.heap[j] = self.heap[j], self.heap[i]\\r\\n\\r\\n def up(self, k):\\r\\n \'\'\'向上调整第 k 个节点\'\'\'\\r\\n while k > 0:\\r\\n parent_index = (k - 1) // 2\\r\\n # 如果子节点大于父节点,则进行调整\\r\\n if self.heap[k] > self.heap[parent_index]:\\r\\n self.swap(k, parent_index)\\r\\n else: break\\r\\n\\r\\n def down(self, k)\\r\\n \'\'\'向下调整第 k 个节点\'\'\'\\r\\n while k < len(self.heap):\\r\\n lchild, rchild = k * 2 + 1, k * 2 + 2\\r\\n # 两个子节点取较大节点\\r\\n child = lchild if self.heap[lchild] > self.heap[rchild] or rchild > len(self.heap) else rchild\\r\\n if child < len(self.heap) and self.heap[child] > self.heap[k]:\\r\\n self.swap(child, k)\\r\\n k = child\\r\\n else: break\\r\\n```\\r\\n\\r\\n可以看到,调整部分的代码的逻辑还是比较清晰的,只需要不断向上或者向下遍历,然后调整对应的值即可。有了两个基础操作,我们可以很方便地完成插入、弹出、取顶、删除等操作了,直接看代码:\\r\\n\\r\\n```python\\r\\nclass Heap:\\r\\n \'\'\'省略 up, down 部分的代码\'\'\'\\r\\n def insert(self, val):\\r\\n \'\'\'将 val 插入堆中\'\'\'\\r\\n self.heap.append(val) # 插到数组尾部\\r\\n self.up(len(self.heap) - 1) # 然后不断向上调整即可\\r\\n\\r\\n def top(self):\\r\\n \'\'\'获取堆顶值\'\'\'\\r\\n return None if len(self.heap) == 0 else self.heap[0] # 只需返回数组中的第一个元素即可\\r\\n\\r\\n def remove(self, k):\\r\\n \'\'\'移除第 k 个节点\'\'\'\\r\\n self.swap(k, len(self.heap) - 1) # 将第 k 个元素交换到尾部\\r\\n ret = self.heap.pop() # 将其从数组中删除\\r\\n self.up(k) # 确认是否需要向上调整\\r\\n self.down(k) # 确认是否需要向下调整\\r\\n return ret\\r\\n\\r\\n def pop(self):\\r\\n \'\'\'弹出堆顶\'\'\'\\r\\n return self.remove(0)\\r\\n```\\r\\n\\r\\n掌握了二叉堆,我们可以很轻松地解决 TOP-k 问题,以及所有需要使用到优先队列的问题,不过值得一提的是,二叉堆作为一种非常常用的数据结构,已经被内置到很多语言的官方库中,在实际使用或者写算法题时,可以直接调包使用,比如 Python3 的 `queue.PriorityQueue` 和 `heapq`,以及 C++ 的 `priority_queue` 等。\\r\\n\\r\\n## 线段树和树状数组\\r\\n线段树和树状数组非常像,可以说树状数组就是线段树的一种简化形式,在应对单点修改的区间问题时,树状数组更为简洁好用,但由于使用了 `low-bit` 技巧,相对来说并没有线段树容易理解,所以本文就先从线段树的原理讲起,再逐步扩展到树状数组。\\r\\n\\r\\n首先,我们用一个例题来作为切入概念:\\r\\n\\r\\n>来源:[303. 区域和检索 - 数组不可变](https://leetcode-cn.com/problems/range-sum-query-immutable/)\\r\\n> 给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。\\r\\n\\r\\n下面是一些示例,给定 `nums = [-2, 0, 3, -5, 2, -1]`,那么对任意的 `(i, j)` 进行求和:\\r\\n```\\r\\nsumRange(0, 2) -> 1\\r\\nsumRange(2, 5) -> -1\\r\\nsumRange(0, 5) -> -3\\r\\n```\\r\\n\\r\\n我们可以很容易地想到 $o(N)$ 的解法,直接对区间 $[i ... j]$ 的数进行求和即可,但是当查询次数很大时,很容易就会超时,而如果我们使用前缀和数组,就可以轻松地在 $o(1)$ 时间内完成任何查询操作,所谓的前缀和数组,就是把前 $i$ 位的数字加起来作为第 $i$ 位的元素值,即 $s[i] = \\\\sum_{j=0}^i a_i$,代码表示如下:\\r\\n\\r\\n```python\\r\\ndef solve(nums):\\r\\n s = [0] + nums\\r\\n for i in range(1, len(nums) + 1):\\r\\n s[i] += s[i - 1]\\r\\n```\\r\\n\\r\\n对于本例,可以算出前缀和数组,为了方便计算,我们往数组前部插入一个零,有了前缀数组,我们就可以使用 $sum(i, j) = s[j + 1] - s[i]$ 来计算任意区间的和了:\\r\\n```\\r\\ns = [0, -2, -2, 1, -4, -2, -3]\\r\\nsumRange(0, 2) = s[3] - s[0] = 1 - 0 = 1\\r\\nsumRange(2, 5) = s[6] - s[2] = -3 - (-2) = -1\\r\\n```\\r\\n\\r\\n可以看到,数组的区间信息可以压缩成更紧凑的形式。对于此例题中的静态数组,前缀和应对起来绰绰有余,但是如果在查询过程中数组的数据发生了变化,我们就不得不在每次变化的时候花上 $o(N)$ 的 时间开销去更新前缀数组,比如下面这道例题。\\r\\n\\r\\n>来源:[307. 区域和检索 - 数组可修改](https://leetcode-cn.com/problems/range-sum-query-mutable/)\\r\\n> 给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。\\r\\n> update(i, val) 函数可以通过将下标为 i 的数值更新为 val,从而对数列进行修改。\\r\\n\\r\\n\\r\\n前缀和的本质是维护区间信息,但在应对可变数组时的灵活性太差,所以我们使用更强大的线段树来解决这个问题。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/79983584-940f7780-84da-11ea-8882-a06db59b8056.png)\\r\\n\\r\\n上图表示了线段树的结构,线段树本质上是一个二叉树,它通过不断地把数组二分的方式来从根节点向叶子节点扩展,每个节点都维护了一个区间内的数组信息,这个数组信息可以是区间内的数组和以及最大最小值。\\r\\n\\r\\n```python\\r\\nclass Node:\\r\\n def __init__(self, l, r):\\r\\n self.l = l\\r\\n self.r = r\\r\\n self.lchild = None\\r\\n self.rchild = None\\r\\n self.val = 0\\r\\n\\r\\nclass NumArray:\\r\\n\\r\\n def __init__(self, nums: List[int]):\\r\\n self.nums = nums\\r\\n self.tree = self.build(0, len(nums) - 1)\\r\\n \\r\\n def build(self, l, r):\\r\\n if l > r: return None\\r\\n node = Node(l, r)\\r\\n if l == r:\\r\\n node.val = self.nums[l]\\r\\n return node\\r\\n m = (l + r) >> 1\\r\\n node.lchild = self.build(l, m)\\r\\n node.rchild = self.build(m + 1, r)\\r\\n node.val = node.lchild.val + node.rchild.val\\r\\n return node\\r\\n\\r\\n def update(self, i: int, val: int) -> None:\\r\\n d = val - self.nums[i]\\r\\n self.nums[i] = val\\r\\n q = [self.tree]\\r\\n while q:\\r\\n node = q.pop()\\r\\n if i >= node.l and i <= node.r: node.val += d\\r\\n else: continue\\r\\n for child in [node.lchild, node.rchild]:\\r\\n if child: q.append(child)\\r\\n\\r\\n def sumRange(self, i: int, j: int) -> int:\\r\\n ret = 0\\r\\n q = [self.tree]\\r\\n while q:\\r\\n node = q.pop()\\r\\n if not node: continue\\r\\n if node.l >= i and node.r <= j:\\r\\n ret += node.val\\r\\n elif node.l < j:\\r\\n q.append(node.lchild)\\r\\n elif node.r > i:\\r\\n q.append(node.rchild)\\r\\n return ret\\r\\n```\\r\\n\\r\\n[未完待续]","content":"
\\n

今天教你怎么在线性数组和二叉树间反复横跳。

\\n
\\n

目录:

\\n
    \\n
  • \\n
  • \\n
  • \\n
\\n

在最简单的一类数据结构中,有两种最为基础:线性数组和树,它们在实际的应用中十分常用,比如线性数组用来存储结构化数据,树用来存储一些有层级归属关系的数据。一个最典型的使用树的场景就是浏览器中的 GUI 渲染策略,在你阅读本篇文章的时候,浏览器就已经从 HTML 文件中解析出当前界面上的所有元素,并按照从属关系依次渲染出来,你只需要按下键盘上的 F12 键,就可以看到页面中所有元素的从属关系。

\\n

然而,一般意义上的树并不是高度结构化的数据,虽然一颗树的所有信息都可以由根节点遍历出来,但是在没有提前建立索引的情况下,你很难轻易地直接获取树中任意节点的数据。所以人们为了更高的随机索引速度,会更倾向于使用结构稳定的二叉树,尤其是完全二叉树,这是由于完全二叉树的左右子树高度差不超过 1,其每层的节点数量都是 2n2^n2n ,所以完全二叉树可以很轻松地存储在线性数组里,然后通过 child = tree[parent_index * 2] 的方式去递推任意孩子节点的索引值。

\\n

二叉树的这种特性使得树状结构可以很方便地存储在线性数组中,而本文就将阐述那些和线性数组关系紧密的树状数据结构们。

\\n

二叉堆

\\n

在使用二叉堆之前,你需要知道什么是堆,以及为什么要有堆。

\\n

根据维基百科的描述,堆是一种具有特殊顺序的树,也就是说,堆就是一种树,就像二叉搜索树那样,堆的节点间也有一些大小关系,最常用的堆是最大堆最小堆,又称大(小)顶堆、大(小)根堆等,以最大堆为例,最大堆中的任意一个节点都比它的子节点的值更大,也就是说,堆的根节点的元素是整个堆的最大值,值得注意的是,这种大小约束只对父节点和子节点生效,而相同层级的节点不需要有大小关系上的约束。

\\n

\\"image\\"

\\n

除此之外,堆还必须是一颗完全二叉树,这可以保证对堆的各种操作的均摊复杂度最低。

\\n

但是你可能会依然很疑惑,就像我在数据结构课上听到一个全新的数据结构的时候,心里会想:“wow, awesome, 然后呢?”,人们造出来拥有很多复杂结构的事物,并不是完全为了消磨时间,而是因为为了解决问题而不得不让这些工具变得这么复杂,本文中提到的三种数据结构也是如此,它们本身的特性使得它们在解决某些问题时异常好用。

\\n

而堆的最常用例子就是优先队列,你可以看到堆的根节点始终是最大值或者最小值,这种最值可以以优先级的形式体现出来,而且这种极值顺序会在动态增减的过程中始终以 O(logN)O(log N)O(logN) 的时间复杂度保持着,回想一下,如果你要对一个普通的线性数组取极大值,你就不得不付出 O(N)O(N)O(N) 的时间复杂度。堆的特性可以大大降低很多需要动态维护优先级的算法的复杂度,比如原始的迪杰斯特拉最短路径算法的时间复杂度是 O(N2)O(N^2)O(N2),而使用堆优化后可以达到 O(NlogN)O(N logN)O(NlogN)

\\n

现在你知道了什么是堆,以及堆用来解决那些问题,下面就介绍一下堆的常用操作,本文所述的堆均代表二叉堆。

\\n

二叉堆通常存储在一个数组中,而堆的插入(insert)、弹出(pop)、取堆顶(top)、删除(remove)操作均可以通过不断交换数组元素来完成,而这些操作均有两个最基础的操作组成:向上调整(up)和向下调整(down),顾名思义,向上调整就是从当前节点开始向根节点遍历,确认该路径上所有节点是否满足“子节点小于(大于)父节点”的顺序,如果不满足,则进行调整;而向下调整则是向叶子节点进行遍历,然后执行同样的操作,写成代码形式(以最大堆为例):

\\n
class Heap:\\n  def __init__(self):\\n    self.heap = []\\n\\n  def swap(self, i, j):\\n    self.heap[i], self.heap[j] = self.heap[j], self.heap[i]\\n\\n  def up(self, k):\\n    '''向上调整第 k 个节点'''\\n    while k > 0:\\n        parent_index = (k - 1) // 2\\n        # 如果子节点大于父节点,则进行调整\\n        if self.heap[k] > self.heap[parent_index]:\\n          self.swap(k, parent_index)\\n        else: break\\n\\n  def down(self, k)\\n    '''向下调整第 k 个节点'''\\n    while k < len(self.heap):\\n      lchild, rchild = k * 2 + 1, k * 2 + 2\\n      # 两个子节点取较大节点\\n      child = lchild if self.heap[lchild] > self.heap[rchild] or rchild > len(self.heap) else rchild\\n      if child < len(self.heap) and self.heap[child] > self.heap[k]:\\n        self.swap(child, k)\\n        k = child\\n      else: break\\n
\\n

可以看到,调整部分的代码的逻辑还是比较清晰的,只需要不断向上或者向下遍历,然后调整对应的值即可。有了两个基础操作,我们可以很方便地完成插入、弹出、取顶、删除等操作了,直接看代码:

\\n
class Heap:\\n  '''省略 up, down 部分的代码'''\\n  def insert(self, val):\\n    '''将 val 插入堆中'''\\n    self.heap.append(val) # 插到数组尾部\\n    self.up(len(self.heap) - 1) # 然后不断向上调整即可\\n\\n  def top(self):\\n    '''获取堆顶值'''\\n    return None if len(self.heap) == 0 else self.heap[0] # 只需返回数组中的第一个元素即可\\n\\n  def remove(self, k):\\n    '''移除第 k 个节点'''\\n    self.swap(k, len(self.heap) - 1) # 将第 k 个元素交换到尾部\\n    ret = self.heap.pop() # 将其从数组中删除\\n    self.up(k) # 确认是否需要向上调整\\n    self.down(k) # 确认是否需要向下调整\\n    return ret\\n\\n  def pop(self):\\n    '''弹出堆顶'''\\n    return self.remove(0)\\n
\\n

掌握了二叉堆,我们可以很轻松地解决 TOP-k 问题,以及所有需要使用到优先队列的问题,不过值得一提的是,二叉堆作为一种非常常用的数据结构,已经被内置到很多语言的官方库中,在实际使用或者写算法题时,可以直接调包使用,比如 Python3 的 queue.PriorityQueueheapq,以及 C++ 的 priority_queue 等。

\\n

线段树和树状数组

\\n

线段树和树状数组非常像,可以说树状数组就是线段树的一种简化形式,在应对单点修改的区间问题时,树状数组更为简洁好用,但由于使用了 low-bit 技巧,相对来说并没有线段树容易理解,所以本文就先从线段树的原理讲起,再逐步扩展到树状数组。

\\n

首先,我们用一个例题来作为切入概念:

\\n
\\n

来源:303. 区域和检索 - 数组不可变\\n给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。

\\n
\\n

下面是一些示例,给定 nums = [-2, 0, 3, -5, 2, -1],那么对任意的 (i, j) 进行求和:

\\n
sumRange(0, 2) -> 1\\nsumRange(2, 5) -> -1\\nsumRange(0, 5) -> -3\\n
\\n

我们可以很容易地想到 o(N)o(N)o(N) 的解法,直接对区间 [i...j][i ... j][i...j] 的数进行求和即可,但是当查询次数很大时,很容易就会超时,而如果我们使用前缀和数组,就可以轻松地在 o(1)o(1)o(1) 时间内完成任何查询操作,所谓的前缀和数组,就是把前 iii 位的数字加起来作为第 iii 位的元素值,即 s[i]=j=0iais[i] = \\\\sum_{j=0}^i a_is[i]=j=0iai,代码表示如下:

\\n
def solve(nums):\\n  s = [0] + nums\\n  for i in range(1, len(nums) + 1):\\n    s[i] += s[i - 1]\\n
\\n

对于本例,可以算出前缀和数组,为了方便计算,我们往数组前部插入一个零,有了前缀数组,我们就可以使用 sum(i,j)=s[j+1]s[i]sum(i, j) = s[j + 1] - s[i]sum(i,j)=s[j+1]s[i] 来计算任意区间的和了:

\\n
s = [0, -2, -2, 1, -4, -2, -3]\\nsumRange(0, 2) = s[3] - s[0] = 1 - 0 = 1\\nsumRange(2, 5) = s[6] - s[2] = -3 - (-2) = -1\\n
\\n

可以看到,数组的区间信息可以压缩成更紧凑的形式。对于此例题中的静态数组,前缀和应对起来绰绰有余,但是如果在查询过程中数组的数据发生了变化,我们就不得不在每次变化的时候花上 o(N)o(N)o(N) 的 时间开销去更新前缀数组,比如下面这道例题。

\\n
\\n

来源:307. 区域和检索 - 数组可修改\\n给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。\\nupdate(i, val) 函数可以通过将下标为 i 的数值更新为 val,从而对数列进行修改。

\\n
\\n

前缀和的本质是维护区间信息,但在应对可变数组时的灵活性太差,所以我们使用更强大的线段树来解决这个问题。

\\n

\\"image\\"

\\n

上图表示了线段树的结构,线段树本质上是一个二叉树,它通过不断地把数组二分的方式来从根节点向叶子节点扩展,每个节点都维护了一个区间内的数组信息,这个数组信息可以是区间内的数组和以及最大最小值。

\\n
class Node:\\n    def __init__(self, l, r):\\n        self.l = l\\n        self.r = r\\n        self.lchild = None\\n        self.rchild = None\\n        self.val = 0\\n\\nclass NumArray:\\n\\n    def __init__(self, nums: List[int]):\\n        self.nums = nums\\n        self.tree = self.build(0, len(nums) - 1)\\n \\n    def build(self, l, r):\\n        if l > r: return None\\n        node = Node(l, r)\\n        if l == r:\\n            node.val = self.nums[l]\\n            return node\\n        m = (l + r) >> 1\\n        node.lchild = self.build(l, m)\\n        node.rchild = self.build(m + 1, r)\\n        node.val = node.lchild.val + node.rchild.val\\n        return node\\n\\n    def update(self, i: int, val: int) -> None:\\n        d = val - self.nums[i]\\n        self.nums[i] = val\\n        q = [self.tree]\\n        while q:\\n            node = q.pop()\\n            if i >= node.l and i <= node.r: node.val += d\\n            else: continue\\n            for child in [node.lchild, node.rchild]:\\n                if child: q.append(child)\\n\\n    def sumRange(self, i: int, j: int) -> int:\\n        ret = 0\\n        q = [self.tree]\\n        while q:\\n            node = q.pop()\\n            if not node: continue\\n            if node.l >= i and node.r <= j:\\n                ret += node.val\\n            elif node.l < j:\\n                q.append(node.lchild)\\n            elif node.r > i:\\n                q.append(node.rchild)\\n        return ret\\n
\\n

[未完待续]

\\n"},"29":{"id":29,"date":"2020/05/24","author":"Yidadaa","title":"随机采样一致性与特征图匹配","mdContent":"Rocco[1] 等人在其弱监督语义级别图像匹配的工作中,将特征匹配与随机采样一致性算法(RANdom SAmple Consensus, RANSAC)联系在一起,提出了一个可微分的基于语义的评分损失函数,文中对于语义特征匹配和 RANSAC 算法的阐述令人耳目一新,遂作此文对相关概念追本溯源。\\r\\n\\r\\n## 随机采样一致性(RANSAC)\\r\\n真实世界的数据往往充满各种各样的噪声,如果想得到足够鲁棒的适用于所有场景的模型,就要尽量降低噪声的影响。由 Fischler[3] 等人提出的随机采样一致性算法,可以很好从含有大量离群点和噪音地数据中恢复出足够准确地模型参数。相对于在统计学领域中大量使用的 M 估计(M-estimators)和最小均方回归(Least Median Square Regression)等稳健估计方法(Robust Estimation),RANSAC 则主要应用于计算机视觉领域,而且从 Fichler[3] 的论文就可以看出,RANSAC 算法从诞生之初便和图像匹配存在不解之缘。\\r\\n\\r\\n简略来看,RANSAC 从所有数据中采样尽量少的观测点来估计模型参数,而不是像其他依赖于从尽可能多的数据中过滤掉离群点,RANSCAC 使用一组足够少的点来初始化算法,然后不断地使用迭代式方法来增大具有**数据一致性**的可信点规模。\\r\\n\\r\\n算法的流程可以简要地描述如下:\\r\\n1. 从数据样本 $S$ 中**随机**选取 $K$ 个点,得到假设子集 $S_K$,然后使用 $S_K$ 优化模型参数 $M(\\\\theta)$,得到初始模型;\\r\\n2. 对所有数据样本 $S$ 应用模型 $M(\\\\theta)$,然后根据每个数据样本的误差 $||M(\\\\theta, x_i) - y_i||$ 来所有数据样本划分为内群点(inliner points)和离群点(outliner points),其中误差大于超参数 $\\\\epsilon$ 的称为离群点 $S_{outliner}$,反正则为内群点 $S_{inliner}$;\\r\\n3. 如果内群点的占比 $R = \\\\frac{S_{inliner}}{S}$ 大于超参数阈值 $\\\\tau$,则使用所有的内群点 $S_{inliner}$ 来重新优化模型参数,并将得到的模型作为最终模型,终止迭代;\\r\\n4. 否则使用内群点 $S_{inliner}$ 优化模型,并重复执行步骤 2,直到条件 3 满足,或者达到迭代次数上限 $N$。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 1 RANSAC 迭代过程演示

\\r\\n\\r\\n\\r\\n\\r\\n可以看到 RANSAC 算法的性能由超参数 $N, K, \\\\epsilon, \\\\tau$ 来决定,通常来讲,迭代次数 $N$ 必须设置的足够大,才能使得至少有一次数据采样 $S_K$ 不包含任何离群点的概率 $p$ (一般来说 $p = 0.99$)满足我们的使用要求,以此来避免离群点的影响,我们可以通过简单地推导来得到迭代上限的近似估计。\\r\\n\\r\\n假设每次随机选择一个点,该点为内群点的概率为 $u$,则其为离群点的概率为 $v = 1 - u$,则在 $N$ 次迭代,每次迭代选取 $K$ 个点的情况下,有\\r\\n$$1 - p = (1 - u^K)^N$$\\r\\n从而估计出\\r\\n$$N = \\\\frac{log(1 - p)}{log(1 - (1 - v)^K)}$$\\r\\n\\r\\n举个粒子,假设数据总量 $1000$,有大概 $100$ 个离群点,则有 $u = 0.9, v = 0.1$,每次迭代选取 $K = 200$ 个点来估计参数,则迭代上限应该为 $N = log(1 - 0.99) \\\\div log(1 - 0.9^{200}) \\\\approx 21076$ 次。\\r\\n\\r\\n此外,RANSAC 算法还有很多变种,比如在估计离群点时应用极大似然估计的 MLESAC[4],每次迭代采样时使用权重采样的 IMPSAC[5]。\\r\\n\\r\\n## 图像特征匹配(Image Feature Matching)\\r\\n图像特征匹配是计算机视觉中的经典任务,无论是做图像检索、三维重建,还是做相机重定位或者 SLAM,都需要先从图像中提取出匹配好的特征点对。以两幅图像为例,图像特征匹配一般包含两个阶段[6]:\\r\\n\\r\\n1. 从图像对 $(I_s, I_t)$ 中分别提取特征点 $(F_s, F_t)$;\\r\\n2. 对两个特征点集进行匹配,得到匹配完毕的特征对 $M_{s \\\\rightarrow t}$。\\r\\n\\r\\n其中第一步提取的特征点,可以是经典的手动构造的特征描述子 SIFT、SURF 或者 ORB 等,也可以是由神经网络端到端地生成特征描述子,比如 LIFT[7]、DELF[8] 以及 D2-Net[9] 等。然后对得到的特征点进行特征匹配,常见的策略有 opencv 自带的 Brute-Force 暴力匹配和快速近似 k 近邻匹配[10]、朴素最近邻匹配、双向最近邻(或称 cycle consistent)等匹配方法。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 2 图像匹配的一般流程[11]

\\r\\n\\r\\n在进行特征点匹配时,为了保证匹配的鲁棒性,需要尽可能多得降低离群点以及错误匹配的影响,此时就要结合前文中提到的 RANSAC 算法过滤掉离群点。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n\\r\\n

图 3 使用 RANSAC 过滤离群点。A 和 B 中的红色点表示在另一幅图中不存在对应的特征点,蓝色点表示存在对应的特征点,但是没有得到完美匹配,黄色的点表示匹配成功。

\\r\\n\\r\\n考虑同一个场景下的图像匹配,如图 3 所示,两张图片在同一场景的不同视角进行拍摄,因此我们如果想要将两张图片对齐,就要求解出两个视角的单应矩阵 $H$,由于我们选用的特征描述子不可能总是完美地表示原始图片中的视觉特征,所以我们在使用汉明距离或者 $L_2$ 距离 $D$ 应用 k 紧邻或者暴力匹配算法时,总是会产生错误匹配,也就是图 3 中的蓝色点,那么此时在所有特征点对 $M_{s \\\\rightarrow t}$ 中,蓝色的匹配就是离群点,我们的目标便是使用 RANSAC 算法来降低这些离群点的影响[12]:\\r\\n1. 从 $M_{s \\\\rightarrow t}$ 中随机选取 $k$ 个特征点对 $M_k$;\\r\\n2. 使用 $M_k$ 求解单应矩阵 $H$;\\r\\n3. 对图像 $I_s$ 中的所有特征点 $F_s$ 应用单应矩阵 $H$,将其投影到图像 $I_s$ 的成像坐标系中,得到投影后的特征点 $\\\\hat F_s = Project(H, F_s)$;\\r\\n4. 根据预设定阈值 $\\\\epsilon$ 计算内群点,即满足 $D(\\\\hat{F_s}, M_{s \\\\rightarrow t}F_s) \\\\leq \\\\epsilon$ 的匹配;\\r\\n5. 如果内群点占比不满足条件,则重复上述步骤 2 - 4;否则使用当前的所有内群点对重新计算 $H$ 作为最终结果。\\r\\n\\r\\n结合了 RANSAC 的改进算法和各种改良后的特征描述子的特征匹配算法的性能已经足够良好[6],并且能够稳定地胜任 SLAM、三维重建和位姿估计等视觉任务。然而上述算法却无法直接应用到神经网络中,这是因为每次采样选点的过程都是不可微分的,这与梯度下降法的使用场景相悖。此外,经典的视觉特征描述子也很难描述出图像中物体的语义特征,因此,可微分的 RANSAC 算法应运而生,而随着深度学习的兴起,语义层面的特征匹配方法也层出不穷。\\r\\n\\r\\n## 语义特征匹配\\r\\n前文提到,在大多数计算机视觉任务中,我们只需要找到同一个场景中在不同视角拍摄的图片之间的响应或者匹配,就可以完成三维重建或者相机重定位等任务,然而有时候我们需要对不同场景的图像进行语义级别的匹配,比如当场景中存在动态物体时,传统匹配方法无法灵活处理动态部分的场景,这时候就需要我们使用语义级别的特征匹配方法。\\r\\n

\\r\\n \\r\\n

\\r\\n

图 4 语义级别的图像匹配[13]

\\r\\n\\r\\n为了解决传统特征描述子的描述能力受限的问题,人们首先考虑从深度学习学到的特征开始入手,使用各种各样的 CNN 学习到的特征描述子进行特征匹配[14],比如使用物体级别的框体匹配(learned at the object-proposal level)的 SC-NET [15][16],以及直接使用手工标注的特征响应的有监督的方法 [17]。\\r\\n\\r\\n首个弱监督的端到端的语义特征匹配方法是 Rocco 的这篇文章[1],作者声称从经典的 RANSAC 算法中得到启发,直接从特征相关矩阵中学习特征相似度较大的区域,从而学习到密集的特征响应信号,同时为了使得整个流程可以微分,使用了 Spatial Transformer Networks 中的图像变换模型来执行图像采样。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 5 Rocco[1] 提出的端到端的特征特征匹配流程

\\r\\n\\r\\n前文提到,图像匹配的流程一般包含两个步骤,首先是特征提取,其次是特征匹配。Rocco[1] 的 pipeline 使用一个参数共享的孪生 CNN 来提取原始图像的特征图,从而得到图像对的特征图 $(F_s, F_t)$,然后对于特征匹配部分,则使用一个图像变换参数估计模型 $G$ 来学习 $F_s \\\\rightarrow F_t$ 的仿射变换矩阵 $T$ 的参数 $g$,这也是整个流程的核心所在,那么如何得到图像变换参数估计模型 $G$ 的监督信号呢?我们还要先从特征图的相关矩阵(correlation map)说起,相关矩阵也是计算机视觉中注意力机制[19][20]的基础,对特征图 $(F_s, F_t)$ 计算相关矩阵:\\r\\n\\r\\n$$\\r\\nV_s = Flatten(F_s), V_t = Flatten(F_t); R^{h \\\\times w \\\\times c} \\\\rightarrow R^{hw \\\\times c}\\r\\n$$\\r\\n\\r\\n$$C = Softmax(V_sV_t^T); R^{hw \\\\times c} \\\\times R^{c \\\\times hw} \\\\rightarrow R^{hw \\\\times hw} $$\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 6 相关性矩阵的计算

\\r\\n\\r\\n直观上理解相关性矩阵的计算,就是用特征图中位于 $(i, j)$ 处的长度为 $C$ 的向量 $F_{(i, j)}$ 来描述位置 $(i, j)$ 处的特征,先将维度为 $h\\\\times w \\\\times c$ 的特征图重整(reshape)为维度为 $hw \\\\times c$ 的向量矩阵,然后使用向量乘法与 Softmax 来计算一个相似性度量,可以看到这个相似性度量与余弦距离在形式上较为接近,这样可以直观地将相关矩阵 $C$ 理解为描述两张特征图中任意两个位置的特征向量之间的相似性。\\r\\n\\r\\n可以发现,特征图 $F_s$ 与 $F_t$ 的相似性越高,相关矩阵 $C$ 的对角线上的相似性得分就越高,回顾图 1 中我们用直线拟合来介绍 RANSAC 的例子,可以发现这两个问题有着形式上的相似性,自然而然地,我们便可以使用相关矩阵 $C$ 对角线上的得分来作为两张特征图的相似性度量得分,而我们想要学习得到的图像变换模型 $G$ 的目标函数就可以描述为 $L_g = Score(C(F_s · G_\\\\theta (F_s, F_t), F_t))$。\\r\\n\\r\\n为了增加鲁棒性,不仅考虑 $C$ 的对角线上的得分,而且考虑对角线周边的区域得分,所以 Rocco[1] 来使用一个以对角线为中心的掩层(mask)来框选得分区域,就是图 5 中的 $m$。\\r\\n\\r\\n

\\r\\n \\r\\n

\\r\\n

图 7 学习图像变换模型的过程就是 RANSAC 采样的过程

\\r\\n\\r\\n如果我们把相关矩阵 $C$ 中掩层区域外的值看作离群点,掩层内的值看错内群点,而图像变换参数估计模型 $G$ 估计出的参数则用来对 $F_s$ 进行采样,可以发现,优化目标函数 $L_g$ 的过程其实与前文中提到的 RANSAC 过程是一致的,我们使用掩层内的值不断地优化模型 $G$,就相当于 RANSAC 中使用内群点不断地优化模型参数,从而学习到更加鲁棒的模型。\\r\\n\\r\\n到此为止,图像变换矩阵 $G$ 便描述了特征图 $F_s$ 与 $F_t$ 之间的特征响应,而且得益于 CNN 的感受野,这种特征响应也描述了两张图像在语义层面上的相关性。\\r\\n\\r\\n## 扩展应用\\r\\n相较于直接使用 $L_1$ 或者 $L_2$ 等误差函数,前文提到的 $L_g$ 可以更好得描述特征图间得语义相关性,这使得其可以在图像匹配之外的场景中应用,比如 Wang[21] 就将其应用在挖掘视频帧间响应的任务中,以无监督的形式在多种视觉任务上得到了与监督学习匹敌的性能数据。其他诸如在动态场景的任务,如 SLAM、视频深度估计或者三维重建中也有较大的应用潜力。\\r\\n\\r\\n## 参考资料\\r\\n1. Rocco, Ignacio, Relja Arandjelović, and Josef Sivic. \\"End-to-end weakly-supervised semantic alignment.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2018.\\r\\n2. Derpanis, Konstantinos G. \\"Overview of the RANSAC Algorithm.\\" Image Rochester NY 4.1 (2010): 2-3.\\r\\n3. Fischler, Martin A., and Robert C. Bolles. \\"Random sample consensus: a paradigm for model fitting with applications to image analysis and automated cartography.\\" Communications of the ACM 24.6 (1981): 381-395.Communications of the ACM, 24(6):381–395, 1981.\\r\\n4. Torr, Philip HS, and Andrew Zisserman. \\"MLESAC: A new robust estimator with application to estimating image geometry.\\" Computer vision and image understanding 78.1 (2000): 138-156.\\r\\n5. Torr, Philip H. S., and Colin Davidson. \\"IMPSAC: Synthesis of importance sampling and random sample consensus.\\" IEEE Transactions on Pattern Analysis and Machine Intelligence 25.3 (2003): 354-364.\\r\\n6. Jin, Yuhe, et al. \\"Image Matching across Wide Baselines: From Paper to Practice.\\" arXiv preprint arXiv:2003.01587 (2020).\\r\\n7. Yi, Kwang Moo, et al. \\"Lift: Learned invariant feature transform.\\" European Conference on Computer Vision. Springer, Cham, 2016.\\r\\n8. Noh, Hyeonwoo, et al. \\"Large-scale image retrieval with attentive deep local features.\\" Proceedings of the IEEE international conference on computer vision. 2017.\\r\\n9. Dusmanu, Mihai, et al. \\"D2-Net: A Trainable CNN for Joint Description and Detection of Local Features.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.\\r\\n10. “Feature Matching.” OpenCV, opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_feature2d/py_matcher/py_matcher.html.\\r\\n11. http://cmp.felk.cvut.cz/~mishkdmy/slides/EECVC2019_Mishkin_image_matching.pdf\\r\\n12. https://people.cs.umass.edu/~elm/Teaching/ppt/370/370_10_RANSAC.pptx.pdf\\r\\n13. Chen, Yun-Chun, et al. \\"Deep semantic matching with foreground detection and cycle-consistency.\\" Asian Conference on Computer Vision. Springer, Cham, 2018.\\r\\n14. Ufer, Nikolai, and Bjorn Ommer. \\"Deep semantic feature matching.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.\\r\\n15. Han, Kai, et al. \\"Scnet: Learning semantic correspondence.\\" Proceedings of the IEEE International Conference on Computer Vision. 2017.\\r\\n16. Ufer, Nikolai, and Bjorn Ommer. \\"Deep semantic feature matching.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.\\r\\n17. Rocco, Ignacio, Relja Arandjelovic, and Josef Sivic. \\"Convolutional neural network architecture for geometric matching.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.\\r\\n18. Jaderberg, Max, Karen Simonyan, and Andrew Zisserman. \\"Spatial transformer networks.\\" Advances in neural information processing systems. 2015.\\r\\n19. Vaswani, Ashish, et al. \\"Attention is all you need.\\" Advances in neural information processing systems. 2017.\\r\\n20. Wang, Xiaolong, et al. \\"Non-local neural networks.\\" Proceedings of the IEEE conference on computer vision and pattern recognition. 2018.\\r\\n21. Wang, Xiaolong, Allan Jabri, and Alexei A. Efros. \\"Learning correspondence from the cycle-consistency of time.\\" Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.","content":"

Rocco[1] 等人在其弱监督语义级别图像匹配的工作中,将特征匹配与随机采样一致性算法(RANdom SAmple Consensus, RANSAC)联系在一起,提出了一个可微分的基于语义的评分损失函数,文中对于语义特征匹配和 RANSAC 算法的阐述令人耳目一新,遂作此文对相关概念追本溯源。

\\n

随机采样一致性(RANSAC)

\\n

真实世界的数据往往充满各种各样的噪声,如果想得到足够鲁棒的适用于所有场景的模型,就要尽量降低噪声的影响。由 Fischler[3] 等人提出的随机采样一致性算法,可以很好从含有大量离群点和噪音地数据中恢复出足够准确地模型参数。相对于在统计学领域中大量使用的 M 估计(M-estimators)和最小均方回归(Least Median Square Regression)等稳健估计方法(Robust Estimation),RANSAC 则主要应用于计算机视觉领域,而且从 Fichler[3] 的论文就可以看出,RANSAC 算法从诞生之初便和图像匹配存在不解之缘。

\\n

简略来看,RANSAC 从所有数据中采样尽量少的观测点来估计模型参数,而不是像其他依赖于从尽可能多的数据中过滤掉离群点,RANSCAC 使用一组足够少的点来初始化算法,然后不断地使用迭代式方法来增大具有数据一致性的可信点规模。

\\n

算法的流程可以简要地描述如下:

\\n
    \\n
  1. 从数据样本 SSS随机选取 KKK 个点,得到假设子集 SKS_KSK,然后使用 SKS_KSK 优化模型参数 M(θ)M(\\\\theta)M(θ),得到初始模型;
  2. \\n
  3. 对所有数据样本 SSS 应用模型 M(θ)M(\\\\theta)M(θ),然后根据每个数据样本的误差 M(θ,xi)yi||M(\\\\theta, x_i) - y_i||M(θ,xi)yi 来所有数据样本划分为内群点(inliner points)和离群点(outliner points),其中误差大于超参数 ϵ\\\\epsilonϵ 的称为离群点 SoutlinerS_{outliner}Soutliner,反正则为内群点 SinlinerS_{inliner}Sinliner
  4. \\n
  5. 如果内群点的占比 R=SinlinerSR = \\\\frac{S_{inliner}}{S}R=SSinliner 大于超参数阈值 τ\\\\tauτ,则使用所有的内群点 SinlinerS_{inliner}Sinliner 来重新优化模型参数,并将得到的模型作为最终模型,终止迭代;
  6. \\n
  7. 否则使用内群点 SinlinerS_{inliner}Sinliner 优化模型,并重复执行步骤 2,直到条件 3 满足,或者达到迭代次数上限 NNN
  8. \\n
\\n

\\n \\n

\\n

图 1 RANSAC 迭代过程演示

\\n

可以看到 RANSAC 算法的性能由超参数 N,K,ϵ,τN, K, \\\\epsilon, \\\\tauN,K,ϵ,τ 来决定,通常来讲,迭代次数 NNN 必须设置的足够大,才能使得至少有一次数据采样 SKS_KSK 不包含任何离群点的概率 ppp (一般来说 p=0.99p = 0.99p=0.99)满足我们的使用要求,以此来避免离群点的影响,我们可以通过简单地推导来得到迭代上限的近似估计。

\\n

假设每次随机选择一个点,该点为内群点的概率为 uuu,则其为离群点的概率为 v=1uv = 1 - uv=1u,则在 NNN 次迭代,每次迭代选取 KKK 个点的情况下,有

\\n

1p=(1uK)N1 - p = (1 - u^K)^N\\n1p=(1uK)N

\\n

从而估计出

\\n

N=log(1p)log(1(1v)K)N = \\\\frac{log(1 - p)}{log(1 - (1 - v)^K)}\\nN=log(1(1v)K)log(1p)

\\n

举个粒子,假设数据总量 100010001000,有大概 100100100 个离群点,则有 u=0.9,v=0.1u = 0.9, v = 0.1u=0.9,v=0.1,每次迭代选取 K=200K = 200K=200 个点来估计参数,则迭代上限应该为 N=log(10.99)÷log(10.9200)21076N = log(1 - 0.99) \\\\div log(1 - 0.9^{200}) \\\\approx 21076N=log(10.99)÷log(10.9200)21076 次。

\\n

此外,RANSAC 算法还有很多变种,比如在估计离群点时应用极大似然估计的 MLESAC[4],每次迭代采样时使用权重采样的 IMPSAC[5]。

\\n

图像特征匹配(Image Feature Matching)

\\n

图像特征匹配是计算机视觉中的经典任务,无论是做图像检索、三维重建,还是做相机重定位或者 SLAM,都需要先从图像中提取出匹配好的特征点对。以两幅图像为例,图像特征匹配一般包含两个阶段[6]:

\\n
    \\n
  1. 从图像对 (Is,It)(I_s, I_t)(Is,It) 中分别提取特征点 (Fs,Ft)(F_s, F_t)(Fs,Ft)
  2. \\n
  3. 对两个特征点集进行匹配,得到匹配完毕的特征对 MstM_{s \\\\rightarrow t}Mst
  4. \\n
\\n

其中第一步提取的特征点,可以是经典的手动构造的特征描述子 SIFT、SURF 或者 ORB 等,也可以是由神经网络端到端地生成特征描述子,比如 LIFT[7]、DELF[8] 以及 D2-Net[9] 等。然后对得到的特征点进行特征匹配,常见的策略有 opencv 自带的 Brute-Force 暴力匹配和快速近似 k 近邻匹配[10]、朴素最近邻匹配、双向最近邻(或称 cycle consistent)等匹配方法。

\\n

\\n \\n

\\n

图 2 图像匹配的一般流程[11]

\\n

在进行特征点匹配时,为了保证匹配的鲁棒性,需要尽可能多得降低离群点以及错误匹配的影响,此时就要结合前文中提到的 RANSAC 算法过滤掉离群点。

\\n

\\n \\n

\\n

图 3 使用 RANSAC 过滤离群点。A 和 B 中的红色点表示在另一幅图中不存在对应的特征点,蓝色点表示存在对应的特征点,但是没有得到完美匹配,黄色的点表示匹配成功。

\\n

考虑同一个场景下的图像匹配,如图 3 所示,两张图片在同一场景的不同视角进行拍摄,因此我们如果想要将两张图片对齐,就要求解出两个视角的单应矩阵 HHH,由于我们选用的特征描述子不可能总是完美地表示原始图片中的视觉特征,所以我们在使用汉明距离或者 L2L_2L2 距离 DDD 应用 k 紧邻或者暴力匹配算法时,总是会产生错误匹配,也就是图 3 中的蓝色点,那么此时在所有特征点对 MstM_{s \\\\rightarrow t}Mst 中,蓝色的匹配就是离群点,我们的目标便是使用 RANSAC 算法来降低这些离群点的影响[12]:

\\n
    \\n
  1. MstM_{s \\\\rightarrow t}Mst 中随机选取 kkk 个特征点对 MkM_kMk
  2. \\n
  3. 使用 MkM_kMk 求解单应矩阵 HHH
  4. \\n
  5. 对图像 IsI_sIs 中的所有特征点 FsF_sFs 应用单应矩阵 HHH,将其投影到图像 IsI_sIs 的成像坐标系中,得到投影后的特征点 F^s=Project(H,Fs)\\\\hat F_s = Project(H, F_s)F^s=Project(H,Fs)
  6. \\n
  7. 根据预设定阈值 ϵ\\\\epsilonϵ 计算内群点,即满足 D(Fs^,MstFs)ϵD(\\\\hat{F_s}, M_{s \\\\rightarrow t}F_s) \\\\leq \\\\epsilonD(Fs^,MstFs)ϵ 的匹配;
  8. \\n
  9. 如果内群点占比不满足条件,则重复上述步骤 2 - 4;否则使用当前的所有内群点对重新计算 HHH 作为最终结果。
  10. \\n
\\n

结合了 RANSAC 的改进算法和各种改良后的特征描述子的特征匹配算法的性能已经足够良好[6],并且能够稳定地胜任 SLAM、三维重建和位姿估计等视觉任务。然而上述算法却无法直接应用到神经网络中,这是因为每次采样选点的过程都是不可微分的,这与梯度下降法的使用场景相悖。此外,经典的视觉特征描述子也很难描述出图像中物体的语义特征,因此,可微分的 RANSAC 算法应运而生,而随着深度学习的兴起,语义层面的特征匹配方法也层出不穷。

\\n

语义特征匹配

\\n

前文提到,在大多数计算机视觉任务中,我们只需要找到同一个场景中在不同视角拍摄的图片之间的响应或者匹配,就可以完成三维重建或者相机重定位等任务,然而有时候我们需要对不同场景的图像进行语义级别的匹配,比如当场景中存在动态物体时,传统匹配方法无法灵活处理动态部分的场景,这时候就需要我们使用语义级别的特征匹配方法。

\\n

\\n \\n

\\n

图 4 语义级别的图像匹配[13]

\\n

为了解决传统特征描述子的描述能力受限的问题,人们首先考虑从深度学习学到的特征开始入手,使用各种各样的 CNN 学习到的特征描述子进行特征匹配[14],比如使用物体级别的框体匹配(learned at the object-proposal level)的 SC-NET [15][16],以及直接使用手工标注的特征响应的有监督的方法 [17]。

\\n

首个弱监督的端到端的语义特征匹配方法是 Rocco 的这篇文章[1],作者声称从经典的 RANSAC 算法中得到启发,直接从特征相关矩阵中学习特征相似度较大的区域,从而学习到密集的特征响应信号,同时为了使得整个流程可以微分,使用了 Spatial Transformer Networks 中的图像变换模型来执行图像采样。

\\n

\\n \\n

\\n

图 5 Rocco[1] 提出的端到端的特征特征匹配流程

\\n

前文提到,图像匹配的流程一般包含两个步骤,首先是特征提取,其次是特征匹配。Rocco[1] 的 pipeline 使用一个参数共享的孪生 CNN 来提取原始图像的特征图,从而得到图像对的特征图 (Fs,Ft)(F_s, F_t)(Fs,Ft),然后对于特征匹配部分,则使用一个图像变换参数估计模型 GGG 来学习 FsFtF_s \\\\rightarrow F_tFsFt 的仿射变换矩阵 TTT 的参数 ggg,这也是整个流程的核心所在,那么如何得到图像变换参数估计模型 GGG 的监督信号呢?我们还要先从特征图的相关矩阵(correlation map)说起,相关矩阵也是计算机视觉中注意力机制[19][20]的基础,对特征图 (Fs,Ft)(F_s, F_t)(Fs,Ft) 计算相关矩阵:

\\n

Vs=Flatten(Fs),Vt=Flatten(Ft);Rh×w×cRhw×cV_s = Flatten(F_s), V_t = Flatten(F_t); R^{h \\\\times w \\\\times c} \\\\rightarrow R^{hw \\\\times c}\\nVs=Flatten(Fs),Vt=Flatten(Ft);Rh×w×cRhw×c

\\n

C=Softmax(VsVtT);Rhw×c×Rc×hwRhw×hwC = Softmax(V_sV_t^T); R^{hw \\\\times c} \\\\times R^{c \\\\times hw} \\\\rightarrow R^{hw \\\\times hw} \\nC=Softmax(VsVtT);Rhw×c×Rc×hwRhw×hw

\\n

\\n \\n

\\n

图 6 相关性矩阵的计算

\\n

直观上理解相关性矩阵的计算,就是用特征图中位于 (i,j)(i, j)(i,j) 处的长度为 CCC 的向量 F(i,j)F_{(i, j)}F(i,j) 来描述位置 (i,j)(i, j)(i,j) 处的特征,先将维度为 h×w×ch\\\\times w \\\\times ch×w×c 的特征图重整(reshape)为维度为 hw×chw \\\\times chw×c 的向量矩阵,然后使用向量乘法与 Softmax 来计算一个相似性度量,可以看到这个相似性度量与余弦距离在形式上较为接近,这样可以直观地将相关矩阵 CCC 理解为描述两张特征图中任意两个位置的特征向量之间的相似性。

\\n

可以发现,特征图 FsF_sFsFtF_tFt 的相似性越高,相关矩阵 CCC 的对角线上的相似性得分就越高,回顾图 1 中我们用直线拟合来介绍 RANSAC 的例子,可以发现这两个问题有着形式上的相似性,自然而然地,我们便可以使用相关矩阵 CCC 对角线上的得分来作为两张特征图的相似性度量得分,而我们想要学习得到的图像变换模型 GGG 的目标函数就可以描述为 Lg=Score(C(FsGθ(Fs,Ft),Ft))L_g = Score(C(F_s · G_\\\\theta (F_s, F_t), F_t))Lg=Score(C(FsGθ(Fs,Ft),Ft))

\\n

为了增加鲁棒性,不仅考虑 CCC 的对角线上的得分,而且考虑对角线周边的区域得分,所以 Rocco[1] 来使用一个以对角线为中心的掩层(mask)来框选得分区域,就是图 5 中的 mmm

\\n

\\n \\n

\\n

图 7 学习图像变换模型的过程就是 RANSAC 采样的过程

\\n

如果我们把相关矩阵 CCC 中掩层区域外的值看作离群点,掩层内的值看错内群点,而图像变换参数估计模型 GGG 估计出的参数则用来对 FsF_sFs 进行采样,可以发现,优化目标函数 LgL_gLg 的过程其实与前文中提到的 RANSAC 过程是一致的,我们使用掩层内的值不断地优化模型 GGG,就相当于 RANSAC 中使用内群点不断地优化模型参数,从而学习到更加鲁棒的模型。

\\n

到此为止,图像变换矩阵 GGG 便描述了特征图 FsF_sFsFtF_tFt 之间的特征响应,而且得益于 CNN 的感受野,这种特征响应也描述了两张图像在语义层面上的相关性。

\\n

扩展应用

\\n

相较于直接使用 L1L_1L1 或者 L2L_2L2 等误差函数,前文提到的 LgL_gLg 可以更好得描述特征图间得语义相关性,这使得其可以在图像匹配之外的场景中应用,比如 Wang[21] 就将其应用在挖掘视频帧间响应的任务中,以无监督的形式在多种视觉任务上得到了与监督学习匹敌的性能数据。其他诸如在动态场景的任务,如 SLAM、视频深度估计或者三维重建中也有较大的应用潜力。

\\n

参考资料

\\n
    \\n
  1. Rocco, Ignacio, Relja Arandjelović, and Josef Sivic. "End-to-end weakly-supervised semantic alignment." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2018.
  2. \\n
  3. Derpanis, Konstantinos G. "Overview of the RANSAC Algorithm." Image Rochester NY 4.1 (2010): 2-3.
  4. \\n
  5. Fischler, Martin A., and Robert C. Bolles. "Random sample consensus: a paradigm for model fitting with applications to image analysis and automated cartography." Communications of the ACM 24.6 (1981): 381-395.Communications of the ACM, 24(6):381–395, 1981.
  6. \\n
  7. Torr, Philip HS, and Andrew Zisserman. "MLESAC: A new robust estimator with application to estimating image geometry." Computer vision and image understanding 78.1 (2000): 138-156.
  8. \\n
  9. Torr, Philip H. S., and Colin Davidson. "IMPSAC: Synthesis of importance sampling and random sample consensus." IEEE Transactions on Pattern Analysis and Machine Intelligence 25.3 (2003): 354-364.
  10. \\n
  11. Jin, Yuhe, et al. "Image Matching across Wide Baselines: From Paper to Practice." arXiv preprint arXiv:2003.01587 (2020).
  12. \\n
  13. Yi, Kwang Moo, et al. "Lift: Learned invariant feature transform." European Conference on Computer Vision. Springer, Cham, 2016.
  14. \\n
  15. Noh, Hyeonwoo, et al. "Large-scale image retrieval with attentive deep local features." Proceedings of the IEEE international conference on computer vision. 2017.
  16. \\n
  17. Dusmanu, Mihai, et al. "D2-Net: A Trainable CNN for Joint Description and Detection of Local Features." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.
  18. \\n
  19. “Feature Matching.” OpenCV, opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_feature2d/py_matcher/py_matcher.html.
  20. \\n
  21. http://cmp.felk.cvut.cz/~mishkdmy/slides/EECVC2019_Mishkin_image_matching.pdf
  22. \\n
  23. https://people.cs.umass.edu/~elm/Teaching/ppt/370/370_10_RANSAC.pptx.pdf
  24. \\n
  25. Chen, Yun-Chun, et al. "Deep semantic matching with foreground detection and cycle-consistency." Asian Conference on Computer Vision. Springer, Cham, 2018.
  26. \\n
  27. Ufer, Nikolai, and Bjorn Ommer. "Deep semantic feature matching." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.
  28. \\n
  29. Han, Kai, et al. "Scnet: Learning semantic correspondence." Proceedings of the IEEE International Conference on Computer Vision. 2017.
  30. \\n
  31. Ufer, Nikolai, and Bjorn Ommer. "Deep semantic feature matching." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.
  32. \\n
  33. Rocco, Ignacio, Relja Arandjelovic, and Josef Sivic. "Convolutional neural network architecture for geometric matching." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2017.
  34. \\n
  35. Jaderberg, Max, Karen Simonyan, and Andrew Zisserman. "Spatial transformer networks." Advances in neural information processing systems. 2015.
  36. \\n
  37. Vaswani, Ashish, et al. "Attention is all you need." Advances in neural information processing systems. 2017.
  38. \\n
  39. Wang, Xiaolong, et al. "Non-local neural networks." Proceedings of the IEEE conference on computer vision and pattern recognition. 2018.
  40. \\n
  41. Wang, Xiaolong, Allan Jabri, and Alexei A. Efros. "Learning correspondence from the cycle-consistency of time." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.
  42. \\n
\\n"},"30":{"id":30,"date":"2021/12/26","author":"Yidadaa","title":"Rust 新手错误和最差实践","mdContent":"> 原文:[Common Newbie Mistakes and Bad Practices in Rust: Bad Habits](https://adventures.michaelfbryan.com/posts/rust-best-practices/bad-habits/#using-sentinel-values) \\r\\n> 译者:[Yidadaa](https://github.com/Yidadaa)\\r\\n\\r\\n很多从其他语言过来的 Rust 新手都会不可避免地利用之前的编码经验来写 Rust,这无可厚非,毕竟确实没必要从头开始学习编程知识。但是,这些经验性知识,却极有可能导致你写出来很垃圾的 Rust 代码。\\r\\n\\r\\n## 别再用哨兵值了\\r\\n\\r\\n这可能是我最讨厌的一点。在大多数沿袭 C 语言设计的语言中(C,C#,Java 等),经常使用一个特殊值来表示某个函数执行失败的情况。比如,在 C# 中,用于在字符串中查找另一个字符串的索引位置的函数 `String.IndexOf(t)` 会在找不到 `t` 时返回 `-1`,从而写出这样的 C# 代码:\\r\\n\\r\\n```c#\\r\\nstring sentence = \\"The fox jumps over the dog\\";\\r\\n\\r\\nint index = sentence.IndexOf(\\"fox\\");\\r\\n\\r\\nif (index != -1)\\r\\n{\\r\\n string wordsAfterFox = sentence.SubString(index);\\r\\n Console.WriteLine(wordsAfterFox);\\r\\n}\\r\\n```\\r\\n\\r\\n在其他的语言中,这种用法更是数不胜数,类似的哨兵值还有空字符串 `\\"\\"` 或者 `null`、`None` 之类的空值。\\r\\n\\r\\n既然它这么常用,为什么还要说它很差劲呢?原因就是你极有可能会忘记处理哨兵值所代表的失败情况,然后导致整个程序直接崩溃。\\r\\n\\r\\nRust 则提供了很好的解决方案,那就是 `Option`。`Option` 从设计层面就杜绝了忘记考虑 `None` 时的情况,编译器会在编译时就进行强制检查,如果你忘了处理 `None`,编译器会马上告诉你。上面字符串例子的代码,在 Rust 中可以写成这样:\\r\\n\\r\\n```rust\\r\\nlet sentence = \\"The fox jumps over the dog\\";\\r\\nlet index = sentence.find(\\"fox\\");\\r\\n\\r\\n// let words_after_fox = &sentence[index..];\\r\\n// 如果你直接使用 index,会得到报错:Error: Can\'t index str with Option\\r\\n\\r\\nif let Some(fox) = index {\\r\\n let words_after_fox = &sentence[fox..];\\r\\n println!(\\"{}\\", words_after_fox);\\r\\n}\\r\\n```\\r\\n\\r\\n## 别再用匈牙利命名了\\r\\n\\r\\n上世纪 70 年代,程序员们逐渐开始在无类型或动态类型语言中使用[匈牙利命名法](https://en.wikipedia.org/wiki/Hungarian_notation),他们给变量名加上不同的前缀来表示变量的类型,比如 `bVisited` 表示布尔型的变量 `visited`,`strName` 表示字符串类型的变量 `name`。\\r\\n\\r\\n我们可以在 `Delphi` 语言中见到大量的例子,`T` 开头的表示类(class)或者类型(type),`F` 表示属性值(fields),`A` 表示参数(arguments),诸如此类。\\r\\n\\r\\n```delphi\\r\\ntype\\r\\n TKeyValue = class\\r\\n private\\r\\n FKey: integer;\\r\\n FValue: TObject;\\r\\n public\\r\\n property Key: integer read FKey write FKey;\\r\\n property Value: TObject read FValue write FValue;\\r\\n function Frobnicate(ASomeArg: string): string;\\r\\n end;\\r\\n```\\r\\n\\r\\nC# 中也有类似的使用习惯,比如用 `I` 开头表示一个接口(interface),所以 C# 程序员很可能会写出这种 Rust 代码:\\r\\n\\r\\n```rust\\r\\ntrait IClone {\\r\\n fn clone(&self) -> Self;\\r\\n}\\r\\n```\\r\\n\\r\\n你大可以直接扔掉前面的 `I`,因为 Rust 的语法已经保证了我们很难混淆 `trait` 和 `type`,不像 C# 很容易就分不清 `interface` 和 `class`(译者按:Typescript 中就是 `interface`、`type` 和 `class` 大混战了,狗头.jpg)。\\r\\n\\r\\n此外,你也没有必要在给一些工具函数或者中间变量命名时带上它的类型信息,比如下面的代码:\\r\\n\\r\\n```rust\\r\\nlet account_bytes: Vec = read_some_input();\\r\\nlet account_str = String::from_utf8(account_bytes)?;\\r\\nlet account: Account = account_str.parse()?;\\r\\n```\\r\\n\\r\\n既然 `String.from_utf8()` 已经明明白白地返回了一个字符串,为什么还要在命名时加上 `_str` 后缀呢?\\r\\n\\r\\n与其他语言不同,Rust 语言鼓励程序员在对变量进行一系列变换操作时,使用同名变量覆写掉不再使用的旧值,比如:\\r\\n\\r\\n```rust\\r\\nlet account: Vec = read_some_input();\\r\\nlet account = String::from_utf8(account)?;\\r\\nlet account: Account = account.parse()?;\\r\\n```\\r\\n\\r\\n使用相同的变量名可以很好地保证概念的一致性。\\r\\n\\r\\n有些语言会明令禁止[覆写变量](https://rules.sonarsource.com/cpp/RSPEC-1117),尤其像 Javascript 这种动态类型语言,因为频繁变化的类型,在缺少类型推断的情况下,尤其有可能会导致 bug 出现。\\r\\n\\r\\n## 你可能不需要这么多 `Rc>`\\r\\n\\r\\nOOP 编程实践常常会保存其他对象的引用,并在合适的时候调用他们的函数,这没啥不好的,依赖注入(Dependency Injection)是个蛮不错的实践,不过有别于大多数面向对象的语言,Rust 并没有垃圾内存回收机制(Garbage Collector),并且对共享可变性非常敏感。\\r\\n\\r\\n举个例子,我们正要实现一个打怪兽的游戏,玩家需要对怪物们造成足量伤害才算打败他们(我也不知道为什么要这么设定,可能是接受了什么委托?)。\\r\\n\\r\\n先创建一个 `Monster` 类,包含 `health` 生命值属性以及 `takeDamage()` 遭受伤害的方法,为了能知道怪物遭受了多少伤害,我们允许为 `Monster` 类注入一个回调函数,该回调函数可以接收每次遭受的伤害值。\\r\\n\\r\\n```ts\\r\\ntype OnReceivedDamage = (damageReceived: number) => void;\\r\\n\\r\\nclass Monster {\\r\\n health: number = 50;\\r\\n receivedDamage: OnReceivedDamage[] = [];\\r\\n\\r\\n takeDamage(amount: number) {\\r\\n amount = Math.min(this.health, amount);\\r\\n this.health -= amount;\\r\\n this.receivedDamage.forEach((cb) => cb(amount));\\r\\n }\\r\\n\\r\\n on(event: \\"damaged\\", callback: OnReceivedDamage): void {\\r\\n this.receivedDamage.push(callback);\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n然后设计一个伤害计数类 `DamageCounter`,可以累计怪物伤害值:\\r\\n\\r\\n```ts\\r\\nclass DamageCounter {\\r\\n damageInflicted: number = 0;\\r\\n\\r\\n reachedTargetDamage(): boolean {\\r\\n return this.damageInflicted > 100;\\r\\n }\\r\\n\\r\\n onDamageInflicted(amount: number) {\\r\\n this.damageInflicted += amount;\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n然后我们对怪物造成随机伤害,直到伤害计数达到上限。\\r\\n\\r\\n```ts\\r\\nconst counter = new DamageCounter();\\r\\n\\r\\nconst monsters = [\\r\\n new Monster(),\\r\\n new Monster(),\\r\\n new Monster(),\\r\\n new Monster(),\\r\\n new Monster(),\\r\\n];\\r\\nmonsters.forEach((m) =>\\r\\n m.on(\\"damaged\\", (amount) => counter.onDamageInflicted(amount))\\r\\n);\\r\\n\\r\\nwhile (!counter.reachedTargetDamage()) {\\r\\n // 随机选中怪物\\r\\n const index = Math.floor(Math.random() * monsters.length);\\r\\n const target = monsters[index];\\r\\n // 产生随机伤害\\r\\n const damage = Math.round(Math.random() * 50);\\r\\n target.takeDamage(damage);\\r\\n\\r\\n console.log(`Monster ${index} received ${damage} damage`);\\r\\n}\\r\\n```\\r\\n\\r\\n这里是[在线运行示例](https://www.typescriptlang.org/play?#code/C4TwDgpgBA8gdgJQgYwgSwG4QCYBECGAtvgObQC8UAFIQFxQCyA9nAM7AQBOANFNkaQhJUmHPTgBXQgCMuASijkAfFAxM02ANwAobcgA2+Vq0Yt2XKAG9tUW1AAWEfPuD3xU2Z0VQArAAYdOyhOFHQsPAEyenhhMJwCYjIAbQBdb1SdGztgfABrCATBKiImCThgdxl5Kyyg2xKy4G8GfFcAOkI0OCpXNFY2x2dXXgbyuUC6217+wZd7KABaSlHgTTrauum2kJFwwrI2gDMmTgBRfGR7KmRpRRUbnvs+kcJSsfGNgF9dIJYqCCw5XoACJ+IkcMDeMhnPppBdctFEKFRBFwXJ6GoNDVJlMnv0dnFUYI2mAJKwrtD9LD4R8gt9vnpDMYoPsIABhN4cLzWIJgwQASTgh30aGQHGwlU83gCPzsIQujmwABV8JwyMBWVR0VBpEwmPonHBsTiQsAJJwjVs+WRBcLReKoCoAIx+GV02W2Fis20isU4YqvRqS6o8nFWyIQH32nBQADUy0D5QmtnpumQZia6caFkocAgAHcWRGOdnOFrMum2E1XlWuCZKEk84XmLWy3JeE3TK2tR2C13zG3e82M1we1BOy2B1qUjoawP+sczgqaHcoIQ2n9QRHsJDqAB9F6chTKKBZ8pcDdwb1C33igNHuQfbT5p4G6gAQjPXO2TkuOBVaoQBqEZagooa2AA9BBUBgKKuRQPgwT4HA2BMIQa4jpwGyVuwUBdNgEAAB7NK09hHPoepli07ScMhqGEFqABUc5cv0BpwCQri0nYOFNDkgFNJQLF1kk+FETOGxQVArgQEa1rQGgTSIdIinYRmfARiRNFvNgVDUWRtEoWhWpQIxvh+NxUyquqbQ5PkmryU+QQ4fqEBtBRJBUAABpOXJQAAJJYYmEZ8wTIuEAWWPJoXydQwBMDk+gaeCeE3tGEqRV+F7yVGfrYJ8XkfJ8QA)。\\r\\n\\r\\n然后我们用 Rust 重写上述逻辑,`Monster` 结构体结构保持不变,使用 `Box` 来接收闭包,该闭包接受一个 `u32` 型参数。\\r\\n\\r\\n```rust\\r\\ntype OnReceivedDamage = Box;\\r\\n\\r\\nstruct Monster {\\r\\n health: u32,\\r\\n received_damage: Vec,\\r\\n}\\r\\n\\r\\nimpl Monster {\\r\\n fn take_damage(&mut self, amount: u32) {\\r\\n let damage_received = cmp::min(self.health, amount);\\r\\n self.health -= damage_received;\\r\\n for callback in &mut self.received_damage {\\r\\n callback(damage_received);\\r\\n }\\r\\n }\\r\\n\\r\\n fn add_listener(&mut self, listener: OnReceivedDamage) {\\r\\n self.received_damage.push(listener);\\r\\n }\\r\\n}\\r\\n\\r\\nimpl Default for Monster {\\r\\n fn default() -> Self {\\r\\n Monster { health: 100, received_damage: Vec::new() }\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n随后是 `DamageCounter` 类:\\r\\n\\r\\n```rust\\r\\n#[derive(Default)]\\r\\nstruct DamageCounter {\\r\\n damage_inflicted: u32,\\r\\n}\\r\\n\\r\\nimpl DamageCounter {\\r\\n fn reached_target_damage(&self) -> bool {\\r\\n self.damage_inflicted > 100\\r\\n }\\r\\n\\r\\n fn on_damage_received(&mut self, damage: u32) {\\r\\n self.damage_inflicted += damage;\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n然后开始打怪:\\r\\n\\r\\n```rust\\r\\nfn main() {\\r\\n let mut rng = rand::thread_rng();\\r\\n let mut counter = DamageCounter::default();\\r\\n let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\r\\n\\r\\n for monster in &mut monsters {\\r\\n monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n }\\r\\n\\r\\n while !counter.reached_target_damage() {\\r\\n let index = rng.gen_range(0..monsters.len());\\r\\n let target = &mut monsters[index];\\r\\n\\r\\n let damage = rng.gen_range(0..50);\\r\\n target.take_damage(damage);\\r\\n\\r\\n println!(\\"Monster {} received {} damage\\", index, damage);\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n同样地,Rust 代码也有一个[在线运行示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a8cc547728ef102bbd5dc6b9cafb0ff6)。\\r\\n\\r\\n然而,编译器狠狠地给我们报了四个错误,全都集中在 `monster.add_listener()` 这一行:\\r\\n\\r\\n```rust\\r\\nerror[E0596]: cannot borrow `counter` as mutable, as it is a captured variable in a `Fn` closure\\r\\n --\x3e src/main.rs:47:48\\r\\n |\\r\\n47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n | ^^^^^^^ cannot borrow as mutable\\r\\n\\r\\nerror[E0499]: cannot borrow `counter` as mutable more than once at a time\\r\\n --\x3e src/main.rs:47:39\\r\\n |\\r\\n47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n | ---------^^^^^^^^------------------------------------\\r\\n | | | |\\r\\n | | | borrows occur due to use of `counter` in closure\\r\\n | | `counter` was mutably borrowed here in the previous iteration of the loop\\r\\n | cast requires that `counter` is borrowed for `\'static`\\r\\n\\r\\nerror[E0597]: `counter` does not live long enough\\r\\n --\x3e src/main.rs:47:48\\r\\n |\\r\\n47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n | ------------------^^^^^^^----------------------------\\r\\n | | | |\\r\\n | | | borrowed value does not live long enough\\r\\n | | value captured here\\r\\n | cast requires that `counter` is borrowed for `\'static`\\r\\n...\\r\\n60 | }\\r\\n | - `counter` dropped here while still borrowed\\r\\n\\r\\nerror[E0502]: cannot borrow `counter` as immutable because it is also borrowed as mutable\\r\\n --\x3e src/main.rs:50:12\\r\\n |\\r\\n47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n | -----------------------------------------------------\\r\\n | | | |\\r\\n | | | first borrow occurs due to use of `counter` in closure\\r\\n | | mutable borrow occurs here\\r\\n | cast requires that `counter` is borrowed for `\'static`\\r\\n...\\r\\n50 | while !counter.reached_target_damage() {\\r\\n | ^^^^^^^ immutable borrow occurs here\\r\\n```\\r\\n\\r\\n看起来是一团乱麻,让我来翻译翻译,什么叫惊喜:\\r\\n\\r\\n- 闭包捕获了对 `counter` 的引用;\\r\\n- `counter.on_damage_received()` 方法接受 `&mut self`,所以闭包需要 `&mut` 可变引用。由于这个闭包在循环里,所以对同一个对象的可变引用 `&mut` 会执行多次,这是不被 Rust 允许的;\\r\\n- 我们传入的闭包没有任何生命周期标记,意味着它需要掌控所有包含变量的所有权,所以 `counter` 需要被移动到闭包内部,而在循环中,重复移动某值就会造成 `use of moved value` 错误;\\r\\n- 当 `counter` 被移动到闭包内后,我们又尝试在条件语句中使用它,显然也会报错。\\r\\n\\r\\n总之,情况不太妙。\\r\\n\\r\\n一个经典解决方法是把 `DamageCounter` 用引用计数指针裹起来,这样我们可以重复使用它了,此外由于我们需要使用 `&mut self`,所以我们需要使用 `RefCell` 来在运行时做借用检查(borrow checking),而不是在编译时。\\r\\n\\r\\n```diff\\r\\n fn main() {\\r\\n let mut rng = rand::thread_rng();\\r\\n- let mut counter = DamageCounter::default();\\r\\n+ let mut counter = Rc::new(RefCell::new(DamageCounter::default()));\\r\\n let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\r\\n\\r\\n for monster in &mut monsters {\\r\\n- monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\r\\n+ let counter = Rc::clone(&counter);\\r\\n+ monster.add_listener(Box::new(move |damage| {\\r\\n+ counter.borrow_mut().on_damage_received(damage)\\r\\n+ }));\\r\\n }\\r\\n\\r\\n- while !counter.reached_target_damage() {\\r\\n+ while !counter.borrow().reached_target_damage() {\\r\\n let index = rng.gen_range(0..monsters.len());\\r\\n let target = &mut monsters[index];\\r\\n ...\\r\\n }\\r\\n }\\r\\n```\\r\\n\\r\\n这里是[改动后的代码](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=7aca92432f337fa29de62999ea5709b8)。\\r\\n\\r\\n虽然现在代码可以正常运行了,但是整块代码会被 `RcVec>>` 之类的玩意儿搞得乌烟瘴气。而且当代码变得更复杂时,`RefCell` 也有可能会被可变借用多次。而如果在多线程中使用了 `Arc>>`,`RefCell` 引发 panic 之后,整个程序会死锁。\\r\\n\\r\\n所以,一个更好的解决方法是避免在结构体中存储持久化引用。我们对 `Monster::take_damage()` 稍加改造:\\r\\n\\r\\n```rust\\r\\nstruct Monster {\\r\\n health: u32,\\r\\n}\\r\\n\\r\\nimpl Monster {\\r\\n fn take_damage(&mut self, amount: u32, on_damage_received: impl FnOnce(u32)) {\\r\\n let damage_received = cmp::min(self.health, amount);\\r\\n self.health -= damage_received;\\r\\n on_damage_received(damage_received);\\r\\n }\\r\\n}\\r\\n\\r\\nimpl Default for Monster {\\r\\n fn default() -> Self { Monster { health: 100 } }\\r\\n}\\r\\n\\r\\n// 省略了 `DamageCounter` 的代码\\r\\n\\r\\nfn main() {\\r\\n let mut rng = rand::thread_rng();\\r\\n let mut counter = DamageCounter::default();\\r\\n let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\r\\n\\r\\n while !counter.reached_target_damage() {\\r\\n let index = rng.gen_range(0..monsters.len());\\r\\n let target = &mut monsters[index];\\r\\n\\r\\n let damage = rng.gen_range(0..50);\\r\\n target.take_damage(damage, |dmg| counter.on_damage_received(dmg));\\r\\n\\r\\n println!(\\"Monster {} received {} damage\\", index, damage);\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n这里是[在线示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=52b789a7616efe6c2e24b7e1949f7c03)。\\r\\n\\r\\n由于避免了存储回调函数,改造后的代码行数从 62 行下降到了 47 行。\\r\\n\\r\\n此外,我们也可以给 `take_damage()` 加个返回值,这样可以把伤害值放在返回值里,以备后用:\\r\\n\\r\\n```rust\\r\\nimpl Monster {\\r\\n fn take_damage(&mut self, amount: u32) -> AttackSummary {\\r\\n let damage_received = cmp::min(self.health, amount);\\r\\n self.health -= damage_received;\\r\\n AttackSummary { damage_received }\\r\\n }\\r\\n}\\r\\n\\r\\nstruct AttackSummary {\\r\\n damage_received: u32,\\r\\n}\\r\\n\\r\\n// 省略了 `DamageCounter` 的代码\\r\\n\\r\\nfn main() {\\r\\n let mut rng = rand::thread_rng();\\r\\n let mut counter = DamageCounter::default();\\r\\n let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\r\\n\\r\\n while !counter.reached_target_damage() {\\r\\n let index = rng.gen_range(0..monsters.len());\\r\\n let target = &mut monsters[index];\\r\\n\\r\\n let damage = rng.gen_range(0..50);\\r\\n let AttackSummary { damage_received } = target.take_damage(damage);\\r\\n counter.on_damage_received(damage_received);\\r\\n\\r\\n println!(\\"Monster {} received {} damage\\", index, damage);\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n当代码复杂度上升时,代码也不会变成一团糟,而且它看起来更“函数式”。\\r\\n\\r\\n## 错用整数类型\\r\\n\\r\\n另一个从 C 语言带来的坏毛病是错用整数类型,导致代码里到处都是 `usize` 的类型转换,尤其是在对数组做索引时。\\r\\n\\r\\nC 程序员在初学时就被各种教程教会了使用 `int` 类型来做索引和循环,当他们开始写 Rust 时,也自然而然地用 `Vec` 类型来做数组切片。但是阿 Rust 真的很严格,不让程序员使用 `usize` 以外的类型对数组、切片和 `Vec` 进行索引,这就不得不在索引的时候进行一次 `i32 as usize` 的类型转换。\\r\\n\\r\\nRust 这么做有诸多好处:\\r\\n\\r\\n- 无符号类型可以避免负数索引(译者按:Python 程序员请求出战);\\r\\n- `usize` 与普通指针的大小相同,指针运算不会造成隐式类型转换;\\r\\n- 内存操作函数 `std::mem::size_of()` 和 `std::mem::align_of()` 返回 `usize` 类型。\\r\\n\\r\\n所以,请尽量使用 `usize` 类型作为可能涉及索引操作的中间变量的首选类型。\\r\\n\\r\\n## 没人比我更懂 `unsafe`\\r\\n\\r\\n每次当我看到 C 程序员使用 `std::mem::transmute()` 函数或者裸指针来跳过编译器的借用检查时,我都会想起论坛中那条古老的 Rust 圣经:[Obstacles, by Daniel Keep](https://users.rust-lang.org/t/rust-koans/2408?u=michael-f-bryan)。\\r\\n\\r\\n建议你现在就去读一读,我可以等。(译者按:有空了给大伙翻译一下。)\\r\\n\\r\\n你可能已经身经百战见得多了,精通八种编程语言,所以毫无顾忌地破坏 Rust 精心构筑的规则:创建自引用的结构体、用 `unsafe` 创建全局变量。而且每一次,你都用同样的借口:“这是个单线程程序,所以 `static mut` 百分之一万没问题”、“这在 C 语言里跑得好好的”。\\r\\n\\r\\n`unsafe` 很微妙,你必须要对 Rust 的借用检查规则和内存模型有深刻的认识才行。我也不想像祥林嫂一样念叨:“未成年人请在编译器监督下编写 `unsafe` 多线程代码”,但如果你刚开始学这门语言,我衷心建议你耐心从编译器报错的痛苦中慢慢品味 Rust 的美妙。\\r\\n\\r\\n当你成为了 Rust 大师,你可以尽情玩弄 `unsafe` 代码,但在那之前,我还是想告诉你,`unsafe` 不是杀死编译器报错的板蓝根,也不是能让你自在书写 C 风味 Rust 代码的作弊码。\\r\\n\\r\\n## 不舍得用命名空间\\r\\n\\r\\nC 语言中的另一个常见实践是给函数增加所属的库名或者模块名作为前缀,比如 `rune_wasmer_runtime_load()` 就表示 `rune` 库的 `wasmer/runtime` 模块下的 `load()` 函数。Rust 提供了非常好用的命名空间机制,请尽情使用它,比如刚刚这个函数就可以写成 `rune::wasmer::Runtime::load()`。\\r\\n\\r\\n## 滥用切片索引\\r\\n\\r\\nC 语言离不开 `for (int i = 0; i < n; i ++)`,就像西方不能没有耶路撒冷。\\r\\n\\r\\n所以下面的 Rust 的代码也就不足为奇:\\r\\n\\r\\n```rust\\r\\nlet points: Vec = ...;\\r\\nlet differences = Vec::new();\\r\\n\\r\\nfor i in 1..points.len() [\\r\\n let current = points[i];\\r\\n let previous = points[i-1];\\r\\n differences.push(current - previous);\\r\\n]\\r\\n```\\r\\n\\r\\n就像呼吸一样自然。然而,就算是老司机也难免会中招下标越界 bug ,尤其当你想在循环里取前一个值时,你就得花心思去考虑 `i` 是否是从 1 开始的。\\r\\n\\r\\nRust 很担心你,所以拿出了迭代器,切片类型甚至还有 `windows()` 和 `array_windows()` 这种高级函数来获取相邻的元素对。上面的代码可以重写为:\\r\\n\\r\\n```rust\\r\\nlet points: Vec = ...;\\r\\nlet mut differences = Vec::new();\\r\\n\\r\\nfor [previous, current] in points.array_windows().copied() {\\r\\n differences.push(current - previous);\\r\\n}\\r\\n```\\r\\n\\r\\n甚至可以用链式调用来炫技:\\r\\n\\r\\n```rust\\r\\nlet differences: Vec<_> = points\\r\\n .array_windows()\\r\\n .copied()\\r\\n .map(|[previous, current]| current - previous)\\r\\n .collect();\\r\\n```\\r\\n\\r\\n有些人会主张使用了 `map()` 和 `collect` 版本的代码更加“函数式“,我则觉得仁者见仁,智者见智。\\r\\n\\r\\n不仅如此,迭代器的性能往往比朴素的 `for` 循环更好,你可以在[这里](https://users.rust-lang.org/t/we-all-know-iter-is-faster-than-loop-but-why/51486/7?u=michael-f-bryan)了解原因。\\r\\n\\r\\n## 滥用迭代器\\r\\n\\r\\n一旦你用迭代器用上瘾了,你极有可能跑向对立面:拿着迭代器这个锤子,看啥都像钉子。由 `map`,`filter` 和 `and_then()` 堆叠成的链式调用会让代码可读性下降,而且频繁使用闭包,会让数据类型变得不再直观。\\r\\n\\r\\n下面有个例子,演示了迭代器如何让你的代码变得更复杂,你可以读一读这段代码,并猜猜它是干啥的:\\r\\n\\r\\n```rust\\r\\npub fn functional_blur(input: &Matrix) -> Matrix {\\r\\n assert!(input.width >= 3);\\r\\n assert!(input.height >= 3);\\r\\n\\r\\n // 先保存首尾两行,方便后续使用\\r\\n let mut rows = input.rows();\\r\\n let first_row = rows.next().unwrap();\\r\\n let last_row = rows.next_back().unwrap();\\r\\n\\r\\n let top_row = input.rows();\\r\\n let middle_row = input.rows().skip(1);\\r\\n let bottom_row = input.rows().skip(2);\\r\\n\\r\\n let blurred_elements = top_row\\r\\n .zip(middle_row)\\r\\n .zip(bottom_row)\\r\\n .flat_map(|((top, middle), bottom)| blur_rows(top, middle, bottom));\\r\\n\\r\\n let elements: Vec = first_row\\r\\n .iter()\\r\\n .copied()\\r\\n .chain(blurred_elements)\\r\\n .chain(last_row.iter().copied())\\r\\n .collect();\\r\\n\\r\\n Matrix::new_row_major(elements, input.width, input.height)\\r\\n}\\r\\n\\r\\nfn blur_rows<\'a>(\\r\\n top_row: &\'a [f32],\\r\\n middle_row: &\'a [f32],\\r\\n bottom_row: &\'a [f32],\\r\\n) -> impl Iterator + \'a {\\r\\n // 保存头尾元素,以备后用\\r\\n let &first = middle_row.first().unwrap();\\r\\n let &last = middle_row.last().unwrap();\\r\\n\\r\\n // 获取上中下的 3x3 矩阵来做平均\\r\\n let top_window = top_row.windows(3);\\r\\n let middle_window = middle_row.windows(3);\\r\\n let bottom_window = bottom_row.windows(3);\\r\\n\\r\\n // 滑动窗口取均值,除了首尾元素\\r\\n let averages = top_window\\r\\n .zip(middle_window)\\r\\n .zip(bottom_window)\\r\\n .map(|((top, middle), bottom)| top.iter().chain(middle).chain(bottom).sum::() / 9.0);\\r\\n\\r\\n std::iter::once(first)\\r\\n .chain(averages)\\r\\n .chain(std::iter::once(last))\\r\\n}\\r\\n```\\r\\n\\r\\n[在线示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=da8fa6e55ca5a0de6005b13672688c14)。\\r\\n\\r\\n看起来好像并不难,做个均值滤波罢了,不过我这里有个更好的实现:\\r\\n\\r\\n```rust\\r\\npub fn imperative_blur(input: &Matrix) -> Matrix {\\r\\n assert!(input.width >= 3);\\r\\n assert!(input.height >= 3);\\r\\n\\r\\n // 直接从输入值拷贝返回值矩阵,这样就不需要考虑边界条件\\r\\n let mut output = input.clone();\\r\\n\\r\\n for y in 1..(input.height - 1) {\\r\\n for x in 1..(input.width - 1) {\\r\\n let mut pixel_value = 0.0;\\r\\n\\r\\n pixel_value += input[[x - 1, y - 1]];\\r\\n pixel_value += input[[x, y - 1]];\\r\\n pixel_value += input[[x + 1, y - 1]];\\r\\n\\r\\n pixel_value += input[[x - 1, y]];\\r\\n pixel_value += input[[x, y]];\\r\\n pixel_value += input[[x + 1, y]];\\r\\n\\r\\n pixel_value += input[[x - 1, y + 1]];\\r\\n pixel_value += input[[x, y + 1]];\\r\\n pixel_value += input[[x + 1, y + 1]];\\r\\n\\r\\n output[[x, y]] = pixel_value / 9.0;\\r\\n }\\r\\n }\\r\\n\\r\\n output\\r\\n}\\r\\n```\\r\\n\\r\\n[在线示例](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ed5a8cbe8cfab762c32466c551957810)。\\r\\n\\r\\n我想你的心里已经有答案了吧。\\r\\n\\r\\n## 不会用模式匹配\\r\\n\\r\\n让我们回到一开始的 `IndexOf()` 函数,我们用 `Option` 类型举了一个很好的例子,先看下原始代码:\\r\\n\\r\\n```c#\\r\\nint index = sentence.IndexOf(\\"fox\\");\\r\\n\\r\\nif (index != -1)\\r\\n{\\r\\n string wordsAfterFox = sentence.SubString(index);\\r\\n Console.WriteLine(wordsAfterFox);\\r\\n}\\r\\n```\\r\\n\\r\\n然后,你可能会看到这样的 Rust 代码:\\r\\n\\r\\n```rust\\r\\nlet opt: Option<_> = ...;\\r\\n\\r\\nif opt.is_some() {\\r\\n let value = opt.unwrap();\\r\\n ...\\r\\n}\\r\\n```\\r\\n\\r\\n或者这样的:\\r\\n\\r\\n```rust\\r\\nlet list: &[f32] = ...;\\r\\n\\r\\nif !list.is_empty() {\\r\\n let first = list[0];\\r\\n ...\\r\\n}\\r\\n```\\r\\n\\r\\n这些条件语句都在避免某些边界条件,不过就像之前说到的哨兵值一样,我们在重构的时候依然会极有可能引入 bug。\\r\\n\\r\\n而使用 Rust 的模式匹配,你可以保证当且仅当值有效时才会执行到对应的代码:\\r\\n\\r\\n```rust\\r\\nif let Some(value) = opt {\\r\\n ...\\r\\n}\\r\\n\\r\\nif let [first, ..] = list {\\r\\n ...\\r\\n}\\r\\n```\\r\\n\\r\\n相比于之前的代码,由于避免了 `opt.unwrap()` 和 `list[index]`,模式匹配可以有更好的性能(作者的一点忠告:不要在网上听风就是雨,如果你真的想知道真相,建议写个 Benchmark 验证下)。\\r\\n\\r\\n## 别再构造函数后初始化\\r\\n\\r\\n许多语言都会在构造对象后调用对应的初始化函数(`init()` 之类的),但这有悖于 Rust 的约定:让无效状态不可见。\\r\\n\\r\\n假设你在写个 NLP 程序,需要加载一个包含所有关键词的词表:\\r\\n\\r\\n```rust\\r\\nlet mut dict = Dictionary::new();\\r\\n// 读取文件并且把值存到哈希表或者列表里\\r\\ndict.load_from_file(\\"./words.txt\\")?;\\r\\n```\\r\\n\\r\\n然而,如果这么写了,意味着 `Dictionary` 类有两个状态:空的和满的。\\r\\n\\r\\n那么如果后续代码假设 `Dictionary` 有值,并且直接使用它,那当我们错误地对一个空状态的 `Dictionary` 进行索引时,就会造成 panic。\\r\\n\\r\\n在 Rust 中,最好在构造时就对结构体进行初始化,来避免结构体的空状态。\\r\\n\\r\\n```rust\\r\\nlet dict = Dictionary::from_file(\\"./words.txt\\")?;\\r\\n\\r\\nimpl Dictionary {\\r\\n fn from_file(filename: impl AsRef) -> Result {\\r\\n let text = std::fs::read_to_string(filename)?;\\r\\n let mut words = Vec::new();\\r\\n for line in text.lines() {\\r\\n words.push(line);\\r\\n }\\r\\n Ok(Dictionary { words })\\r\\n }\\r\\n}\\r\\n```\\r\\n\\r\\n`Dictionary::from_file()` 直接执行了初始化操作,并返回了初始化后的、立即可用的结构体,从而避免了上述问题。\\r\\n\\r\\n当然,遇到这种问题的频率因人而异,完全取决于你的编码经验和代码风格。\\r\\n\\r\\n一般来讲,函数式语言强调不可变性,所以函数式语言的使用者会天然地掌握这个经验。毕竟当你不能随便改变某个值时,你也不大可能创建一个初始化了一半的变量,然后再用什么其他值去填满它。\\r\\n\\r\\n但面向对象的语言就不太一样了,它可能更鼓励你先构造个空对象,然后再调用具体函数初始化它,毕竟对象引用很容易为 `null`,而且他们也不关心什么可变性之类的玩意儿……现在你知道为啥那些面向对象语言会经常由于 `NullPointerException` 崩溃了吧。\\r\\n\\r\\n## 保护性拷贝\\r\\n\\r\\n不可变对象的一个显而易见的优点时,你永远可以相信它不会发生变化,而放心地使用它的值。但某些语言,比如 Python 或者 Java,不可变性没有传递性。举个例子,`x` 是个不可变对象,`x.y` 却不一定是不可变的,除非显式地定义它的可变性。\\r\\n\\r\\n这意味着会出现下面的 Python 代码:\\r\\n\\r\\n```python\\r\\nclass ImmutablePerson:\\r\\n def __init__(self, name: str, age: int, addresses: List[str]):\\r\\n self._name = name\\r\\n self._age = age\\r\\n self._addresses = addresses\\r\\n\\r\\n # 只读属性\\r\\n @property\\r\\n def name(self): return self._name\\r\\n @property\\r\\n def age(self): return self._age\\r\\n @property\\r\\n def addresses(self): return self._addresses\\r\\n```\\r\\n\\r\\n后来其他人用了这个号称不可变的 `ImmutablePerson`,但是却不小心把它搞乱了:\\r\\n\\r\\n```python\\r\\ndef send_letters(message: str, addresses: List[str]):\\r\\n # 注意:发信 api 只接受大写字母,所以这里要预处理\\r\\n for i, address in enumerate(addresses):\\r\\n addresses[i] = addresses.upper()\\r\\n\\r\\n client = PostOfficeClient()\\r\\n client.send_bulk_mail(message, addresses)\\r\\n\\r\\n\\r\\nperson = ImmutablePerson(\\"Joe Bloggs\\", 42, [\\"123 Fake Street\\"])\\r\\n\\r\\nsend_letters(\\r\\n f\\"Dear {person.name}, I Nigerian prince. Please help me moving my monies.\\",\\r\\n person.addresses\\r\\n)\\r\\n\\r\\nprint(person.addresses) # [\\"123 FAKE STREET\\"]\\r\\n```\\r\\n\\r\\n我承认,这个例子确实有点刻意了,但是修改一个函数的传参却非常常见(译者按:尤其在某些深度学习项目里)。当你知道你自己定义的 `ImmutablePerson` 的 `addresses` 属性不可变时,不会有什么大问题,但是当你和别人协作,而且别人还不知道 `addresses` 不可变时,那就出大问题了。\\r\\n\\r\\n事情也不是无法挽回,解决这个问题的经典方法是,总是在获取属性值的时候,返回它的拷贝,而非它自己:\\r\\n\\r\\n```python\\r\\nclass ImmutablePerson:\\r\\n ...\\r\\n\\r\\n @property\\r\\n def addresses(self): return self._addresses.copy()\\r\\n```\\r\\n\\r\\n这样就可以保证别人在使用该对象的属性时,不会意外改变它的原始值。\\r\\n\\r\\n考虑到这篇文章的主题是 Rust,你可能已经猜到了造成这种问题的根本原因:别名与可变性。\\r\\n\\r\\n并且你也可能想到,这种情况不会发生在 Rust 中,生命周期机制以及“有且只能有一处可变引用”的机制,保证了程序员无法在取得变量的所有权情况下去改动它的值,也没法显式地使用 `std::sync::Mutex` 去改变某个共享引用值。\\r\\n\\r\\n> 备注:你可能见过别人用 `.clone()` 来处理借用检查器的报错,然后就大喊“你看,Rust 还不是强迫我们做了保护性拷贝措施?”。我想说的是,这种代码基本都是由于程序员不熟悉生命周期机制,或者代码设计有问题导致的。\\r\\n\\r\\n## 总结\\r\\n\\r\\n本文并不能覆盖所有的最差实践,有些是因为我没亲身经历过,有些则是由于没法给出精简的例子。\\r\\n\\r\\n衷心地感谢回复我在 Rust 论坛发布的[这个帖子](https://users.rust-lang.org/t/common-newbie-mistakes-or-bad-practices/64821/8)的各位同仁,尽管帖子的最后有点跑偏,各位 Rust 老鸟的论战还是让我受益颇深。\\r\\n","content":"
\\n

原文:Common Newbie Mistakes and Bad Practices in Rust: Bad Habits
\\n译者:Yidadaa

\\n
\\n

很多从其他语言过来的 Rust 新手都会不可避免地利用之前的编码经验来写 Rust,这无可厚非,毕竟确实没必要从头开始学习编程知识。但是,这些经验性知识,却极有可能导致你写出来很垃圾的 Rust 代码。

\\n

别再用哨兵值了

\\n

这可能是我最讨厌的一点。在大多数沿袭 C 语言设计的语言中(C,C#,Java 等),经常使用一个特殊值来表示某个函数执行失败的情况。比如,在 C# 中,用于在字符串中查找另一个字符串的索引位置的函数 String.IndexOf(t) 会在找不到 t 时返回 -1,从而写出这样的 C# 代码:

\\n
string sentence = "The fox jumps over the dog";\\n\\nint index = sentence.IndexOf("fox");\\n\\nif (index != -1)\\n{\\n  string wordsAfterFox = sentence.SubString(index);\\n  Console.WriteLine(wordsAfterFox);\\n}\\n
\\n

在其他的语言中,这种用法更是数不胜数,类似的哨兵值还有空字符串 "" 或者 nullNone 之类的空值。

\\n

既然它这么常用,为什么还要说它很差劲呢?原因就是你极有可能会忘记处理哨兵值所代表的失败情况,然后导致整个程序直接崩溃。

\\n

Rust 则提供了很好的解决方案,那就是 OptionOption 从设计层面就杜绝了忘记考虑 None 时的情况,编译器会在编译时就进行强制检查,如果你忘了处理 None,编译器会马上告诉你。上面字符串例子的代码,在 Rust 中可以写成这样:

\\n
let sentence = "The fox jumps over the dog";\\nlet index = sentence.find("fox");\\n\\n// let words_after_fox = &sentence[index..];\\n// 如果你直接使用 index,会得到报错:Error: Can't index str with Option<usize>\\n\\nif let Some(fox) = index {\\n  let words_after_fox = &sentence[fox..];\\n  println!("{}", words_after_fox);\\n}\\n
\\n

别再用匈牙利命名了

\\n

上世纪 70 年代,程序员们逐渐开始在无类型或动态类型语言中使用匈牙利命名法,他们给变量名加上不同的前缀来表示变量的类型,比如 bVisited 表示布尔型的变量 visitedstrName 表示字符串类型的变量 name

\\n

我们可以在 Delphi 语言中见到大量的例子,T 开头的表示类(class)或者类型(type),F 表示属性值(fields),A 表示参数(arguments),诸如此类。

\\n
type\\n TKeyValue = class\\n  private\\n    FKey: integer;\\n    FValue: TObject;\\n  public\\n    property Key: integer read FKey write FKey;\\n    property Value: TObject read FValue write FValue;\\n    function Frobnicate(ASomeArg: string): string;\\n  end;\\n
\\n

C# 中也有类似的使用习惯,比如用 I 开头表示一个接口(interface),所以 C# 程序员很可能会写出这种 Rust 代码:

\\n
trait IClone {\\n  fn clone(&self) -> Self;\\n}\\n
\\n

你大可以直接扔掉前面的 I,因为 Rust 的语法已经保证了我们很难混淆 traittype,不像 C# 很容易就分不清 interfaceclass(译者按:Typescript 中就是 interfacetypeclass 大混战了,狗头.jpg)。

\\n

此外,你也没有必要在给一些工具函数或者中间变量命名时带上它的类型信息,比如下面的代码:

\\n
let account_bytes: Vec<u8> = read_some_input();\\nlet account_str = String::from_utf8(account_bytes)?;\\nlet account: Account = account_str.parse()?;\\n
\\n

既然 String.from_utf8() 已经明明白白地返回了一个字符串,为什么还要在命名时加上 _str 后缀呢?

\\n

与其他语言不同,Rust 语言鼓励程序员在对变量进行一系列变换操作时,使用同名变量覆写掉不再使用的旧值,比如:

\\n
let account: Vec<u8> = read_some_input();\\nlet account = String::from_utf8(account)?;\\nlet account: Account = account.parse()?;\\n
\\n

使用相同的变量名可以很好地保证概念的一致性。

\\n

有些语言会明令禁止覆写变量,尤其像 Javascript 这种动态类型语言,因为频繁变化的类型,在缺少类型推断的情况下,尤其有可能会导致 bug 出现。

\\n

你可能不需要这么多 Rc<RefCell<T>>

\\n

OOP 编程实践常常会保存其他对象的引用,并在合适的时候调用他们的函数,这没啥不好的,依赖注入(Dependency Injection)是个蛮不错的实践,不过有别于大多数面向对象的语言,Rust 并没有垃圾内存回收机制(Garbage Collector),并且对共享可变性非常敏感。

\\n

举个例子,我们正要实现一个打怪兽的游戏,玩家需要对怪物们造成足量伤害才算打败他们(我也不知道为什么要这么设定,可能是接受了什么委托?)。

\\n

先创建一个 Monster 类,包含 health 生命值属性以及 takeDamage() 遭受伤害的方法,为了能知道怪物遭受了多少伤害,我们允许为 Monster 类注入一个回调函数,该回调函数可以接收每次遭受的伤害值。

\\n
type OnReceivedDamage = (damageReceived: number) => void;\\n\\nclass Monster {\\n  health: number = 50;\\n  receivedDamage: OnReceivedDamage[] = [];\\n\\n  takeDamage(amount: number) {\\n    amount = Math.min(this.health, amount);\\n    this.health -= amount;\\n    this.receivedDamage.forEach((cb) => cb(amount));\\n  }\\n\\n  on(event: "damaged", callback: OnReceivedDamage): void {\\n    this.receivedDamage.push(callback);\\n  }\\n}\\n
\\n

然后设计一个伤害计数类 DamageCounter,可以累计怪物伤害值:

\\n
class DamageCounter {\\n  damageInflicted: number = 0;\\n\\n  reachedTargetDamage(): boolean {\\n    return this.damageInflicted > 100;\\n  }\\n\\n  onDamageInflicted(amount: number) {\\n    this.damageInflicted += amount;\\n  }\\n}\\n
\\n

然后我们对怪物造成随机伤害,直到伤害计数达到上限。

\\n
const counter = new DamageCounter();\\n\\nconst monsters = [\\n  new Monster(),\\n  new Monster(),\\n  new Monster(),\\n  new Monster(),\\n  new Monster(),\\n];\\nmonsters.forEach((m) =>\\n  m.on("damaged", (amount) => counter.onDamageInflicted(amount))\\n);\\n\\nwhile (!counter.reachedTargetDamage()) {\\n  // 随机选中怪物\\n  const index = Math.floor(Math.random() * monsters.length);\\n  const target = monsters[index];\\n  // 产生随机伤害\\n  const damage = Math.round(Math.random() * 50);\\n  target.takeDamage(damage);\\n\\n  console.log(`Monster ${index} received ${damage} damage`);\\n}\\n
\\n

这里是在线运行示例

\\n

然后我们用 Rust 重写上述逻辑,Monster 结构体结构保持不变,使用 Box<dyn Fn(u32)> 来接收闭包,该闭包接受一个 u32 型参数。

\\n
type OnReceivedDamage = Box<dyn Fn(u32)>;\\n\\nstruct Monster {\\n    health: u32,\\n    received_damage: Vec<OnReceivedDamage>,\\n}\\n\\nimpl Monster {\\n    fn take_damage(&mut self, amount: u32) {\\n        let damage_received = cmp::min(self.health, amount);\\n        self.health -= damage_received;\\n        for callback in &mut self.received_damage {\\n            callback(damage_received);\\n        }\\n    }\\n\\n    fn add_listener(&mut self, listener: OnReceivedDamage) {\\n        self.received_damage.push(listener);\\n    }\\n}\\n\\nimpl Default for Monster {\\n    fn default() -> Self {\\n        Monster { health: 100, received_damage: Vec::new() }\\n    }\\n}\\n
\\n

随后是 DamageCounter 类:

\\n
#[derive(Default)]\\nstruct DamageCounter {\\n    damage_inflicted: u32,\\n}\\n\\nimpl DamageCounter {\\n    fn reached_target_damage(&self) -> bool {\\n        self.damage_inflicted > 100\\n    }\\n\\n    fn on_damage_received(&mut self, damage: u32) {\\n        self.damage_inflicted += damage;\\n    }\\n}\\n
\\n

然后开始打怪:

\\n
fn main() {\\n    let mut rng = rand::thread_rng();\\n    let mut counter = DamageCounter::default();\\n    let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\n\\n    for monster in &mut monsters {\\n        monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n    }\\n\\n    while !counter.reached_target_damage() {\\n        let index = rng.gen_range(0..monsters.len());\\n        let target = &mut monsters[index];\\n\\n        let damage = rng.gen_range(0..50);\\n        target.take_damage(damage);\\n\\n        println!("Monster {} received {} damage", index, damage);\\n    }\\n}\\n
\\n

同样地,Rust 代码也有一个在线运行示例

\\n

然而,编译器狠狠地给我们报了四个错误,全都集中在 monster.add_listener() 这一行:

\\n
error[E0596]: cannot borrow `counter` as mutable, as it is a captured variable in a `Fn` closure\\n  --> src/main.rs:47:48\\n   |\\n47 |         monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n   |                                                ^^^^^^^ cannot borrow as mutable\\n\\nerror[E0499]: cannot borrow `counter` as mutable more than once at a time\\n  --> src/main.rs:47:39\\n   |\\n47 |         monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n   |                              ---------^^^^^^^^------------------------------------\\n   |                              |        |        |\\n   |                              |        |        borrows occur due to use of `counter` in closure\\n   |                              |        `counter` was mutably borrowed here in the previous iteration of the loop\\n   |                              cast requires that `counter` is borrowed for `'static`\\n\\nerror[E0597]: `counter` does not live long enough\\n  --> src/main.rs:47:48\\n   |\\n47 |         monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n   |                              ------------------^^^^^^^----------------------------\\n   |                              |        |        |\\n   |                              |        |        borrowed value does not live long enough\\n   |                              |        value captured here\\n   |                              cast requires that `counter` is borrowed for `'static`\\n...\\n60 | }\\n   | - `counter` dropped here while still borrowed\\n\\nerror[E0502]: cannot borrow `counter` as immutable because it is also borrowed as mutable\\n  --> src/main.rs:50:12\\n   |\\n47 |         monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n   |                              -----------------------------------------------------\\n   |                              |        |        |\\n   |                              |        |        first borrow occurs due to use of `counter` in closure\\n   |                              |        mutable borrow occurs here\\n   |                              cast requires that `counter` is borrowed for `'static`\\n...\\n50 |     while !counter.reached_target_damage() {\\n   |            ^^^^^^^ immutable borrow occurs here\\n
\\n

看起来是一团乱麻,让我来翻译翻译,什么叫惊喜

\\n
    \\n
  • 闭包捕获了对 counter 的引用;
  • \\n
  • counter.on_damage_received() 方法接受 &mut self,所以闭包需要 &mut 可变引用。由于这个闭包在循环里,所以对同一个对象的可变引用 &mut 会执行多次,这是不被 Rust 允许的;
  • \\n
  • 我们传入的闭包没有任何生命周期标记,意味着它需要掌控所有包含变量的所有权,所以 counter 需要被移动到闭包内部,而在循环中,重复移动某值就会造成 use of moved value 错误;
  • \\n
  • counter 被移动到闭包内后,我们又尝试在条件语句中使用它,显然也会报错。
  • \\n
\\n

总之,情况不太妙。

\\n

一个经典解决方法是把 DamageCounter 用引用计数指针裹起来,这样我们可以重复使用它了,此外由于我们需要使用 &mut self,所以我们需要使用 RefCell 来在运行时做借用检查(borrow checking),而不是在编译时。

\\n
 fn main() {\\n     let mut rng = rand::thread_rng();\\n-    let mut counter = DamageCounter::default();\\n+    let mut counter = Rc::new(RefCell::new(DamageCounter::default()));\\n     let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\n\\n     for monster in &mut monsters {\\n-        monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));\\n+        let counter = Rc::clone(&counter);\\n+        monster.add_listener(Box::new(move |damage| {\\n+            counter.borrow_mut().on_damage_received(damage)\\n+        }));\\n     }\\n\\n-    while !counter.reached_target_damage() {\\n+    while !counter.borrow().reached_target_damage() {\\n         let index = rng.gen_range(0..monsters.len());\\n         let target = &mut monsters[index];\\n         ...\\n     }\\n }\\n
\\n

这里是改动后的代码

\\n

虽然现在代码可以正常运行了,但是整块代码会被 Rc<RefCell>Vec<Foo>>> 之类的玩意儿搞得乌烟瘴气。而且当代码变得更复杂时,RefCell 也有可能会被可变借用多次。而如果在多线程中使用了 Arc<Mutex<Vec<Foo>>>RefCell 引发 panic 之后,整个程序会死锁。

\\n

所以,一个更好的解决方法是避免在结构体中存储持久化引用。我们对 Monster::take_damage() 稍加改造:

\\n
struct Monster {\\n    health: u32,\\n}\\n\\nimpl Monster {\\n    fn take_damage(&mut self, amount: u32, on_damage_received: impl FnOnce(u32)) {\\n        let damage_received = cmp::min(self.health, amount);\\n        self.health -= damage_received;\\n        on_damage_received(damage_received);\\n    }\\n}\\n\\nimpl Default for Monster {\\n  fn default() -> Self { Monster { health: 100 } }\\n}\\n\\n// 省略了 `DamageCounter` 的代码\\n\\nfn main() {\\n    let mut rng = rand::thread_rng();\\n    let mut counter = DamageCounter::default();\\n    let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\n\\n    while !counter.reached_target_damage() {\\n        let index = rng.gen_range(0..monsters.len());\\n        let target = &mut monsters[index];\\n\\n        let damage = rng.gen_range(0..50);\\n        target.take_damage(damage, |dmg| counter.on_damage_received(dmg));\\n\\n        println!("Monster {} received {} damage", index, damage);\\n    }\\n}\\n
\\n

这里是在线示例

\\n

由于避免了存储回调函数,改造后的代码行数从 62 行下降到了 47 行。

\\n

此外,我们也可以给 take_damage() 加个返回值,这样可以把伤害值放在返回值里,以备后用:

\\n
impl Monster {\\n    fn take_damage(&mut self, amount: u32) -> AttackSummary {\\n        let damage_received = cmp::min(self.health, amount);\\n        self.health -= damage_received;\\n        AttackSummary { damage_received }\\n    }\\n}\\n\\nstruct AttackSummary {\\n    damage_received: u32,\\n}\\n\\n// 省略了 `DamageCounter` 的代码\\n\\nfn main() {\\n    let mut rng = rand::thread_rng();\\n    let mut counter = DamageCounter::default();\\n    let mut monsters: Vec<_> = (0..5).map(|_| Monster::default()).collect();\\n\\n    while !counter.reached_target_damage() {\\n        let index = rng.gen_range(0..monsters.len());\\n        let target = &mut monsters[index];\\n\\n        let damage = rng.gen_range(0..50);\\n        let AttackSummary { damage_received } = target.take_damage(damage);\\n        counter.on_damage_received(damage_received);\\n\\n        println!("Monster {} received {} damage", index, damage);\\n    }\\n}\\n
\\n

当代码复杂度上升时,代码也不会变成一团糟,而且它看起来更“函数式”。

\\n

错用整数类型

\\n

另一个从 C 语言带来的坏毛病是错用整数类型,导致代码里到处都是 usize 的类型转换,尤其是在对数组做索引时。

\\n

C 程序员在初学时就被各种教程教会了使用 int 类型来做索引和循环,当他们开始写 Rust 时,也自然而然地用 Vec<i32> 类型来做数组切片。但是阿 Rust 真的很严格,不让程序员使用 usize 以外的类型对数组、切片和 Vec 进行索引,这就不得不在索引的时候进行一次 i32 as usize 的类型转换。

\\n

Rust 这么做有诸多好处:

\\n
    \\n
  • 无符号类型可以避免负数索引(译者按:Python 程序员请求出战);
  • \\n
  • usize 与普通指针的大小相同,指针运算不会造成隐式类型转换;
  • \\n
  • 内存操作函数 std::mem::size_of()std::mem::align_of() 返回 usize 类型。
  • \\n
\\n

所以,请尽量使用 usize 类型作为可能涉及索引操作的中间变量的首选类型。

\\n

没人比我更懂 unsafe

\\n

每次当我看到 C 程序员使用 std::mem::transmute() 函数或者裸指针来跳过编译器的借用检查时,我都会想起论坛中那条古老的 Rust 圣经:Obstacles, by Daniel Keep

\\n

建议你现在就去读一读,我可以等。(译者按:有空了给大伙翻译一下。)

\\n

你可能已经身经百战见得多了,精通八种编程语言,所以毫无顾忌地破坏 Rust 精心构筑的规则:创建自引用的结构体、用 unsafe 创建全局变量。而且每一次,你都用同样的借口:“这是个单线程程序,所以 static mut 百分之一万没问题”、“这在 C 语言里跑得好好的”。

\\n

unsafe 很微妙,你必须要对 Rust 的借用检查规则和内存模型有深刻的认识才行。我也不想像祥林嫂一样念叨:“未成年人请在编译器监督下编写 unsafe 多线程代码”,但如果你刚开始学这门语言,我衷心建议你耐心从编译器报错的痛苦中慢慢品味 Rust 的美妙。

\\n

当你成为了 Rust 大师,你可以尽情玩弄 unsafe 代码,但在那之前,我还是想告诉你,unsafe 不是杀死编译器报错的板蓝根,也不是能让你自在书写 C 风味 Rust 代码的作弊码。

\\n

不舍得用命名空间

\\n

C 语言中的另一个常见实践是给函数增加所属的库名或者模块名作为前缀,比如 rune_wasmer_runtime_load() 就表示 rune 库的 wasmer/runtime 模块下的 load() 函数。Rust 提供了非常好用的命名空间机制,请尽情使用它,比如刚刚这个函数就可以写成 rune::wasmer::Runtime::load()

\\n

滥用切片索引

\\n

C 语言离不开 for (int i = 0; i < n; i ++),就像西方不能没有耶路撒冷。

\\n

所以下面的 Rust 的代码也就不足为奇:

\\n
let points: Vec<Coordinate> = ...;\\nlet differences = Vec::new();\\n\\nfor i in 1..points.len() [\\n  let current = points[i];\\n  let previous = points[i-1];\\n  differences.push(current - previous);\\n]\\n
\\n

就像呼吸一样自然。然而,就算是老司机也难免会中招下标越界 bug ,尤其当你想在循环里取前一个值时,你就得花心思去考虑 i 是否是从 1 开始的。

\\n

Rust 很担心你,所以拿出了迭代器,切片类型甚至还有 windows()array_windows() 这种高级函数来获取相邻的元素对。上面的代码可以重写为:

\\n
let points: Vec<Coordinate> = ...;\\nlet mut differences = Vec::new();\\n\\nfor [previous, current] in points.array_windows().copied() {\\n  differences.push(current - previous);\\n}\\n
\\n

甚至可以用链式调用来炫技:

\\n
let differences: Vec<_> = points\\n  .array_windows()\\n  .copied()\\n  .map(|[previous, current]| current - previous)\\n  .collect();\\n
\\n

有些人会主张使用了 map()collect 版本的代码更加“函数式“,我则觉得仁者见仁,智者见智。

\\n

不仅如此,迭代器的性能往往比朴素的 for 循环更好,你可以在这里了解原因。

\\n

滥用迭代器

\\n

一旦你用迭代器用上瘾了,你极有可能跑向对立面:拿着迭代器这个锤子,看啥都像钉子。由 mapfilterand_then() 堆叠成的链式调用会让代码可读性下降,而且频繁使用闭包,会让数据类型变得不再直观。

\\n

下面有个例子,演示了迭代器如何让你的代码变得更复杂,你可以读一读这段代码,并猜猜它是干啥的:

\\n
pub fn functional_blur(input: &Matrix) -> Matrix {\\n    assert!(input.width >= 3);\\n    assert!(input.height >= 3);\\n\\n    // 先保存首尾两行,方便后续使用\\n    let mut rows = input.rows();\\n    let first_row = rows.next().unwrap();\\n    let last_row = rows.next_back().unwrap();\\n\\n    let top_row = input.rows();\\n    let middle_row = input.rows().skip(1);\\n    let bottom_row = input.rows().skip(2);\\n\\n    let blurred_elements = top_row\\n        .zip(middle_row)\\n        .zip(bottom_row)\\n        .flat_map(|((top, middle), bottom)| blur_rows(top, middle, bottom));\\n\\n    let elements: Vec<f32> = first_row\\n        .iter()\\n        .copied()\\n        .chain(blurred_elements)\\n        .chain(last_row.iter().copied())\\n        .collect();\\n\\n    Matrix::new_row_major(elements, input.width, input.height)\\n}\\n\\nfn blur_rows<'a>(\\n    top_row: &'a [f32],\\n    middle_row: &'a [f32],\\n    bottom_row: &'a [f32],\\n) -> impl Iterator<Item = f32> + 'a {\\n    // 保存头尾元素,以备后用\\n    let &first = middle_row.first().unwrap();\\n    let &last = middle_row.last().unwrap();\\n\\n    // 获取上中下的 3x3 矩阵来做平均\\n    let top_window = top_row.windows(3);\\n    let middle_window = middle_row.windows(3);\\n    let bottom_window = bottom_row.windows(3);\\n\\n    // 滑动窗口取均值,除了首尾元素\\n    let averages = top_window\\n        .zip(middle_window)\\n        .zip(bottom_window)\\n        .map(|((top, middle), bottom)| top.iter().chain(middle).chain(bottom).sum::<f32>() / 9.0);\\n\\n    std::iter::once(first)\\n        .chain(averages)\\n        .chain(std::iter::once(last))\\n}\\n
\\n

在线示例

\\n

看起来好像并不难,做个均值滤波罢了,不过我这里有个更好的实现:

\\n
pub fn imperative_blur(input: &Matrix) -> Matrix {\\n    assert!(input.width >= 3);\\n    assert!(input.height >= 3);\\n\\n    // 直接从输入值拷贝返回值矩阵,这样就不需要考虑边界条件\\n    let mut output = input.clone();\\n\\n    for y in 1..(input.height - 1) {\\n        for x in 1..(input.width - 1) {\\n            let mut pixel_value = 0.0;\\n\\n            pixel_value += input[[x - 1, y - 1]];\\n            pixel_value += input[[x, y - 1]];\\n            pixel_value += input[[x + 1, y - 1]];\\n\\n            pixel_value += input[[x - 1, y]];\\n            pixel_value += input[[x, y]];\\n            pixel_value += input[[x + 1, y]];\\n\\n            pixel_value += input[[x - 1, y + 1]];\\n            pixel_value += input[[x, y + 1]];\\n            pixel_value += input[[x + 1, y + 1]];\\n\\n            output[[x, y]] = pixel_value / 9.0;\\n        }\\n    }\\n\\n    output\\n}\\n
\\n

在线示例

\\n

我想你的心里已经有答案了吧。

\\n

不会用模式匹配

\\n

让我们回到一开始的 IndexOf() 函数,我们用 Option 类型举了一个很好的例子,先看下原始代码:

\\n
int index = sentence.IndexOf("fox");\\n\\nif (index != -1)\\n{\\n  string wordsAfterFox = sentence.SubString(index);\\n  Console.WriteLine(wordsAfterFox);\\n}\\n
\\n

然后,你可能会看到这样的 Rust 代码:

\\n
let opt: Option<_> = ...;\\n\\nif opt.is_some() {\\n  let value = opt.unwrap();\\n  ...\\n}\\n
\\n

或者这样的:

\\n
let list: &[f32] = ...;\\n\\nif !list.is_empty() {\\n  let first = list[0];\\n  ...\\n}\\n
\\n

这些条件语句都在避免某些边界条件,不过就像之前说到的哨兵值一样,我们在重构的时候依然会极有可能引入 bug。

\\n

而使用 Rust 的模式匹配,你可以保证当且仅当值有效时才会执行到对应的代码:

\\n
if let Some(value) = opt {\\n  ...\\n}\\n\\nif let [first, ..] = list {\\n  ...\\n}\\n
\\n

相比于之前的代码,由于避免了 opt.unwrap()list[index],模式匹配可以有更好的性能(作者的一点忠告:不要在网上听风就是雨,如果你真的想知道真相,建议写个 Benchmark 验证下)。

\\n

别再构造函数后初始化

\\n

许多语言都会在构造对象后调用对应的初始化函数(init() 之类的),但这有悖于 Rust 的约定:让无效状态不可见。

\\n

假设你在写个 NLP 程序,需要加载一个包含所有关键词的词表:

\\n
let mut dict = Dictionary::new();\\n// 读取文件并且把值存到哈希表或者列表里\\ndict.load_from_file("./words.txt")?;\\n
\\n

然而,如果这么写了,意味着 Dictionary 类有两个状态:空的和满的。

\\n

那么如果后续代码假设 Dictionary 有值,并且直接使用它,那当我们错误地对一个空状态的 Dictionary 进行索引时,就会造成 panic。

\\n

在 Rust 中,最好在构造时就对结构体进行初始化,来避免结构体的空状态。

\\n
let dict = Dictionary::from_file("./words.txt")?;\\n\\nimpl Dictionary {\\n  fn from_file(filename: impl AsRef<Path>) -> Result<Self, Error> {\\n    let text = std::fs::read_to_string(filename)?;\\n    let mut words = Vec::new();\\n    for line in text.lines() {\\n      words.push(line);\\n    }\\n    Ok(Dictionary { words })\\n  }\\n}\\n
\\n

Dictionary::from_file() 直接执行了初始化操作,并返回了初始化后的、立即可用的结构体,从而避免了上述问题。

\\n

当然,遇到这种问题的频率因人而异,完全取决于你的编码经验和代码风格。

\\n

一般来讲,函数式语言强调不可变性,所以函数式语言的使用者会天然地掌握这个经验。毕竟当你不能随便改变某个值时,你也不大可能创建一个初始化了一半的变量,然后再用什么其他值去填满它。

\\n

但面向对象的语言就不太一样了,它可能更鼓励你先构造个空对象,然后再调用具体函数初始化它,毕竟对象引用很容易为 null,而且他们也不关心什么可变性之类的玩意儿……现在你知道为啥那些面向对象语言会经常由于 NullPointerException 崩溃了吧。

\\n

保护性拷贝

\\n

不可变对象的一个显而易见的优点时,你永远可以相信它不会发生变化,而放心地使用它的值。但某些语言,比如 Python 或者 Java,不可变性没有传递性。举个例子,x 是个不可变对象,x.y 却不一定是不可变的,除非显式地定义它的可变性。

\\n

这意味着会出现下面的 Python 代码:

\\n
class ImmutablePerson:\\n  def __init__(self, name: str, age: int, addresses: List[str]):\\n    self._name = name\\n    self._age = age\\n    self._addresses = addresses\\n\\n  # 只读属性\\n  @property\\n  def name(self): return self._name\\n  @property\\n  def age(self): return self._age\\n  @property\\n  def addresses(self): return self._addresses\\n
\\n

后来其他人用了这个号称不可变的 ImmutablePerson,但是却不小心把它搞乱了:

\\n
def send_letters(message: str, addresses: List[str]):\\n  # 注意:发信 api 只接受大写字母,所以这里要预处理\\n  for i, address in enumerate(addresses):\\n    addresses[i] = addresses.upper()\\n\\n  client = PostOfficeClient()\\n  client.send_bulk_mail(message, addresses)\\n\\n\\nperson = ImmutablePerson("Joe Bloggs", 42, ["123 Fake Street"])\\n\\nsend_letters(\\n  f"Dear {person.name}, I Nigerian prince. Please help me moving my monies.",\\n  person.addresses\\n)\\n\\nprint(person.addresses) # ["123 FAKE STREET"]\\n
\\n

我承认,这个例子确实有点刻意了,但是修改一个函数的传参却非常常见(译者按:尤其在某些深度学习项目里)。当你知道你自己定义的 ImmutablePersonaddresses 属性不可变时,不会有什么大问题,但是当你和别人协作,而且别人还不知道 addresses 不可变时,那就出大问题了。

\\n

事情也不是无法挽回,解决这个问题的经典方法是,总是在获取属性值的时候,返回它的拷贝,而非它自己:

\\n
class ImmutablePerson:\\n  ...\\n\\n  @property\\n  def addresses(self): return self._addresses.copy()\\n
\\n

这样就可以保证别人在使用该对象的属性时,不会意外改变它的原始值。

\\n

考虑到这篇文章的主题是 Rust,你可能已经猜到了造成这种问题的根本原因:别名与可变性。

\\n

并且你也可能想到,这种情况不会发生在 Rust 中,生命周期机制以及“有且只能有一处可变引用”的机制,保证了程序员无法在取得变量的所有权情况下去改动它的值,也没法显式地使用 std::sync::Mutex<T> 去改变某个共享引用值。

\\n
\\n

备注:你可能见过别人用 .clone() 来处理借用检查器的报错,然后就大喊“你看,Rust 还不是强迫我们做了保护性拷贝措施?”。我想说的是,这种代码基本都是由于程序员不熟悉生命周期机制,或者代码设计有问题导致的。

\\n
\\n

总结

\\n

本文并不能覆盖所有的最差实践,有些是因为我没亲身经历过,有些则是由于没法给出精简的例子。

\\n

衷心地感谢回复我在 Rust 论坛发布的这个帖子的各位同仁,尽管帖子的最后有点跑偏,各位 Rust 老鸟的论战还是让我受益颇深。

\\n"},"31":{"id":31,"date":"2022/01/21","author":"Yidadaa","title":"2021,新世界","mdContent":"*本文约 6000 字,预计阅读耗时 10 分钟。*\\r\\n\\r\\n> 真要是清水一潭也有点可怕。 \\r\\n> 但世界拥挤不堪……妈妈。 \\r\\n> —— 赛博文学 \\r\\n\\r\\n2021 年 1 月 1 日凌晨 0 点 04 分,我发了一条仅两人可见的动态:2021 年的第一分钟,和妮妮拥吻中度过。\\r\\n\\r\\n世界的这场改变了无数人生活的大灾变,像是与我们无关,在这地球上成千上万个我们听不到也看不到的地方敲响跨年钟声的一刻,在这别处有成千上万个人或欢呼、或吵闹、或欢笑、或沉默不语的跨年瞬间,一切都与在这间小小的宿舍里的我们无关,我们只管将浓情蜜意托付彼此,悄然无声地用湿润的舌苔碰触着。\\r\\n\\r\\n成都的雪已经许久没有来了,也或许是来的时候我也没有觉察,正如被厚积云覆盖的四川盆地上空闪烁的星空,虽然时刻闪耀着,我却总不能看到他们。\\r\\n\\r\\n往年,我寄希望于用数据刻画过去的一年,然而生活总是给我新的体悟,它给我的教训如此深刻,以致无法用任何数字衡量。所以,今年暂且用随想的形式记录过去这一年吧。\\r\\n\\r\\n现在,已然奏起了德彪西的月光曲,思绪随音符流淌。\\r\\n\\r\\n### 一、蜜语\\r\\n\\r\\n钱钟书老爷子说:中年人谈恋爱,就像老房子着火,没得救了。\\r\\n\\r\\n我在临近二十四岁的时候第一次遇见爱情,虽说不算是中年,但总体来讲来的是有些晚了,可能老房子还未变老,房里的蛛网还未结起来,木制构件也没被岁月浸透得太过酥碎,所以干柴烈火燃起来的时候,有一些奋不顾身的热烈,也有一些后悔得起的韧性。\\r\\n\\r\\n疫情来临那一年,是缺失的一年,也是遇见爱情的一年,也是因了爱情的浸润,让我失去了审视过去的动力,成了拖延的理由,所以干脆放弃写那年的总结。\\r\\n\\r\\n这也正是恋爱后一年的注脚,如果一个人被什么美好的事情蒙蔽了头脑,他大抵感受不到光脚踩到的石头,也无瑕顾及曾不小心撞到了什么人,犯下了什么错。\\r\\n\\r\\n人生的帆胀满了风,只顾乘风破浪地前行,彩霞在地平线上飘荡着,帆船手的背影满是热烈的欣喜。\\r\\n\\r\\n要从再往前一年的九月份开始谈,故事才好起个头。2020 年 9 月份,我刚从腾讯实习返校,顺便开始在亚马逊的远程实习,学妹因为一些科研琐事来询问。后来我才知道她对科研并不感兴趣,其实我也对科研不感兴趣,她只喜欢画画,科研只为毕业,我只喜欢代码,科研也只为毕业。\\r\\n\\r\\n但那时我们都是台上的演员,演一出勤学好问的师兄师妹情谊好戏,所以只能装模作样地你来我往,对无聊的科研议题指手画脚。\\r\\n\\r\\n但演员之意不在演戏,在乎假戏真做之间。我已觉得人生了无生趣,但在精神归隐之前,还想体验一下鱼水之情,常言道:英雄难过美人关,我倒要看看美人有何把戏。\\r\\n\\r\\n不料却真着了道,我向来对美食无感,但那几个月却跑遍春熙路的各色餐馆,颇有寻医问药之意,只为能与美人幽会。\\r\\n\\r\\n往返美食的电车上,我和学妹之间隔着一个哆嗦的距离,此间暧昧说也说不清,道也道不明,电车就这么事不关己地往前走着,全然不顾两个各怀鬼胎的演员,谈说着貌合神离的天地。\\r\\n\\r\\n“学妹,你可知叔本华的钟摆理论?人生一世,免不了遭受痛苦,叔本华告诫我们,人这一生,恰似钟摆,要么在欲求不满的痛苦中挣扎,要么在欲望满足的无聊中迷失。”,我右手肘摆成标准的直角,电车扶手的力规整地传导到我的身体,整个人随着弯曲的铁轨微微晃动,像是一片在微波中摇摆的、充满哲思的浮萍,眼角却离不开学妹戴了美瞳的眼睛,铁轨两旁的路灯流星一样在她眼里曳尾,在我心里留下各色烙印。\\r\\n\\r\\n“嗯?学长好懂哦,我只觉得人生好苦,上学好累”,学妹目不转睛地盯着手机,手指翻飞,“不过我有个高中同学,他说国庆要来找我玩,想去国色天香游乐园玩,所以过两天不能陪学长了哦”。\\r\\n\\r\\n“是吗,哈哈,这电车会不会开啊,给爷颠得站不稳了都”。我手臂一下子没了力气,竟有宵小之徒从中作梗,岂可修孰不可修,嘴上也没了威风,土龙路到合信路的这段路可真漫长。\\r\\n\\r\\n两天后,我感冒了,学妹从国色天香回来,我在合信路地铁站接她,顺便去龙湖的药店买感冒药。彼时疫情已肆虐多时,药店管制甚严,我俩一言不发地买完药,回到一组团门前,生硬地道别。然而回到寝室,学妹却发信说今天游乐园好无聊,还是想去龙湖玩。\\r\\n\\r\\n于是在 2020 年十月底的某一天,我们站在龙湖十七层的私人影院店门前阳台上闲谈,等着私人影院的预约包间开场。\\r\\n\\r\\n时至今日,我已记不清当时是晴或阴,但在十七楼的阳台远眺校园,上空总是有一团薄雾氤氲,图书馆被光晕笼罩着。\\r\\n\\r\\n2021 年年底,我拿到了招行寄来腾讯大厦的金葵花联名卡,尾号是 0926,看到数字的那一瞬间,我便为世间的巧合感叹:这尾号正是学妹的生日。而我记住这个生日,不仅仅是因为陪她度过了 2021 年的 21 岁生日,而且也因为她在那个阳台上,略带遗憾地跟我说:我原本想在 19 岁结束前脱单,但看来这个愿望并没有实现。\\r\\n\\r\\n后来在私人影院的沙发上,学妹在我的耳边问我:“你是不是喜欢我”。我毫无疑问地说:“是的”。\\r\\n\\r\\n随后,便是:玉楼冰簟鸳鸯锦,粉融香汗流山枕。帘外辘轳声,敛眉含笑惊。柳阴烟漠漠,低鬓蝉钗落。须作一生拼,尽君今日欢。\\r\\n\\r\\n随后半年,也无非是:相见休言有泪珠,酒阑重得叙欢娱,凤屏鸳枕宿金铺。兰麝细香闻喘息,绮罗纤缕见肌肤,此时还恨薄情无?\\r\\n\\r\\n### 二、入世\\r\\n\\r\\n七月,我毕业了。\\r\\n\\r\\n别离是一种愁绪,是不知何时嵌进指尖的刺,吃痛也是在事发之后。\\r\\n\\r\\n如果你在 2021 年上半年去检查郫县男高硕丰七组团某栋三楼的五间宿舍,会发现它们被走廊顶的几根网线连在了一起,这里必然存在着私通网线的苟且之事,而且显而易见的是,我就是始作俑者之一。\\r\\n\\r\\n费拉不堪的校园网一直在承受着它这个角色的双亲本就应当承受的来自莘莘学子们的真挚问候,这在七年前我们来这个鸟不拉屎的地方读本科的时候就已经形成了一个共识。那时我们甚至还不能带自己的电脑,只能徒步两公里横穿除了大而一无是处的校园去寻找附近的网吧。\\r\\n\\r\\n后来这所以电子信息技术教育声名在外的学校,终于允许它的二年级学生携带禁忌之圣物——笔记本电脑来到学校,并开启他们在计算机科学之境抑或是召唤师峡谷的冒险旅途。\\r\\n\\r\\n而我们在这里成为了研究生老油条之后,必然对每月大几十的网费感到不值,并誓将网费下降到每人每月不足十块钱,所以我们便构建了这么一套共享网络机制。我在提出这个提议时,得到了诸位好兄弟的一致同意,毕竟这群憨批也觉得有便宜不占是王八蛋,只不过在构建起这么一个网络之后,走廊上时常会响起他们对共享网络卡顿之优美的溢美之词。\\r\\n\\r\\n后来,我在繁华帝都的十平米单间里使用五倍于之前网速的网络百无聊赖地在某视频网站高速冲浪时,还是会不由自主地怀念起这群傻逼的粗鄙之语。\\r\\n\\r\\n七月之后,我在北京望京利星行某层报到,用秀丽笔签下了第一份正式聘用合同,并且满怀憧憬地和几十万同期的新生代农民工一起准备为建设美好未来而奋斗,如果我当时知道后来半年发生的事情,嘴角的弧度应该会少上几分。\\r\\n\\r\\n我比大部分同龄农民工要幸运一些,可以和同住校园宿舍六七年的老同学继续合租,七月中旬,我拎着行李来到圣鑫家园 2044 入住客厅隔断的单间,然而住在主卧的启迪却忧心忡忡,他说最近帝都严查客厅隔断,昨晚梦见我的房间隔断轰然倒塌,我在灰尘弥漫的客厅痛哭,我说快别他妈放屁了,你什么时候见过我哭。\\r\\n\\r\\n事实也的确如此,我的房间确实安然无恙,然而入住不到一个月之后,我们几个人在太阳宫派出所大厅里焦急地等待阿 SIR 审问完那个花臂东北黑中介,并深刻挂念着自己的几万块房租能否被讨要回来。\\r\\n\\r\\n生活是条不知疲倦的鲇鱼,在时间涡流中翻滚着,它不在乎你是否刚来到这片鱼塘,尽情地用尾巴慰问我们的脸颊。\\r\\n\\r\\n在派出所对峙的前一天,是一个美好的周六,来到帝都之后,除了需要每天加班到晚上十点的峙龙,我们都对自己的工作十分满意,所以那天早上睡梦中的我们,对急促的敲门声毫无防备。\\r\\n\\r\\n第一次敲门,一对有些拘谨的情侣来问我们这里是不是 2044 号房间,我们亲切地告知他们可能走错楼栋了。\\r\\n\\r\\n第二次敲门,还是他们,问我们为什么还没搬走?我们有些疑惑,但还是亲切地告知他们应该是走错房间了。\\r\\n\\r\\n直到第三次敲门,他们拿出了和我们一摸一样的租房合同,我们才意识到都被那个东北花臂黑中介给骗了,之后房东加入战局,好戏开场,大家齐聚太阳宫派出所,把那个中介纠到阿 SIR 面前对峙,才把房租讨要回来。\\r\\n\\r\\n随后,我们火急火燎地找下一个住处,好歹是再次安顿了下来,并感叹社会实在凶险。\\r\\n\\r\\n但社会竟会如此凶险。\\r\\n\\r\\n2021 年 9 月份,国家“双减”政策执行细则出台,我所在的在线教育公司不得不大张旗鼓地裁员,我也如惊弓之鸟,慌忙寻找下家,在第一家公司的两个多月生活如梦一场,如同十六岁无果的恋爱,给得了爱情的模样,却给不了未来。\\r\\n\\r\\n慌乱之中,成都腾讯企微某组组长捞起我的简历,亲切地问候:北京可苦?不如回成都。\\r\\n\\r\\n成都确实好,这里有大豪斯,香串串,以及七年的成都记忆。我眩晕了,心动到彷徨,一发不可收拾,在那不到三个月的时间,在北京的遭遇可谓是灾难,我已对北京彻底失望,所以爽快违约房租合同,帮室友们交了不菲的违约金,只为回到成都怀抱。\\r\\n\\r\\n离京之前,在陕西面馆里,点了一碗胡辣汤,一股血气冲破满脑门,冲破牙龈,在切齿的缝里流淌出来,流淌到商场地板上,流淌到月亮上。时至深秋,太白在天幕的角落躲躲闪闪,像簌然而下的眼泪,在情人的眼角徘徊。加班的人们匆匆闪过,纷纷地,如尾灯在角膜上留下的残影。\\r\\n\\r\\n我只是没有想到,此后的生活只会不断地让人更加失望。\\r\\n\\r\\n我如愿以偿地住上了宽敞房子,与北京十多平米单间一样的价格,在成都可以住上两室一厅,女友对此颇为满意。\\r\\n\\r\\n然而一个多月过后,在不知道第几次十点半下班后,我回到家中,拖着被加班掏空的身体瘫倒在沙发上,已然一具了无生气的尸体,白天组长略含怒气的话语在脑海盘旋,深夜里,突然惊醒,却在回味如何书写公司的项目代码。\\r\\n\\r\\n玉林路旁,旧友相聚,我眼睑低垂,口吐芬芳,咒骂职场艰辛,无比迫切地想要逃离这一切,几杯红啤下肚,烧烤摊却突然停电,我们便踱到道旁。\\r\\n\\r\\n成都仍旧积云密布,暮霭沉沉,小巷里,一束灯光从半空打下,行人来去匆匆,我驻足片刻,便快步离开。\\r\\n\\r\\n电子科大图书馆前的草坪上,伫立着几颗枝繁叶茂的树,天晴时,学生们或在树下乘凉,或在暖阳里依偎;也有晨雾弥漫时,路灯强力的光笼罩整片草坪,空气里没有风,也没有雨,却能真切地嗅到水雾的凉爽气息,那几棵树就这么静静地站着,置身事外地站着,无蝉鸣泣,树影也不婆娑。\\r\\n\\r\\n我恨它们冷漠,恨它们沉默不语,恨它们就只会静静地看着我们,我大吼,你是不是看不到众生痛苦,你是不是视而不见!\\r\\n\\r\\n声音在辽阔的草坪上回旋,消逝在浓雾深处,树们法相庄严,无喜无悲。\\r\\n\\r\\n有些话可以对人说,有些话却只能对树说,但有时对树也开不了口,我的确开不了口,在看到那条暮霭沉沉的小巷中洒下的灯光时,却无端想到树下的欢笑和依偎。\\r\\n\\r\\n我才是傻逼,总奢望在成都的浓雾里看到暖阳,总奢望能刺破云层看到星光。\\r\\n\\r\\n他们总说品学楼是个品字形,我说放屁,这不就是个鸡巴吗?他们笑我粗俗,只会说污秽之语,但是我没有告诉他们,每次洗澡的时候,我都会想起学校的那栋建筑,谁也无法料到,我曾鄙夷的不屑一顾的归属感,竟以这种粗俗的方式永久而强烈地留在了每一天的生活里。\\r\\n\\r\\n### 三、致知\\r\\n\\r\\n生活太苦,日子总是要过,少不了借酒浇愁,代码就是我的酒。\\r\\n\\r\\n我好午夜酗酒,从本科就好这口,凌晨两点灯火阑珊,峙龙躺在床上睡如死猪,我十指飞舞,誓要与键盘大战三百回合,突然峙龙惊醒,骂我键盘太吵,我们进行了一番寝室密友亲切友好的和谐交流,峙龙终于体力不支,再度沉沉睡去。\\r\\n\\r\\n总是有专家发表惊世骇俗言论,说游戏是电子海洛因。我嗤之以鼻,游戏?狗都不玩。代码才是真正的电子海洛因,掌握二十六个英文字母,十几个关键字,你就是计算机的神,什么图灵冯诺依曼迪杰斯特拉,肩膀任你踩踏,拿起键盘,亦如黎明中的花朵,比特于内存之中绽放。\\r\\n\\r\\n我固然喜欢写代码,打工挣钱?行!写代码?行!打工写代码挣钱?当然行!加班打工写代码挣钱?那必然是不行。\\r\\n\\r\\n过度劳动是灵感的毒药,人的大脑是只猛虎,吃不饱睡不足,只能算病猫,只有吃饱歇足了,才是能吃人能下山的猛虎。\\r\\n\\r\\n现在有人过来捏着老虎鼻子灌毒药,老虎指定是要吃人的。\\r\\n\\r\\n2021 的前半年,在学校里一边接私活挣钱,一边谈恋爱,一边写毕设。虽然私活的代码有些枯燥,毕设全靠无中生有,但生活还算滋润。\\r\\n\\r\\n后来入职前司,每日七点下班,稍作休息后即可重振精神,猛虎归来,趁着夜色,可以熟读源码三百行,也可手搓框架四五个。晚上思绪格外活跃,再加上隔三岔五健身保持精力充沛,一切都格外美好。\\r\\n\\r\\n后来便遭遇了那些变故,前文已形之笔墨,此处不谈。\\r\\n\\r\\n新公司极尽福报之所能,在业界声誉斐然,号称“起夜微信不加班,年终百万也枉然”。我在入职前已做足了心理准备,不料还是人民资本家技高一筹,每日奔波劳碌于需求,下班之后再无余力。\\r\\n\\r\\n最终还是下定决心,准备离开这个是非之地,心情便一下愉悦不少。\\r\\n\\r\\n回到代码本身,程序员大致可分为两个派路,一派精研技术,力求登峰造极;一派注重体验,追求创造价值。虽然大部分半吊子程序员都很难归到这两类里,但最近一年还是有幸遇到了几位同仁,有人操练类型体操,有人灵感另辟蹊径,都令我敬佩不已。\\r\\n\\r\\n而我必然是属于半吊子程序员之类。之前一直在做毫无技术含量的外包需求,只能果腹,顺便积累项目经验,却既难精研技术,也难打磨用户体验。\\r\\n\\r\\n遂在工作之后悔过自新,向同仁学习,既有学习业界开源项目重造框架,又在开辟新方向,熟读 Rust 心法,颇有要在 GUI 领域做一番事业之志。\\r\\n\\r\\n谈及代码,免不了文人相轻兼夜郎自大之嫌,所幸有开源社区斧正态度。\\r\\n\\r\\n生活亦不易,当多入技术之境忘我钻研,人生苦短,世间七情六欲难窥其真相,唯诉诸逻辑方得正解。\\r\\n\\r\\n### 四、结\\r\\n\\r\\n行文至此,笔墨竭尽,月光曲落多时,爵士乐曲调逡巡,窗外霓虹灯渐微,建筑楼顶的航空障碍灯错落有致。\\r\\n\\r\\n记忆是四维尺度上的信息,极尽文字之所能,也只能管中窥豹,九牛取之一毛。但记忆的胶卷在脑中过了一圈,我似乎能闻到它一帧帧划过,与输片轮摩擦生热烘焙出的特殊味道,这就是世间滋味吗?\\r\\n\\r\\n过去这一年,我从温暖的大西洋流中挣脱出来,一头扎进冰冷的北冰洋,个中冷暖,涕泪有感。\\r\\n\\r\\n再来到这个崭新的又有些熟悉的世界,彷徨多于笃定,苦痛多于欢愉,甚至路程匆匆,很多人来不及深交,很多答案也来不及求索。\\r\\n\\r\\n请允许我庸俗地将时间之弦跳跃拨动,回到和学妹第一次约定的饭桌上,在我说出“六十岁就想暴毙”之前,她就坦言“四十岁准备暴毙”,不由得感叹:人生天地间,忽如远行客,能寻慷慨赴死伴侣,生亦何欢,死亦何苦?\\r\\n\\r\\n","content":"

本文约 6000 字,预计阅读耗时 10 分钟。

\\n
\\n

真要是清水一潭也有点可怕。
\\n但世界拥挤不堪……妈妈。
\\n—— 赛博文学

\\n
\\n

2021 年 1 月 1 日凌晨 0 点 04 分,我发了一条仅两人可见的动态:2021 年的第一分钟,和妮妮拥吻中度过。

\\n

世界的这场改变了无数人生活的大灾变,像是与我们无关,在这地球上成千上万个我们听不到也看不到的地方敲响跨年钟声的一刻,在这别处有成千上万个人或欢呼、或吵闹、或欢笑、或沉默不语的跨年瞬间,一切都与在这间小小的宿舍里的我们无关,我们只管将浓情蜜意托付彼此,悄然无声地用湿润的舌苔碰触着。

\\n

成都的雪已经许久没有来了,也或许是来的时候我也没有觉察,正如被厚积云覆盖的四川盆地上空闪烁的星空,虽然时刻闪耀着,我却总不能看到他们。

\\n

往年,我寄希望于用数据刻画过去的一年,然而生活总是给我新的体悟,它给我的教训如此深刻,以致无法用任何数字衡量。所以,今年暂且用随想的形式记录过去这一年吧。

\\n

现在,已然奏起了德彪西的月光曲,思绪随音符流淌。

\\n

一、蜜语

\\n

钱钟书老爷子说:中年人谈恋爱,就像老房子着火,没得救了。

\\n

我在临近二十四岁的时候第一次遇见爱情,虽说不算是中年,但总体来讲来的是有些晚了,可能老房子还未变老,房里的蛛网还未结起来,木制构件也没被岁月浸透得太过酥碎,所以干柴烈火燃起来的时候,有一些奋不顾身的热烈,也有一些后悔得起的韧性。

\\n

疫情来临那一年,是缺失的一年,也是遇见爱情的一年,也是因了爱情的浸润,让我失去了审视过去的动力,成了拖延的理由,所以干脆放弃写那年的总结。

\\n

这也正是恋爱后一年的注脚,如果一个人被什么美好的事情蒙蔽了头脑,他大抵感受不到光脚踩到的石头,也无瑕顾及曾不小心撞到了什么人,犯下了什么错。

\\n

人生的帆胀满了风,只顾乘风破浪地前行,彩霞在地平线上飘荡着,帆船手的背影满是热烈的欣喜。

\\n

要从再往前一年的九月份开始谈,故事才好起个头。2020 年 9 月份,我刚从腾讯实习返校,顺便开始在亚马逊的远程实习,学妹因为一些科研琐事来询问。后来我才知道她对科研并不感兴趣,其实我也对科研不感兴趣,她只喜欢画画,科研只为毕业,我只喜欢代码,科研也只为毕业。

\\n

但那时我们都是台上的演员,演一出勤学好问的师兄师妹情谊好戏,所以只能装模作样地你来我往,对无聊的科研议题指手画脚。

\\n

但演员之意不在演戏,在乎假戏真做之间。我已觉得人生了无生趣,但在精神归隐之前,还想体验一下鱼水之情,常言道:英雄难过美人关,我倒要看看美人有何把戏。

\\n

不料却真着了道,我向来对美食无感,但那几个月却跑遍春熙路的各色餐馆,颇有寻医问药之意,只为能与美人幽会。

\\n

往返美食的电车上,我和学妹之间隔着一个哆嗦的距离,此间暧昧说也说不清,道也道不明,电车就这么事不关己地往前走着,全然不顾两个各怀鬼胎的演员,谈说着貌合神离的天地。

\\n

“学妹,你可知叔本华的钟摆理论?人生一世,免不了遭受痛苦,叔本华告诫我们,人这一生,恰似钟摆,要么在欲求不满的痛苦中挣扎,要么在欲望满足的无聊中迷失。”,我右手肘摆成标准的直角,电车扶手的力规整地传导到我的身体,整个人随着弯曲的铁轨微微晃动,像是一片在微波中摇摆的、充满哲思的浮萍,眼角却离不开学妹戴了美瞳的眼睛,铁轨两旁的路灯流星一样在她眼里曳尾,在我心里留下各色烙印。

\\n

“嗯?学长好懂哦,我只觉得人生好苦,上学好累”,学妹目不转睛地盯着手机,手指翻飞,“不过我有个高中同学,他说国庆要来找我玩,想去国色天香游乐园玩,所以过两天不能陪学长了哦”。

\\n

“是吗,哈哈,这电车会不会开啊,给爷颠得站不稳了都”。我手臂一下子没了力气,竟有宵小之徒从中作梗,岂可修孰不可修,嘴上也没了威风,土龙路到合信路的这段路可真漫长。

\\n

两天后,我感冒了,学妹从国色天香回来,我在合信路地铁站接她,顺便去龙湖的药店买感冒药。彼时疫情已肆虐多时,药店管制甚严,我俩一言不发地买完药,回到一组团门前,生硬地道别。然而回到寝室,学妹却发信说今天游乐园好无聊,还是想去龙湖玩。

\\n

于是在 2020 年十月底的某一天,我们站在龙湖十七层的私人影院店门前阳台上闲谈,等着私人影院的预约包间开场。

\\n

时至今日,我已记不清当时是晴或阴,但在十七楼的阳台远眺校园,上空总是有一团薄雾氤氲,图书馆被光晕笼罩着。

\\n

2021 年年底,我拿到了招行寄来腾讯大厦的金葵花联名卡,尾号是 0926,看到数字的那一瞬间,我便为世间的巧合感叹:这尾号正是学妹的生日。而我记住这个生日,不仅仅是因为陪她度过了 2021 年的 21 岁生日,而且也因为她在那个阳台上,略带遗憾地跟我说:我原本想在 19 岁结束前脱单,但看来这个愿望并没有实现。

\\n

后来在私人影院的沙发上,学妹在我的耳边问我:“你是不是喜欢我”。我毫无疑问地说:“是的”。

\\n

随后,便是:玉楼冰簟鸳鸯锦,粉融香汗流山枕。帘外辘轳声,敛眉含笑惊。柳阴烟漠漠,低鬓蝉钗落。须作一生拼,尽君今日欢。

\\n

随后半年,也无非是:相见休言有泪珠,酒阑重得叙欢娱,凤屏鸳枕宿金铺。兰麝细香闻喘息,绮罗纤缕见肌肤,此时还恨薄情无?

\\n

二、入世

\\n

七月,我毕业了。

\\n

别离是一种愁绪,是不知何时嵌进指尖的刺,吃痛也是在事发之后。

\\n

如果你在 2021 年上半年去检查郫县男高硕丰七组团某栋三楼的五间宿舍,会发现它们被走廊顶的几根网线连在了一起,这里必然存在着私通网线的苟且之事,而且显而易见的是,我就是始作俑者之一。

\\n

费拉不堪的校园网一直在承受着它这个角色的双亲本就应当承受的来自莘莘学子们的真挚问候,这在七年前我们来这个鸟不拉屎的地方读本科的时候就已经形成了一个共识。那时我们甚至还不能带自己的电脑,只能徒步两公里横穿除了大而一无是处的校园去寻找附近的网吧。

\\n

后来这所以电子信息技术教育声名在外的学校,终于允许它的二年级学生携带禁忌之圣物——笔记本电脑来到学校,并开启他们在计算机科学之境抑或是召唤师峡谷的冒险旅途。

\\n

而我们在这里成为了研究生老油条之后,必然对每月大几十的网费感到不值,并誓将网费下降到每人每月不足十块钱,所以我们便构建了这么一套共享网络机制。我在提出这个提议时,得到了诸位好兄弟的一致同意,毕竟这群憨批也觉得有便宜不占是王八蛋,只不过在构建起这么一个网络之后,走廊上时常会响起他们对共享网络卡顿之优美的溢美之词。

\\n

后来,我在繁华帝都的十平米单间里使用五倍于之前网速的网络百无聊赖地在某视频网站高速冲浪时,还是会不由自主地怀念起这群傻逼的粗鄙之语。

\\n

七月之后,我在北京望京利星行某层报到,用秀丽笔签下了第一份正式聘用合同,并且满怀憧憬地和几十万同期的新生代农民工一起准备为建设美好未来而奋斗,如果我当时知道后来半年发生的事情,嘴角的弧度应该会少上几分。

\\n

我比大部分同龄农民工要幸运一些,可以和同住校园宿舍六七年的老同学继续合租,七月中旬,我拎着行李来到圣鑫家园 2044 入住客厅隔断的单间,然而住在主卧的启迪却忧心忡忡,他说最近帝都严查客厅隔断,昨晚梦见我的房间隔断轰然倒塌,我在灰尘弥漫的客厅痛哭,我说快别他妈放屁了,你什么时候见过我哭。

\\n

事实也的确如此,我的房间确实安然无恙,然而入住不到一个月之后,我们几个人在太阳宫派出所大厅里焦急地等待阿 SIR 审问完那个花臂东北黑中介,并深刻挂念着自己的几万块房租能否被讨要回来。

\\n

生活是条不知疲倦的鲇鱼,在时间涡流中翻滚着,它不在乎你是否刚来到这片鱼塘,尽情地用尾巴慰问我们的脸颊。

\\n

在派出所对峙的前一天,是一个美好的周六,来到帝都之后,除了需要每天加班到晚上十点的峙龙,我们都对自己的工作十分满意,所以那天早上睡梦中的我们,对急促的敲门声毫无防备。

\\n

第一次敲门,一对有些拘谨的情侣来问我们这里是不是 2044 号房间,我们亲切地告知他们可能走错楼栋了。

\\n

第二次敲门,还是他们,问我们为什么还没搬走?我们有些疑惑,但还是亲切地告知他们应该是走错房间了。

\\n

直到第三次敲门,他们拿出了和我们一摸一样的租房合同,我们才意识到都被那个东北花臂黑中介给骗了,之后房东加入战局,好戏开场,大家齐聚太阳宫派出所,把那个中介纠到阿 SIR 面前对峙,才把房租讨要回来。

\\n

随后,我们火急火燎地找下一个住处,好歹是再次安顿了下来,并感叹社会实在凶险。

\\n

但社会竟会如此凶险。

\\n

2021 年 9 月份,国家“双减”政策执行细则出台,我所在的在线教育公司不得不大张旗鼓地裁员,我也如惊弓之鸟,慌忙寻找下家,在第一家公司的两个多月生活如梦一场,如同十六岁无果的恋爱,给得了爱情的模样,却给不了未来。

\\n

慌乱之中,成都腾讯企微某组组长捞起我的简历,亲切地问候:北京可苦?不如回成都。

\\n

成都确实好,这里有大豪斯,香串串,以及七年的成都记忆。我眩晕了,心动到彷徨,一发不可收拾,在那不到三个月的时间,在北京的遭遇可谓是灾难,我已对北京彻底失望,所以爽快违约房租合同,帮室友们交了不菲的违约金,只为回到成都怀抱。

\\n

离京之前,在陕西面馆里,点了一碗胡辣汤,一股血气冲破满脑门,冲破牙龈,在切齿的缝里流淌出来,流淌到商场地板上,流淌到月亮上。时至深秋,太白在天幕的角落躲躲闪闪,像簌然而下的眼泪,在情人的眼角徘徊。加班的人们匆匆闪过,纷纷地,如尾灯在角膜上留下的残影。

\\n

我只是没有想到,此后的生活只会不断地让人更加失望。

\\n

我如愿以偿地住上了宽敞房子,与北京十多平米单间一样的价格,在成都可以住上两室一厅,女友对此颇为满意。

\\n

然而一个多月过后,在不知道第几次十点半下班后,我回到家中,拖着被加班掏空的身体瘫倒在沙发上,已然一具了无生气的尸体,白天组长略含怒气的话语在脑海盘旋,深夜里,突然惊醒,却在回味如何书写公司的项目代码。

\\n

玉林路旁,旧友相聚,我眼睑低垂,口吐芬芳,咒骂职场艰辛,无比迫切地想要逃离这一切,几杯红啤下肚,烧烤摊却突然停电,我们便踱到道旁。

\\n

成都仍旧积云密布,暮霭沉沉,小巷里,一束灯光从半空打下,行人来去匆匆,我驻足片刻,便快步离开。

\\n

电子科大图书馆前的草坪上,伫立着几颗枝繁叶茂的树,天晴时,学生们或在树下乘凉,或在暖阳里依偎;也有晨雾弥漫时,路灯强力的光笼罩整片草坪,空气里没有风,也没有雨,却能真切地嗅到水雾的凉爽气息,那几棵树就这么静静地站着,置身事外地站着,无蝉鸣泣,树影也不婆娑。

\\n

我恨它们冷漠,恨它们沉默不语,恨它们就只会静静地看着我们,我大吼,你是不是看不到众生痛苦,你是不是视而不见!

\\n

声音在辽阔的草坪上回旋,消逝在浓雾深处,树们法相庄严,无喜无悲。

\\n

有些话可以对人说,有些话却只能对树说,但有时对树也开不了口,我的确开不了口,在看到那条暮霭沉沉的小巷中洒下的灯光时,却无端想到树下的欢笑和依偎。

\\n

我才是傻逼,总奢望在成都的浓雾里看到暖阳,总奢望能刺破云层看到星光。

\\n

他们总说品学楼是个品字形,我说放屁,这不就是个鸡巴吗?他们笑我粗俗,只会说污秽之语,但是我没有告诉他们,每次洗澡的时候,我都会想起学校的那栋建筑,谁也无法料到,我曾鄙夷的不屑一顾的归属感,竟以这种粗俗的方式永久而强烈地留在了每一天的生活里。

\\n

三、致知

\\n

生活太苦,日子总是要过,少不了借酒浇愁,代码就是我的酒。

\\n

我好午夜酗酒,从本科就好这口,凌晨两点灯火阑珊,峙龙躺在床上睡如死猪,我十指飞舞,誓要与键盘大战三百回合,突然峙龙惊醒,骂我键盘太吵,我们进行了一番寝室密友亲切友好的和谐交流,峙龙终于体力不支,再度沉沉睡去。

\\n

总是有专家发表惊世骇俗言论,说游戏是电子海洛因。我嗤之以鼻,游戏?狗都不玩。代码才是真正的电子海洛因,掌握二十六个英文字母,十几个关键字,你就是计算机的神,什么图灵冯诺依曼迪杰斯特拉,肩膀任你踩踏,拿起键盘,亦如黎明中的花朵,比特于内存之中绽放。

\\n

我固然喜欢写代码,打工挣钱?行!写代码?行!打工写代码挣钱?当然行!加班打工写代码挣钱?那必然是不行。

\\n

过度劳动是灵感的毒药,人的大脑是只猛虎,吃不饱睡不足,只能算病猫,只有吃饱歇足了,才是能吃人能下山的猛虎。

\\n

现在有人过来捏着老虎鼻子灌毒药,老虎指定是要吃人的。

\\n

2021 的前半年,在学校里一边接私活挣钱,一边谈恋爱,一边写毕设。虽然私活的代码有些枯燥,毕设全靠无中生有,但生活还算滋润。

\\n

后来入职前司,每日七点下班,稍作休息后即可重振精神,猛虎归来,趁着夜色,可以熟读源码三百行,也可手搓框架四五个。晚上思绪格外活跃,再加上隔三岔五健身保持精力充沛,一切都格外美好。

\\n

后来便遭遇了那些变故,前文已形之笔墨,此处不谈。

\\n

新公司极尽福报之所能,在业界声誉斐然,号称“起夜微信不加班,年终百万也枉然”。我在入职前已做足了心理准备,不料还是人民资本家技高一筹,每日奔波劳碌于需求,下班之后再无余力。

\\n

最终还是下定决心,准备离开这个是非之地,心情便一下愉悦不少。

\\n

回到代码本身,程序员大致可分为两个派路,一派精研技术,力求登峰造极;一派注重体验,追求创造价值。虽然大部分半吊子程序员都很难归到这两类里,但最近一年还是有幸遇到了几位同仁,有人操练类型体操,有人灵感另辟蹊径,都令我敬佩不已。

\\n

而我必然是属于半吊子程序员之类。之前一直在做毫无技术含量的外包需求,只能果腹,顺便积累项目经验,却既难精研技术,也难打磨用户体验。

\\n

遂在工作之后悔过自新,向同仁学习,既有学习业界开源项目重造框架,又在开辟新方向,熟读 Rust 心法,颇有要在 GUI 领域做一番事业之志。

\\n

谈及代码,免不了文人相轻兼夜郎自大之嫌,所幸有开源社区斧正态度。

\\n

生活亦不易,当多入技术之境忘我钻研,人生苦短,世间七情六欲难窥其真相,唯诉诸逻辑方得正解。

\\n

四、结

\\n

行文至此,笔墨竭尽,月光曲落多时,爵士乐曲调逡巡,窗外霓虹灯渐微,建筑楼顶的航空障碍灯错落有致。

\\n

记忆是四维尺度上的信息,极尽文字之所能,也只能管中窥豹,九牛取之一毛。但记忆的胶卷在脑中过了一圈,我似乎能闻到它一帧帧划过,与输片轮摩擦生热烘焙出的特殊味道,这就是世间滋味吗?

\\n

过去这一年,我从温暖的大西洋流中挣脱出来,一头扎进冰冷的北冰洋,个中冷暖,涕泪有感。

\\n

再来到这个崭新的又有些熟悉的世界,彷徨多于笃定,苦痛多于欢愉,甚至路程匆匆,很多人来不及深交,很多答案也来不及求索。

\\n

请允许我庸俗地将时间之弦跳跃拨动,回到和学妹第一次约定的饭桌上,在我说出“六十岁就想暴毙”之前,她就坦言“四十岁准备暴毙”,不由得感叹:人生天地间,忽如远行客,能寻慷慨赴死伴侣,生亦何欢,死亦何苦?

\\n"},"34":{"id":34,"date":"2022/11/07","author":"Yidadaa","title":"如何使用 5000 块组装一台顶配 Mac Studio","mdContent":"> 写给程序员的小尺寸高性能黑苹果主机装配指南,全文约 10000 字。\\r\\n\\r\\n> 本文同步发表至:[FlowUs](https://flowus.cn/yifei/share/cb8f8b2f-591f-4a34-a901-b714a4c81bcc) / [知乎](https://zhuanlan.zhihu.com/p/580506404) / [Github](https://github.com/Yidadaa/Yidadaa.github.io/issues/34) / [博客](https://blog.simplenaive.cn/34)\\r\\n\\r\\n> 请使用良好的网络环境访问此文,否则图片可能无法加载。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374112-bac58562-f71f-4f32-b674-b012a275d56a.png)\\r\\n左:Mac Studio,右:穷逼版\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374176-9667385a-dc3d-414e-86b6-f6c88cb4afc7.png)\\r\\n与 24 寸显示器的对比\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374261-61910710-fe87-40c8-91a8-c59bcd20b5c0.png)\\r\\n与 330ml 杯子的对比\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374347-dadcac5b-fde2-4b7f-a74c-c0e9da1b257b.png)\\r\\n日常使用负载\\r\\n\\r\\n## 长话短说\\r\\n|性能对比|CPU|内存|硬盘|性能(R23)|价格|体积|\\r\\n|-|-|-|-|-|-|-|\\r\\n|Mac Studio|M1 Ultra|64GB|1TB 固态|多核 23705|29999|197mm * 197mm * 95mm = 3.68L|\\r\\n|穷逼版|i7 12700|64GB|1TB 固态|多核 21568|4946|196mm * 218mm * 117mm = 4.9L|\\r\\n\\r\\n- 本文只面向程序员群体,优先保证编译性能和开发舒适度,不面向视频剪辑等创意工作者,不考虑视频剪辑性能;\\r\\n- 重点考虑 CPU 单核和多核性能,优先内存和硬盘容量,考虑机箱体积,不太考虑内存频率,不追求硬盘速度,不太考虑显卡性能,不太考虑噪音,完全不考虑外观精致程度;\\r\\n- 优点:便宜,量大管饱,装机完成后,可当做白苹果使用,无痛升级后续系统,可支持 MacOS、Windows 和 Linux 三系统共存;\\r\\n- 缺点:折腾,组装机箱和安装系统需要有一定动手能力,不适用于日薪大于 1w 的用户,不适用于无计算机硬件常识用户;\\r\\n- 硬件到位之后,在网络良好情况下,半天即可完成装机;\\r\\n- 多核编译同一个项目,M1 Max 需 6min,本文主机需 4min,多核性能对比与 R23 跑分对比大致保持一致。\\r\\n\\r\\n\\r\\n## 阅读须知\\r\\n\\r\\n- 适用群体:\\r\\n - 写代码的\\r\\n - 买不起 Mac Studio 的\\r\\n - 有一定的折腾能力的\\r\\n- 不适用群体:\\r\\n - 打游戏的\\r\\n - 富哥富姐请直接使用钞能力\\r\\n - 剪视频的\\r\\n - 懒得折腾的(可以购置硬件后,购买淘宝黑苹果装机服务)\\r\\n- 本文包含以下内容:\\r\\n - 提供了购买硬件和装机过程中的一些需要考虑的事项\\r\\n - 提供了分别对标 M1 Mac mini、M1 max Mac studio 和 M1 ultra Mac studio 性能(不含 GPU 和磁盘性能)且同等体积的装机购置清单\\r\\n - 提供了安装黑苹果所需的硬件考虑事项以及系统安装须知\\r\\n\\r\\n## 为什么需要装这样一台机器\\r\\n\\r\\n> 可以跳过此章节。\\r\\n\\r\\n总而言之,对于我来说,主要有两个原因:\\r\\n\\r\\n- 主要原因:公司电脑性能拉跨,满足不了开发需要;\\r\\n\\r\\n- 次要原因:目前在用的 MacOS 13 Ventura 系统拉跨,降级麻烦。\\r\\n\\r\\n所以,我需要搞一台高性能主机来替换掉公司发的老旧 MBP。\\r\\n\\r\\n其实公司配的 2020 款 MBP 并不算老,就是性能有点差,10 代标压 i5-1038NG7 处理器加 16GB 板载内存,多核性能大概是 M1 的一半,省着点还是可以用的。\\r\\n\\r\\n但我在 MacOS 13 技术预览版刚放出来时候,就迫不及待地手贱升级了一波,然后成功被新系统的各种卡顿 Bug 和内存占用虚高问题治好了低血压。即便到如今已经发布了正式版,这些问题仍旧存在,以至于日常开发时随便开个 VS Code 再加几个网页,内存就基本见底了。\\r\\n\\r\\n后来为了能保证正常开发,我不得不花一周时间把 Vim 打造成主力 IDE,以便直接在开发机上写代码。\\r\\n\\r\\n此外,除了 MacOS 拉跨,公司的项目本身也对设备性能要求很高。\\r\\n\\r\\n身后的同事老哥前段时间斥巨资入手了一款中配 M1 Max 款 Mac Studio,原因就是公司发放的设备很难满足开发需求。我们日常的技术栈是 C++ 和 React,前者在我那台十代标压 i5 上,全量编译整个项目需要花费 20 分钟到 30 分钟的时间,编译个几次项目,半天就没了,倒是非常适合划水。虽然大多数时间并不会全量编译,但每次增量编译耗时也在分钟级别,非常可观,十分难受。\\r\\n\\r\\n后者 React 更是重量级,NodeJS + HMR + 浏览器堪称内存黑洞,再配合 C++ 建立的 Clang 符号表索引,16GB 板载内存直接被榨干,一天下来大多数时间都在等编译和页面响应。\\r\\n\\r\\n当然这也并不是完全没有好处,由于等待的成本过于高昂,我们写下每行代码前都要深思熟虑,每次编译前都要检查个半天,所以那段时间的代码质量总感觉也高了不少 😆。\\r\\n\\r\\n后来我们安排了一台 36 核 72 线程(含两颗 18C36T Intel Xeon Gold 6240 CPU)的开发机,第一次用它编译项目的时候,有种便秘两周之后突然窜稀的畅快感觉,以前 20 多分钟的编译,现在两分钟就完事了,比以前跑单测都快。\\r\\n\\r\\n然而即便用了开发机和 Vim 之后,我还是经常被系统卡顿和内存不足烦得要死,随便开几个网页,该卡还是卡,就这样将就了两个多月,在某次系统彻底无响应之后,我一咬牙一狠心,决定加亿点钱解决这个问题。\\r\\n\\r\\n于是我便问了下身后老哥的 M1 Max 的编译时间,居然只要 6 分钟,已经完全够用了,怪不得他不怎么用开发机,我马上点开了苹果官网:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200374857-d6067e58-1765-4da2-8929-a4b58b24b145.png)\\r\\n\\r\\n然后看到这个价格我火速关闭,那没事了,告辞。\\r\\n\\r\\n看来富哥的路是走不通了,只能使用穷逼的道路曲线救国了。\\r\\n\\r\\n## 性能\\r\\n\\r\\n苹果在发布会上把 M1 系列芯片吹得天花乱坠,拳打 Intel i9-11900,脚踢 RTX 3090,让我们来看看它的真面目:\\r\\n\\r\\n|芯片名称|规格|R23 单核跑分|R23 多核跑分|功耗|\\r\\n|-|-|-|-|-|\\r\\n|Intel Core i5 1038NG7|4 核 8 线程|1116|4979|28W|\\r\\n|Apple M1|8 核|1510|7687|6.8W ~ 39W|\\r\\n|Intel Core i5 12400|6 核 12 线程|1697|11905|65W|\\r\\n|Apple M1 Max|10 核|1533|12337|11W ~ 115W|\\r\\n|Intel Core i7 12700|12 核 20 线程|1879|21568|PL1: 65W, PL2 180W|\\r\\n|Apple M1 Ultra|20 核|1506|23705|13W ~ 215W|\\r\\n|Intel Core i9 12900|16 核 24 线程|1988|26455|PL1: 65W, PL2 200W|\\r\\n\\r\\n确实很强,比 10 代 intel 强多了,但是面对 12 代 intel 还是不太行,毕竟功耗在这摆着,i9 12900 勉强守住了桌面 CPU 的底裤。\\r\\n\\r\\n苹果 M1 系列的能耗比确实惊人,但是我要放在公司使用,完全不用担心电费问题,功耗都是图一乐,于是经过一周的调研,确定了硬件配置单。\\r\\n\\r\\n## 硬件配置单\\r\\n\\r\\n> 下列配置中的硬件,除显卡和网卡外,均为全新价格。\\r\\n\\r\\n> 如需查看更多配置单(M1 / M1 Max 级别),请跳转到文章末尾。\\r\\n\\r\\n|部件|产品|价格|备注|\\r\\n|-|-|-|-|\\r\\n|CPU|Intel i7 12700f 散片|2110|f 后缀代表不集成核显,比带核显版本便宜 100 块左右|\\r\\n|主板|铭瑄 H610itx 挑战者|569|最便宜的能带得动 i7 12700 的 itx 主板|\\r\\n|内存条|酷兽 32GB 3000Mhz 两根共 64GB|798|最便宜的 DDR4 内存,有 2666mhz / 3000mhz / 3200Mhz 三个版本,价格一样,哪个便宜有货买哪个|\\r\\n|硬盘|光威弈 Pro 512GB nvme ssd|295|随便选的,可以加一百多块上 1TB 固态,反正便宜|\\r\\n|散热器|利民 Axp90 x53 + LGA17xx 扣具|159|选购时需要考虑机箱适配的散热器高度,需要加 18 元额外购买利民 LGA17xx 扣具|\\r\\n|机箱|傻瓜超人 K39 4.9L + 显卡延长线|199|196mm * 117mm * 218mm = 4.9L 体积,当前配置的体积最小最便宜的选择|\\r\\n|电源|益衡 7030B 300W Flex 1U 电源|398|这个价格是加 100 块让店家换全模组电源线和静音风扇之后的版本,组装的时候理线会更方便|\\r\\n|显卡|AMD RX550 2GB/4GB Polaris 核心|300|二手亮机卡,有 Lexa 和 Polaris 核心两个版本,建议买 4GB 显存 Polaris 核心版本,各版本价格无区别|\\r\\n|网卡|博通 BCM94360CS2 + m2 ngff 正向转接卡|118|也可以选择 Intel AX201 或者更低端的 Intel 9560,能省个几十块钱,但需要额外配置驱动,没必要|\\r\\n\\r\\n我们一项一项来看,为什么要选用这些配置。\\r\\n\\r\\n### CPU 和主板\\r\\n\\r\\n首先,要再次明确一下目标,我们想要 M1 Ultra 级别的性能,那就只能在 i7 12700 及以上的 CPU 中选,大概有这么几个选项:\\r\\n\\r\\n- Intel i7 12700f 和 12700\\r\\n\\r\\n- Intel i7 12700kf 和 12700k\\r\\n\\r\\n- Intel i9 12900f\\r\\n\\r\\n- Intel i9 12900kf 和 12900k\\r\\n\\r\\n**为什么不选 AMD 的 CPU?**因为性价比不高,12 代 CPU 同价位性能比 AMD 5000 系列同价位性能稍强,价格稍便宜,而且目前 AMD 装黑苹果需要额外配置,有点麻烦,所以完全没有理由选 AMD。\\r\\n\\r\\n**为什么不选 13 代 Intel CPU?**因为性价比不高,目前只发布了带 K 后缀的高性能版本,需要搭配更好的主板和电源,而且 13 代 CPU 也相对更贵。\\r\\n\\r\\n然后,i7 和 i9 分别都有 f / k / kf 几个版本:\\r\\n\\r\\n- 其中凡是带 f 的,都是不带集成显卡的版本,考虑到我们是装黑苹果,12 代集成显卡不能被驱动,所以必须得配个独立显卡,选择 f 版本就行,可以省下 100 块;\\r\\n\\r\\n- 其中凡是带 k 的,都代表着性能增强版,大概比不带 k 的版本性能强 10% - 15%,价格也要贵 5% - 10%,酌情购买。\\r\\n\\r\\n由于 i9 处理器需要主板有更强的供电能力,主板的价格就水涨船高,所以我们退而求其次,选择 i7 12700,性能只比 M1 Ultra 弱 10%。当然,也可以加点钱上 12700k,这样性能和 M1 Ultra 持平,不过这点差距在实际使用时基本没什么区别,12700k 对主板供电能力也有一定的要求,丐板估计带不动。\\r\\n\\r\\n主板就不提了,铭瑄 H610itx 在 600 块价位基本无敌,找不到其他能一起打的。唯一缺点就是 itx 主板接口有点少,只能插两根内存条,目前消费级内存条单条最大也就 32GB,所以最多只能上 64GB 内存。\\r\\n\\r\\n而且这种低端丐板没有额外的 M.2 接口,只能插一条 Nvme 固态,要扩容的话,要么换固态,要么加 SATA 硬盘。不过还好 512GB SSD 够用了,即便是 1TB 的 SSD 也只要 400 出头,可以自己决定要不要加钱。\\r\\n\\r\\n购买建议:\\r\\n\\r\\n- 购买散片板 U 套装即可\\r\\n\\r\\n### 内存条和硬盘\\r\\n\\r\\n酷兽和光威是同一个厂,不同的产品线,酷兽主打性价比,399 块的 32GB 内存条要啥自行车,直接来两条插满。\\r\\n\\r\\n此外这个内存条有 2666mhz / 3000mhz / 3200mhz 三个频率版本,理论上铭瑄 h610itx 主板是支持 3200mhz 高频内存的,只需要在 BIOS 里设置一下就行,但是说实话内存频率感知不强,只有核显打游戏才对内存频率有要求,如果用来开发,随便买就行,高频内存属实浪费预算,哪个有货买哪个。\\r\\n\\r\\n最近固态硬盘也是白菜价,1tb 固态已经降到了 400 以下,当然也有更贵的性能更好的版本,但是对于开发来说感知不强,3000mb/s 和 7000mb/s 虽然看起来差距挺大,但是实际用的时候也就几百毫秒的差距,感知不出来,所以随便买个便宜的就行,重点还是看容量。\\r\\n\\r\\n购买建议:\\r\\n\\r\\n- 哪个便宜买哪个,频率和读写速度并不重要;\\r\\n- Mac Studio 的统一内存,官方读速是 800GB/s,与 3200mhz DDR4 内存条读速差不多;\\r\\n- 如果磁盘性能也想对标一下 Mac Studio,Mac Studio 硬盘速度为:读 5000MB/s,写 4000MB/s,大致与 PCIE 4.0 固态速度接近,所以可以酌情购买 PCIE 4.0 固态硬盘(铭瑄的这块主板支持,精粤的不支持),同体积大概是 PCIE 3.0 硬盘的 1.5 倍价格,512GB 版本大概 400 块可以搞定。\\r\\n\\r\\n\\r\\n### 散热器、机箱和电源\\r\\n\\r\\n这三兄弟要一起看,其实显卡也应该拿过来一起看,但是开发场景,显卡不是很重要,所以显卡另说吧。\\r\\n\\r\\n之所以要放一起看,主要是尺寸问题,我想要体积尽可能接近 Mac Studio,Mac Studio 的体积是 197mm * 197mm * 9.5mm = 3.68L,一开始我买的是下面这个机箱:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375317-17a17f5d-b8ae-424d-aab5-3cf3054e979e.png)\\r\\n\\r\\n整体尺寸是 187mm * 230mm * 97mm = 4.2L,非常小巧。\\r\\n\\r\\n我选择了其中的 A 背板版本,没有显卡位,把空间全部让给散热器和电源,散热器也是买的 Axp90 x53(90mm 宽,53mm 高)。\\r\\n\\r\\n因为我一开始并没有打算装黑苹果,想直接用 Linux 来开发,但是后来发现 Linux 没办法入公司内网,而且企业微信也是残废状态,所以才考虑装黑苹果,于是换成了下面的机箱:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375363-2379fe91-e33e-45f3-816a-1c58aad6c11a.png)\\r\\n\\r\\n体积大了一圈,来到了 196mm * 117mm * 218mm = 4.9L,不过好在便宜,加显卡延长线才 200 块,比上一个 300 块的机箱便宜很多,而且可以加装一个 19cm 以内的双槽短卡,非常适合本文的需求。\\r\\n\\r\\n从上面机箱的商品介绍就可以知道,这个机箱支持 56mm 以内的散热器,一般来讲,散热器的尺寸与散热能力正相关,基本上体积越大,散热能力越好,在看了一圈视频之后,出现次数的就是利民的axp90 系列的几个散热器,价格在 100 元到 150 元之间:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375397-361c99d5-7cf0-489b-837e-f4fb5244390f.png)\\r\\n\\r\\n可以看到主要有 36mm / 47mm / 53mm 三种高度,名字 AXP90 则代表 90mm * 90mm 的尺寸,为了最大化机箱利用率,我选用了 53mm 高度版本,实测整机满载温度可以控制在 80 摄氏度以内,日常使用则只有 27 摄氏度左右,好悬没给 CPU 给冻感冒。\\r\\n\\r\\n此外,如果你想选用一些更小尺寸的机箱,在选散热器的时候一定要注意,最好预留 3mm 以上的空间余量,比如如果限高 56mm 的机箱,如果配了一个 55mm 高度的散热器,风扇叶片和机箱离得太近,会出现十分明显的啸叫。\\r\\n\\r\\n然后是电源,前面了解到 i7 12700 的满载功耗是 180w,再加上 50w 亮机显卡的功耗,总计 230w 的实际载荷,按照 80% 的电源转化率,我们只需要选择 290w 以上的电源即可满足需求。在尺寸方面,由于我们想让机箱的体积尽可能的小,而小尺寸 itx 机箱往往搭配使用小 1u 尺寸的 Flex 电源,这类电源的价格要比常规尺寸的电源稍贵一点,如果想要省钱,可以选购支持 SFX 电源的更大尺寸的机箱。\\r\\n\\r\\n最终我选择了益衡的 7030B 300w 电源:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375452-b24da35f-415b-48cc-8953-af0ebe584500.png)\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375491-e805645b-fc76-4d2c-957f-03bb3a463178.png)\\r\\n\\r\\n小 1u 电源还有个缺点是会比较吵,因为受体积限制,风扇必须提高转速才能保证散热,运行时会有比较明显的高频噪声,所以我选择加了 100 块,买了店家改装之后的版本(上图右侧)。\\r\\n\\r\\n改装版本换了更静音的风扇,并且配备了全模组电源线,大概长下面这个样子:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375578-2df51c37-0c56-486c-ba22-d36fd79793b7.png)\\r\\n\\r\\n电源线统一成了黑色的软线,比原装的五颜六色的硬线好看多了,而且装机的时候理线会非常简单,这一点在小机箱里尤其关键,所以这 100 块还是挺值的。\\r\\n\\r\\n购买建议:\\r\\n- 购买全模组小 1u 电源,考虑购买全模组 + 静音风扇的改装版本\\r\\n- 购买散热器时注意机箱散热器限高\\r\\n- 利民散热器默认附带的主板支架只能与上一代 Intel CPU 搭配使用,需要额外花费 18 元购买利民 LGA17xx 扣具\\r\\n\\r\\n### 显卡\\r\\n\\r\\n显卡需要着重讲一下,因为 MacOS 非常挑显卡,目前市售的 RTX 系列显卡基本都是驱动不了的,只要一些比较老的 Nvidia 显卡才能驱动。而大多数 AMD 显卡都能驱动,比如最近出的 6600 和 6700,但是 6400 除外,详细的 GPU 列表可以[查看这里](https://dortania.github.io/GPU-Buyers-Guide/modern-gpus/amd-gpu.html#native-amd-gpus)。\\r\\n\\r\\n由于我们并不需要特别强劲的显卡性能,所以只需要选择能够正常驱动的低功耗亮机卡即可,我一开始就直接从比较老的 R7 和 R9 系列显卡开始看:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375686-da4a53e8-daf6-4927-87dd-b5586b5628cc.png)\\r\\n\\r\\n可以看到有非常多的选项,我在淘宝搜了一圈之后,发现最低端的 R7 240/250 显卡只要 100 块,于是就直接下单买了一块,结果入手后折腾了许久才发现,这张 2013 年发售的老显卡,最高只能运行在 MacOS 10.x 系统上,最新的 12.x 系统是没法完美驱动的。我花了大量的时间搜索国内外关于这张 Oland 核心的显卡驱动帖子,没有发现任何人能成功在 11.x 及以上的系统上成功运行过它,虽然可以使用仿冒显卡 ID 的方法成功安装并进入系统,但是却无法使用 Metal GPU 加速,导致系统所有的高斯模糊效果以及动效都是缺失的,完全无法满足日常使用。\\r\\n\\r\\n于是我就又斥巨资花了 300 块买了一张 Rx550,这张显卡发售于 2017 年,网上有相当多的黑苹果视频表示这张卡可以完美工作在最新的 MacOS 13.0 系统上,而且完全不需要任何额外操作,插上就能用。当然,前提是买到了正确的版本的显卡,这张显卡有两种核心版本:Lexa 核心和 Buffin 核心(或称 Polaris 核心),其中 Buffin(Polaris) 核心是可以即插即用,但是 Lexa 核心则需要[仿冒参数](https://www.bilibili.com/read/cv15800495)才能工作。\\r\\n\\r\\n很不幸我买的这张 Rx550 2GB 联想拆机卡是 Lexa 核心,不过由于有了之前折腾 R7 240 的经历,对我来说还算简单。\\r\\n\\r\\n此外这张显卡有 2GB 显存和 4GB 显存两个版本,价格基本一样,我个人建议购买 4GB 版本,因为我在实际使用时发现,MacOS 12.x 系统很容易就把 2GB 显存吃满了,所以能买大的还是买大点的。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375752-81118659-e6fe-443e-bf82-bad353cf5940.png)\\r\\n\\r\\n盈通的这张 RX550 4GB 就很适合,虽然是二手,有翻车的风险,但是我们并不会用来打游戏,所以只要它能点亮就行,反正日常负载不会太高,暴毙的可能性比较小。\\r\\n\\r\\n如果实在担心翻车,可以加点钱买全新版本,这张卡有全新正品在售,价格在 500 块到 600 块之间:\\r\\n\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375812-0c4657dc-fded-407c-836b-132ded517957.png)\\r\\n\\r\\n购买建议:\\r\\n\\r\\n- 购买 RX550 4GB Polaris 核心版本\\r\\n- 省钱就买二手,图稳就加点钱买全新\\r\\n- 购买显卡时注意显卡尺寸是否兼容机箱\\r\\n\\r\\n### 无线网卡\\r\\n无线网卡也值得花点篇幅来说一说,目前黑苹果无线网卡可选 Intel NGFF 接口的一系列网卡,以及搭配转接板使用的博通 BCM 系列苹果拆机显卡:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375892-727c87ae-e39e-48eb-8dfb-fe4495c209cf.png)\\r\\n\\r\\n如果从省事的角度来看,直接买博通的拆机网卡即可,插上就能用。如果想省点钱,就买 Intel 最低端的 3168 系列:\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200375922-f435efa8-1651-462d-bfb6-8cddf0228072.png)\\r\\n\\r\\n其实在几年前,Intel 的无线网卡是没办法在苹果系统上使用的,后来有个大佬搓了一个驱动出来,然后 Intel 几乎所有型号的无线网卡一夜之间焕发第二春,具体的支持列表可以查看下面的链接:[Compatibility | OpenIntelWireless](https://openintelwireless.github.io/itlwm/Compat.html#dvm-iwn)。\\r\\n\\r\\n此外无线网卡的天线,有内置和外置两种区别:\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n\\r\\n两者的区别:\\r\\n- 外置天线是机箱后面的小尾巴,看起来可能不太美观,但是信号比较稳定,增益较强;\\r\\n- 内置天线则需要粘在机箱上,容易受机箱内部杂波干扰,增益较小,信号不太稳定,但由于不需要伸出机箱,所以会比较美观一点。\\r\\n\\r\\n我选择了 Intel 9560 内置天线版本,花费 48 块,实装之后可以完美使用 WIFI 和蓝牙,隔空投送也可以正常使用,但是内置天线信号不稳定,蓝牙连接妙控板非常卡顿,基本无法日常使用。而且由于网卡规格较低,虽然标称 2.4G 300Mbps / 5G 1733Mbps,但实测只能跑到 50Mbps,可能最大的原因还是这个内置天线的信号太差,干扰太多。\\r\\n\\r\\n不过问题不大,这个主机的 WIFI 纯粹是为了应付公司的入网认证,认证完了之后就直接关掉用有线连接了,如果你对无线网络有需求,建议购买高规格的外置天线版本。\\r\\n\\r\\n购买建议:\\r\\n- 购买博通 BCM 94360CS + 正向转接卡 + 外置天线版本,不用折腾,信号更好。\\r\\n\\r\\n\\r\\n## 装机\\r\\n\\r\\n十分推荐先观看此视频:[DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!](【DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!】 https://www.bilibili.com/video/BV1v54y1Z7Er/?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9),视频中的配置与本文配置基本一致,可以参考。\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n先上所有硬件的全家福,图中不包含显卡和网卡,因为拍摄照片的时候显卡和网卡还没到。\\r\\n\\r\\n### 安装 CPU\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n主板说明书会提示如何安装 CPU,一定要小心不要弄坏了针脚,主板上的金属压杆会比较紧,确认 CPU 按照防呆口安装妥当后,稍加用力按下压杆,将 CPU 固定好即可。\\r\\n\\r\\n### 上电测试\\r\\n先别急着往机箱里塞,把内存条和硬盘插好之后,连接电源线和键鼠,用螺丝刀碰触主板上的开机引脚,显示器能够显示 BIOS 界面,则表示成功点亮。\\r\\n\\r\\n### 安装散热器\\r\\n按照散热器说明书进行安装即可,注意需要额外购买利民 LGA17xx 扣具。\\r\\n\\r\\n### 安装机箱\\r\\n按照机箱说明书安装即可,建议先安装电源,再安装主板,然后接电源线,安装显卡延长线,安装显卡。\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n## 成品展示\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n\\"image\\"\\r\\n\\r\\n最终的成品大概比 Mac Studio 大了一圈,当然精致程度是没法比的,如果想要精致,可以加钱买更贵的全铝合金一体成型机箱(5.6L 体积):\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200376871-9d4540ef-a61e-44cf-b8f7-576fc212d69e.png)\\r\\n\\r\\n## 系统\\r\\n可以直接跳转到流程清单部分,如果你不需要安装 MacOS,可以无视下列内容。\\r\\n\\r\\n### 为什么不用 Windows 和 Linux?\\r\\n在最开始决定装机的时候,我就在考虑要使用什么操作系统作为主力开发环境,前文已经提到我需要频繁编译,所以编译性能是我首要考虑的问题,这样 Window 肯定就不能用了,Windows 下 C++ 编译性能一向令人诟病,即便有了 WSL,由于它运行在 Hyper-V 虚拟机中,导致 IO 性能跟不上,实际的性能大概是 Linux 原生的 80% 左右,直接损耗了 20% 的性能,要知道 Intel 的 CPU 一次代际升级的性能也才差不多 25%,Window 直接就让 CPU 性能倒退一代,这绝对是无法接受的。\\r\\n\\r\\n我随后的想法是使用 Linux 作为日常开发系统,并使用 KVM 安装 Windows 虚拟机来解决常用软件问题,我也确实尝试了这种做法,并依次使用了下列 Linux 发行版:\\r\\n\\r\\n- 使用 KDE 桌面的 Debian,由于 Stable 版本的 Linux 内核太老,导致无法在 12 代 Intel 平台上正常安装,尝试几次后均未果,作罢;\\r\\n- 使用 KDE 桌面的 Ubuntu,也就是 KUbuntu,这个发行版确实很不错,KDE 对高分屏的支持非常不错,而且窗口主题和动效都很细腻,我用了大概一周时间,在上面使用 KVM 分别安装了 Windows 10 LTSC 以及 Mac OS 12 的虚拟机,前者用于运行企业微信,后者则是装来玩的,由于没有独显直通,Mac OS 虚拟机的运行流畅度十分感人。在 Linux 上运行 Windows 软件的另一个方法是使用 Wine,但无奈兼容性欠佳,无法正常运行最新版本的企业微信,微信倒是可以正常使用;\\r\\n- 使用 Gnome 的 Ubuntu,Gnome 对高分屏的支持太差劲,大多数应用没有对 Wayland 做兼容,甚至浏览器在分数倍缩放下都很糊,而且同样无法通过 Wine 运行企业微信,作罢;\\r\\n- 最后是国产的 Deepin,应该是最贴近日常使用的 Linux 发行版了,应用商店里有 Deepin 团队维护的 Wine 版微信和企业微信,但是企业微信还有点问题,虽然可以正常聊天,但是邮箱界面始终白屏,另外 Tim 也是卧龙凤雏,可以安装但无法启动。\\r\\n\\r\\n我大概用了一周多的 Deepin,并成功在上面编译运行了公司的项目,编译时间大概是开发机的 2 倍速度,可以看到 i7 12700 的多核性能相当强悍。\\r\\n\\r\\n但是网络问题始终没有办法很好的解决,公司的内网认证软件是阿里出品的阿里郎的阉割版本,其入网认证条件非常严格,无法在 Windows 虚拟机中完成认证,更不用提 Wine 了,也没有提供 Linux 版本,导致我的主机就算插上网线也无法访问内网资源,而且这个软件入网必须要有无线网卡,大多数 USB 网卡都无法在 Linux 下免驱运行。\\r\\n\\r\\n为了解决这个问题,我琢磨了很久计算机网络相关的知识,大概有这么几种方法:\\r\\n\\r\\n- 最容易想到的方法自然是用公司的苹果笔记本入网,然后当跳板机开代理给主机用,确实可以用,但是基于代理的方法,无法保证主机的所有流量都走代理,某些应用可能会绕开系统的代理策略,自行建立连接,这种方法不太稳定;\\r\\n- 然后比较容易想到的是使用 Mac 的网络共享,无奈在入网认证使用 802.1x 的安全策略,系统会直接禁用掉网络共享,这条路也走不通;\\r\\n- 最后想到的方法时把 Mac 模拟成一台路由器,直接网线直连主机,然后手动分配 IP 地址即可,经过一番搜索,其实只要开启 Mac 的 IP 路由转发即可,然后将来自 USB 网卡的所有流量都转发到无线网卡上,即可大功告成。\\r\\n\\r\\n关于折腾 Linux 日用开发环境的记录,可以看这篇文章:\\r\\n\\r\\n[Linux 开发环境备忘录](https://flowus.cn/5a11ca75-455d-4a67-840a-42939ef91c2c)\\r\\n\\r\\n最后由于 Linux 上无法找到顺手的快速切换窗口的软件(类似 Mac Spotlight 的功能),还是决定安装黑苹果,可以直接无缝复用 MBP 的那一套工作流。\\r\\n\\r\\n### 主要工具:OpenCore\\r\\n安装黑苹果是一件非常需要耐心的事情,目前主流的安装黑苹果的工具是 OpenCore,其官方教程非常详细,流程也非常长,新手看了也非常头大:\\r\\n\\r\\n[OpenCore Install Guide](https://dortania.github.io/OpenCore-Install-Guide/)\\r\\n\\r\\n在翻阅许久之后,我决定放弃跟着官方教程走,官方手册的内容只适合当作速查表来使用,如果跟着从零开始做,不知道搞到猴年马月。\\r\\n\\r\\n先列举一下我认为比较有用的内容,首先是显卡购买指南:\\r\\n\\r\\n[Introduction | GPU Buyers Guide](https://dortania.github.io/GPU-Buyers-Guide/)\\r\\n\\r\\n里面列举了各种可以用于黑苹果的显卡型号,以及它们支持的系统版本,当然这个指南中的某些老显卡的信息是错的,比如我前面提到的 R7 240,手册上说可以在最新的系统中通过仿冒 ID 的方式运行,但其实只能在 OS 10.x 以前的系统上运行,不过大多数信息都是准确的,可以放心参考。\\r\\n\\r\\n其次是 GPU 仿冒 ID 指南,如果你想折腾一下不直接免驱的显卡,可以参考它来仿冒 ID:\\r\\n\\r\\n[Renaming GPUs (SSDT-GPU-SPOOF) | Getting Started With ACPI](https://dortania.github.io/Getting-Started-With-ACPI/Universal/spoof.html)\\r\\n\\r\\n我个人不推荐用这种方式来搞,太费时间,而且不一定有用,还是建议直接购买免驱显卡。\\r\\n\\r\\n### 流程清单\\r\\n如果自己搞不定,可以直接在淘宝购买黑苹果装机服务,远程手把手指导,价格在 100 元到几百元不等。\\r\\n\\r\\n可以看这个视频,了解大概步骤:[刷新记录!12代平台12400F仅需24秒即可完成黑苹果系统安装,请问还有谁?](https://www.bilibili.com/video/BV1K341137pj/?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9)\\r\\n\\r\\n通用流程:\\r\\n\\r\\n- 硬件准备:\\r\\n - 16GB 及以上容量的内存卡或者移动硬盘,并格式化为 FAT32 格式;\\r\\n - 一台 Windows 电脑(Linux 或 Mac 也行);\\r\\n - 组装好的等待安装系统的主机;\\r\\n - 良好的有线网络环境;\\r\\n - 耐心。\\r\\n- 准备 EFI 引导文件:[各版本 EFI 引导文件](https://flowus.cn/6bb40d7b-8da6-464b-a551-0d0f0d663746)\\r\\n - 如果你的硬件配置是本文推荐的配置,那么直接下载上面列表中的安装包,解压即可;\\r\\n - 或者你可以在 Github 上搜索自己的主板名称,比如我就参考了这个仓库([h610itx + i5 12400 + rx560 + bcm943602cs 网卡](https://github.com/Crack-DanShiFu/Hackintosh-MAXSUN--H610ITX-I512400-rx560))和这个仓库([h610itx + i5 12490f + rx560 + intel ax201 网卡](https://github.com/LimeVista/Hackintosh-H610-12490F-AX201)),在他们的基础上进行了少量修改,就适配了本文的硬件;\\r\\n - 经过上述步骤,你会获得一个 EFI 文件夹,将其拷贝到 U 盘或者移动硬盘中即可,如果你的硬件与上述现有的配置都不一样,请查阅常见问题章节。\\r\\n- 准备基础安装镜像:\\r\\n - OpenCore 提供了一个很细致的教程来下载安装镜像,基础镜像大概 600MB 左右,按照这个教程([在 Windows 上下载安装镜像](https://dortania.github.io/OpenCore-Install-Guide/installer-guide/winblows-install.html#downloading-macos))下载镜像文件并复制到 U 盘即可。\\r\\n - 将 U 盘插入主机,按下 DEL 键进入主板的 BIOS 界面,如果你使用了其他主板,请自行搜索如何进入 BIOS,然后将 U 盘设置为第一启动项即可;\\r\\n- 启动启动后进入安装界面:\\r\\n - 选择磁盘工具,将 SSD 所在盘符格式化为 APFS 格式,并命名为 MacOS(或者其他任何英文名);\\r\\n - 退出磁盘工具,点击安装 MacOS,进入常规安装流程;\\r\\n- 在网络良好情况下,自动重启若干次后,大概 40 分钟即可安装成功;\\r\\n- 安装成功后,自动重启进入系统选择界面,选择 MacOS 盘符进入即可;\\r\\n- 将 EFI 文件夹复制到 SSD 硬盘上,可参考这个教程:[完善引导](https://apple.sqlsec.com/5-%E5%AE%9E%E6%88%98%E6%BC%94%E7%A4%BA/5-6.html);\\r\\n- 到现在,你已经可以正常使用 MacOS,但是还无法使用 Apple ID 和 iCloud,你还需要生成自己的硬件序列号,请参考这个教程:[为自己的黑苹果生成随机三码](https://sleele.com/2019/03/21/smbios/);\\r\\n- 至此,大功告成。\\r\\n\\r\\n## 常见问题\\r\\n\\r\\n### 如何查看我买到的显卡的核心?\\r\\n\\r\\n#### 搜索\\r\\n\\r\\n1. 优先询问卖家,其次在搜索引擎中搜索显卡型号 + 核心即可。\\r\\n\\r\\n#### 使用 PE 工具\\r\\n\\r\\n1. 下载安装[微 PE 工具箱](https://www.wepe.com.cn/download.html),并安装到 U 盘上;\\r\\n2. 下载 GPU-Z:[https://www.techpowerup.com/gpuz/](https://www.techpowerup.com/gpuz/),复制到已经安装了 PE 工具的 U 盘中;\\r\\n3. 进入 BIOS,将 U 盘设置为第一引导,进入 PE 系统,运行 GPU-Z,即可看到显卡核心。\\r\\n\\r\\n![image](https://user-images.githubusercontent.com/16968934/200377738-ce96c52f-d78e-43b8-8e02-922d58260553.png)\\r\\n第一个红框处,即是显卡核心名称。\\r\\n\\r\\n#### 使用现有的 Windows 主机\\r\\n\\r\\n1. 把显卡插到现有的 Windows 主机上;\\r\\n2. 下载安装 GPU-Z,即可看到显卡核心。\\r\\n\\r\\n### 买到了 Lexa 核心的 Rx550,该怎么办?\\r\\n\\r\\n参考这个教程,仿冒 ID 即可:\\r\\n\\r\\n[【黑苹果】通过仿冒ID驱动Lexa核心的Radeon RX 550](https://www.bilibili.com/read/cv15800495)\\r\\n\\r\\n### 12 代处理器如何开启小核心支持?\\r\\n\\r\\n如果你是在别人的 EFI 文件基础上修改,并且别人的 EFI 文件基于 12 代 i3 或者 i5 处理器,而你的处理器是 12 代 i7 及以上,则需要开启小核心支持,查阅:\\r\\n\\r\\n[黑苹果开启十二代酷睿能效核心的驱动_豆豆本豆儿的博客-CSDN博客](https://blog.csdn.net/Z17362251225/article/details/125412246)\\r\\n\\r\\n## 装机单\\r\\n> 价格可能略有浮动。\\r\\n\\r\\n> M1 Max 和 M1 Pro 性能几乎一致,不作区分。\\r\\n\\r\\n### M1 Mac Mini 同等性能(3155 元,32GB + 512GB SSD)\\r\\n|部件|产品|价格|备注|\\r\\n|-|-|-|-|\\r\\n|CPU|Intel i3 12100f 散片|668||\\r\\n|主板|铭瑄 h610itx 挑战者|568||\\r\\n|内存条|酷兽夜枭 16GB 3200Mhz 两根共 32GB|470|京东自营即可|\\r\\n|硬盘|光威弈 Pro 512GB nvme ssd|295|按自己喜好选即可|\\r\\n|散热器|利民 Axp90 x47 + LGA17xx 扣具|139|不用太强力的散热器,随便压一压|\\r\\n|机箱|傻瓜超人 K39 + 定制显卡延长线|200|一定要买定制显卡延长线|\\r\\n|电源|益衡 7030B 300W Flex 1U 电源|398|记得买全模组定制版本|\\r\\n|显卡|盈通 Rx550 4GB|299|淘宝二手,记得询问客服是否是 Polaris 核心|\\r\\n|网卡|博通 BCM94360CS2 + m2 ngff 正向转接卡|118|记得买外置天线|\\r\\n\\r\\n### M1 Max Mac Studio 同等性能(3537 元,32GB + 512GB SSD)\\r\\n\\r\\n|部件|产品|价格|备注|\\r\\n|-|-|-|-|\\r\\n|CPU|Intel i5 12400f 散片|1050||\\r\\n|主板|铭瑄 h610itx 挑战者|568||\\r\\n|内存条|酷兽夜枭 16GB 3200Mhz 两根共 32GB|470|京东自营即可|\\r\\n|硬盘|光威弈 Pro 512GB nvme ssd|295|按自己喜好选即可|\\r\\n|散热器|利民 Axp90 x47 + LGA17xx 扣具|139|不用太强力的散热器,随便压一压|\\r\\n|机箱|傻瓜超人 K39 + 定制显卡延长线|200|一定要买定制显卡延长线|\\r\\n|电源|益衡 7030B 300W Flex 1U 电源|398|记得买全模组定制版本|\\r\\n|显卡|盈通 Rx550 4GB|299|淘宝二手,记得询问客服是否是 Polaris 核心|\\r\\n|网卡|博通 BCM94360CS2 + m2 ngff 正向转接卡|118|记得买外置天线|\\r\\n\\r\\n### M1 Ultra Mac Studio 同等性能(5159 元,64GB + 1TB SSD)\\r\\n\\r\\n|部件|产品|价格|备注|\\r\\n|-|-|-|-|\\r\\n|CPU|Intel i7 12700f 散片|2110|淘宝找个人多的店买就行|\\r\\n|主板|铭瑄 h610itx 挑战者|568|官方旗舰店|\\r\\n|内存条|酷兽夜枭 32GB 3200Mhz 两根共 64GB|798|京东自营即可|\\r\\n|硬盘|光威弈 Pro 1TB nvme ssd|499|如果不需要 1TB,可以换成 512GB,酌情购买|\\r\\n|散热器|利民 Axp90 x53 + LGA17xx 扣具|169|买 53mm 版本,记得买 17xx 扣具|\\r\\n|机箱|傻瓜超人 K39 + 定制显卡延长线|200|一定要买定制显卡延长线|\\r\\n|电源|益衡 7030B 300W Flex 1U 电源|398|记得买全模组定制版本|\\r\\n|显卡|盈通 Rx550 4GB|299|淘宝二手,记得询问客服是否是 Polaris 核心|\\r\\n|网卡|博通 BCM94360CS2 + m2 ngff 转接卡|118|记得买 ngff 转接卡,带外置天线版本|\\r\\n","content":"
\\n

写给程序员的小尺寸高性能黑苹果主机装配指南,全文约 10000 字。

\\n
\\n
\\n

本文同步发表至:FlowUs / 知乎 / Github / 博客

\\n
\\n
\\n

请使用良好的网络环境访问此文,否则图片可能无法加载。

\\n
\\n

\\"image\\"\\n左:Mac Studio,右:穷逼版

\\n

\\"image\\"\\n与 24 寸显示器的对比

\\n

\\"image\\"\\n与 330ml 杯子的对比

\\n

\\"image\\"\\n日常使用负载

\\n

长话短说

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
性能对比CPU内存硬盘性能(R23)价格体积
Mac StudioM1 Ultra64GB1TB 固态多核 2370529999197mm * 197mm * 95mm = 3.68L
穷逼版i7 1270064GB1TB 固态多核 215684946196mm * 218mm * 117mm = 4.9L
\\n
    \\n
  • 本文只面向程序员群体,优先保证编译性能和开发舒适度,不面向视频剪辑等创意工作者,不考虑视频剪辑性能;
  • \\n
  • 重点考虑 CPU 单核和多核性能,优先内存和硬盘容量,考虑机箱体积,不太考虑内存频率,不追求硬盘速度,不太考虑显卡性能,不太考虑噪音,完全不考虑外观精致程度;
  • \\n
  • 优点:便宜,量大管饱,装机完成后,可当做白苹果使用,无痛升级后续系统,可支持 MacOS、Windows 和 Linux 三系统共存;
  • \\n
  • 缺点:折腾,组装机箱和安装系统需要有一定动手能力,不适用于日薪大于 1w 的用户,不适用于无计算机硬件常识用户;
  • \\n
  • 硬件到位之后,在网络良好情况下,半天即可完成装机;
  • \\n
  • 多核编译同一个项目,M1 Max 需 6min,本文主机需 4min,多核性能对比与 R23 跑分对比大致保持一致。
  • \\n
\\n

阅读须知

\\n
    \\n
  • 适用群体:\\n
      \\n
    • 写代码的
    • \\n
    • 买不起 Mac Studio 的
    • \\n
    • 有一定的折腾能力的
    • \\n
    \\n
  • \\n
  • 不适用群体:\\n
      \\n
    • 打游戏的
    • \\n
    • 富哥富姐请直接使用钞能力
    • \\n
    • 剪视频的
    • \\n
    • 懒得折腾的(可以购置硬件后,购买淘宝黑苹果装机服务)
    • \\n
    \\n
  • \\n
  • 本文包含以下内容:\\n
      \\n
    • 提供了购买硬件和装机过程中的一些需要考虑的事项
    • \\n
    • 提供了分别对标 M1 Mac mini、M1 max Mac studio 和 M1 ultra Mac studio 性能(不含 GPU 和磁盘性能)且同等体积的装机购置清单
    • \\n
    • 提供了安装黑苹果所需的硬件考虑事项以及系统安装须知
    • \\n
    \\n
  • \\n
\\n

为什么需要装这样一台机器

\\n
\\n

可以跳过此章节。

\\n
\\n

总而言之,对于我来说,主要有两个原因:

\\n
    \\n
  • \\n

    主要原因:公司电脑性能拉跨,满足不了开发需要;

    \\n
  • \\n
  • \\n

    次要原因:目前在用的 MacOS 13 Ventura 系统拉跨,降级麻烦。

    \\n
  • \\n
\\n

所以,我需要搞一台高性能主机来替换掉公司发的老旧 MBP。

\\n

其实公司配的 2020 款 MBP 并不算老,就是性能有点差,10 代标压 i5-1038NG7 处理器加 16GB 板载内存,多核性能大概是 M1 的一半,省着点还是可以用的。

\\n

但我在 MacOS 13 技术预览版刚放出来时候,就迫不及待地手贱升级了一波,然后成功被新系统的各种卡顿 Bug 和内存占用虚高问题治好了低血压。即便到如今已经发布了正式版,这些问题仍旧存在,以至于日常开发时随便开个 VS Code 再加几个网页,内存就基本见底了。

\\n

后来为了能保证正常开发,我不得不花一周时间把 Vim 打造成主力 IDE,以便直接在开发机上写代码。

\\n

此外,除了 MacOS 拉跨,公司的项目本身也对设备性能要求很高。

\\n

身后的同事老哥前段时间斥巨资入手了一款中配 M1 Max 款 Mac Studio,原因就是公司发放的设备很难满足开发需求。我们日常的技术栈是 C++ 和 React,前者在我那台十代标压 i5 上,全量编译整个项目需要花费 20 分钟到 30 分钟的时间,编译个几次项目,半天就没了,倒是非常适合划水。虽然大多数时间并不会全量编译,但每次增量编译耗时也在分钟级别,非常可观,十分难受。

\\n

后者 React 更是重量级,NodeJS + HMR + 浏览器堪称内存黑洞,再配合 C++ 建立的 Clang 符号表索引,16GB 板载内存直接被榨干,一天下来大多数时间都在等编译和页面响应。

\\n

当然这也并不是完全没有好处,由于等待的成本过于高昂,我们写下每行代码前都要深思熟虑,每次编译前都要检查个半天,所以那段时间的代码质量总感觉也高了不少 😆。

\\n

后来我们安排了一台 36 核 72 线程(含两颗 18C36T Intel Xeon Gold 6240 CPU)的开发机,第一次用它编译项目的时候,有种便秘两周之后突然窜稀的畅快感觉,以前 20 多分钟的编译,现在两分钟就完事了,比以前跑单测都快。

\\n

然而即便用了开发机和 Vim 之后,我还是经常被系统卡顿和内存不足烦得要死,随便开几个网页,该卡还是卡,就这样将就了两个多月,在某次系统彻底无响应之后,我一咬牙一狠心,决定加亿点钱解决这个问题。

\\n

于是我便问了下身后老哥的 M1 Max 的编译时间,居然只要 6 分钟,已经完全够用了,怪不得他不怎么用开发机,我马上点开了苹果官网:

\\n

\\"image\\"

\\n

然后看到这个价格我火速关闭,那没事了,告辞。

\\n

看来富哥的路是走不通了,只能使用穷逼的道路曲线救国了。

\\n

性能

\\n

苹果在发布会上把 M1 系列芯片吹得天花乱坠,拳打 Intel i9-11900,脚踢 RTX 3090,让我们来看看它的真面目:

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
芯片名称规格R23 单核跑分R23 多核跑分功耗
Intel Core i5 1038NG74 核 8 线程1116497928W
Apple M18 核151076876.8W ~ 39W
Intel Core i5 124006 核 12 线程16971190565W
Apple M1 Max10 核15331233711W ~ 115W
Intel Core i7 1270012 核 20 线程187921568PL1: 65W, PL2 180W
Apple M1 Ultra20 核15062370513W ~ 215W
Intel Core i9 1290016 核 24 线程198826455PL1: 65W, PL2 200W
\\n

确实很强,比 10 代 intel 强多了,但是面对 12 代 intel 还是不太行,毕竟功耗在这摆着,i9 12900 勉强守住了桌面 CPU 的底裤。

\\n

苹果 M1 系列的能耗比确实惊人,但是我要放在公司使用,完全不用担心电费问题,功耗都是图一乐,于是经过一周的调研,确定了硬件配置单。

\\n

硬件配置单

\\n
\\n

下列配置中的硬件,除显卡和网卡外,均为全新价格。

\\n
\\n
\\n

如需查看更多配置单(M1 / M1 Max 级别),请跳转到文章末尾。

\\n
\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
部件产品价格备注
CPUIntel i7 12700f 散片2110f 后缀代表不集成核显,比带核显版本便宜 100 块左右
主板铭瑄 H610itx 挑战者569最便宜的能带得动 i7 12700 的 itx 主板
内存条酷兽 32GB 3000Mhz 两根共 64GB798最便宜的 DDR4 内存,有 2666mhz / 3000mhz / 3200Mhz 三个版本,价格一样,哪个便宜有货买哪个
硬盘光威弈 Pro 512GB nvme ssd295随便选的,可以加一百多块上 1TB 固态,反正便宜
散热器利民 Axp90 x53 + LGA17xx 扣具159选购时需要考虑机箱适配的散热器高度,需要加 18 元额外购买利民 LGA17xx 扣具
机箱傻瓜超人 K39 4.9L + 显卡延长线199196mm * 117mm * 218mm = 4.9L 体积,当前配置的体积最小最便宜的选择
电源益衡 7030B 300W Flex 1U 电源398这个价格是加 100 块让店家换全模组电源线和静音风扇之后的版本,组装的时候理线会更方便
显卡AMD RX550 2GB/4GB Polaris 核心300二手亮机卡,有 Lexa 和 Polaris 核心两个版本,建议买 4GB 显存 Polaris 核心版本,各版本价格无区别
网卡博通 BCM94360CS2 + m2 ngff 正向转接卡118也可以选择 Intel AX201 或者更低端的 Intel 9560,能省个几十块钱,但需要额外配置驱动,没必要
\\n

我们一项一项来看,为什么要选用这些配置。

\\n

CPU 和主板

\\n

首先,要再次明确一下目标,我们想要 M1 Ultra 级别的性能,那就只能在 i7 12700 及以上的 CPU 中选,大概有这么几个选项:

\\n
    \\n
  • \\n

    Intel i7 12700f 和 12700

    \\n
  • \\n
  • \\n

    Intel i7 12700kf 和 12700k

    \\n
  • \\n
  • \\n

    Intel i9 12900f

    \\n
  • \\n
  • \\n

    Intel i9 12900kf 和 12900k

    \\n
  • \\n
\\n

**为什么不选 AMD 的 CPU?**因为性价比不高,12 代 CPU 同价位性能比 AMD 5000 系列同价位性能稍强,价格稍便宜,而且目前 AMD 装黑苹果需要额外配置,有点麻烦,所以完全没有理由选 AMD。

\\n

**为什么不选 13 代 Intel CPU?**因为性价比不高,目前只发布了带 K 后缀的高性能版本,需要搭配更好的主板和电源,而且 13 代 CPU 也相对更贵。

\\n

然后,i7 和 i9 分别都有 f / k / kf 几个版本:

\\n
    \\n
  • \\n

    其中凡是带 f 的,都是不带集成显卡的版本,考虑到我们是装黑苹果,12 代集成显卡不能被驱动,所以必须得配个独立显卡,选择 f 版本就行,可以省下 100 块;

    \\n
  • \\n
  • \\n

    其中凡是带 k 的,都代表着性能增强版,大概比不带 k 的版本性能强 10% - 15%,价格也要贵 5% - 10%,酌情购买。

    \\n
  • \\n
\\n

由于 i9 处理器需要主板有更强的供电能力,主板的价格就水涨船高,所以我们退而求其次,选择 i7 12700,性能只比 M1 Ultra 弱 10%。当然,也可以加点钱上 12700k,这样性能和 M1 Ultra 持平,不过这点差距在实际使用时基本没什么区别,12700k 对主板供电能力也有一定的要求,丐板估计带不动。

\\n

主板就不提了,铭瑄 H610itx 在 600 块价位基本无敌,找不到其他能一起打的。唯一缺点就是 itx 主板接口有点少,只能插两根内存条,目前消费级内存条单条最大也就 32GB,所以最多只能上 64GB 内存。

\\n

而且这种低端丐板没有额外的 M.2 接口,只能插一条 Nvme 固态,要扩容的话,要么换固态,要么加 SATA 硬盘。不过还好 512GB SSD 够用了,即便是 1TB 的 SSD 也只要 400 出头,可以自己决定要不要加钱。

\\n

购买建议:

\\n
    \\n
  • 购买散片板 U 套装即可
  • \\n
\\n

内存条和硬盘

\\n

酷兽和光威是同一个厂,不同的产品线,酷兽主打性价比,399 块的 32GB 内存条要啥自行车,直接来两条插满。

\\n

此外这个内存条有 2666mhz / 3000mhz / 3200mhz 三个频率版本,理论上铭瑄 h610itx 主板是支持 3200mhz 高频内存的,只需要在 BIOS 里设置一下就行,但是说实话内存频率感知不强,只有核显打游戏才对内存频率有要求,如果用来开发,随便买就行,高频内存属实浪费预算,哪个有货买哪个。

\\n

最近固态硬盘也是白菜价,1tb 固态已经降到了 400 以下,当然也有更贵的性能更好的版本,但是对于开发来说感知不强,3000mb/s 和 7000mb/s 虽然看起来差距挺大,但是实际用的时候也就几百毫秒的差距,感知不出来,所以随便买个便宜的就行,重点还是看容量。

\\n

购买建议:

\\n
    \\n
  • 哪个便宜买哪个,频率和读写速度并不重要;
  • \\n
  • Mac Studio 的统一内存,官方读速是 800GB/s,与 3200mhz DDR4 内存条读速差不多;
  • \\n
  • 如果磁盘性能也想对标一下 Mac Studio,Mac Studio 硬盘速度为:读 5000MB/s,写 4000MB/s,大致与 PCIE 4.0 固态速度接近,所以可以酌情购买 PCIE 4.0 固态硬盘(铭瑄的这块主板支持,精粤的不支持),同体积大概是 PCIE 3.0 硬盘的 1.5 倍价格,512GB 版本大概 400 块可以搞定。
  • \\n
\\n

散热器、机箱和电源

\\n

这三兄弟要一起看,其实显卡也应该拿过来一起看,但是开发场景,显卡不是很重要,所以显卡另说吧。

\\n

之所以要放一起看,主要是尺寸问题,我想要体积尽可能接近 Mac Studio,Mac Studio 的体积是 197mm * 197mm * 9.5mm = 3.68L,一开始我买的是下面这个机箱:

\\n

\\"image\\"

\\n

整体尺寸是 187mm * 230mm * 97mm = 4.2L,非常小巧。

\\n

我选择了其中的 A 背板版本,没有显卡位,把空间全部让给散热器和电源,散热器也是买的 Axp90 x53(90mm 宽,53mm 高)。

\\n

因为我一开始并没有打算装黑苹果,想直接用 Linux 来开发,但是后来发现 Linux 没办法入公司内网,而且企业微信也是残废状态,所以才考虑装黑苹果,于是换成了下面的机箱:

\\n

\\"image\\"

\\n

体积大了一圈,来到了 196mm * 117mm * 218mm = 4.9L,不过好在便宜,加显卡延长线才 200 块,比上一个 300 块的机箱便宜很多,而且可以加装一个 19cm 以内的双槽短卡,非常适合本文的需求。

\\n

从上面机箱的商品介绍就可以知道,这个机箱支持 56mm 以内的散热器,一般来讲,散热器的尺寸与散热能力正相关,基本上体积越大,散热能力越好,在看了一圈视频之后,出现次数的就是利民的axp90 系列的几个散热器,价格在 100 元到 150 元之间:

\\n

\\"image\\"

\\n

可以看到主要有 36mm / 47mm / 53mm 三种高度,名字 AXP90 则代表 90mm * 90mm 的尺寸,为了最大化机箱利用率,我选用了 53mm 高度版本,实测整机满载温度可以控制在 80 摄氏度以内,日常使用则只有 27 摄氏度左右,好悬没给 CPU 给冻感冒。

\\n

此外,如果你想选用一些更小尺寸的机箱,在选散热器的时候一定要注意,最好预留 3mm 以上的空间余量,比如如果限高 56mm 的机箱,如果配了一个 55mm 高度的散热器,风扇叶片和机箱离得太近,会出现十分明显的啸叫。

\\n

然后是电源,前面了解到 i7 12700 的满载功耗是 180w,再加上 50w 亮机显卡的功耗,总计 230w 的实际载荷,按照 80% 的电源转化率,我们只需要选择 290w 以上的电源即可满足需求。在尺寸方面,由于我们想让机箱的体积尽可能的小,而小尺寸 itx 机箱往往搭配使用小 1u 尺寸的 Flex 电源,这类电源的价格要比常规尺寸的电源稍贵一点,如果想要省钱,可以选购支持 SFX 电源的更大尺寸的机箱。

\\n

最终我选择了益衡的 7030B 300w 电源:

\\n

\\"image\\"

\\n

\\"image\\"

\\n

小 1u 电源还有个缺点是会比较吵,因为受体积限制,风扇必须提高转速才能保证散热,运行时会有比较明显的高频噪声,所以我选择加了 100 块,买了店家改装之后的版本(上图右侧)。

\\n

改装版本换了更静音的风扇,并且配备了全模组电源线,大概长下面这个样子:

\\n

\\"image\\"

\\n

电源线统一成了黑色的软线,比原装的五颜六色的硬线好看多了,而且装机的时候理线会非常简单,这一点在小机箱里尤其关键,所以这 100 块还是挺值的。

\\n

购买建议:

\\n
    \\n
  • 购买全模组小 1u 电源,考虑购买全模组 + 静音风扇的改装版本
  • \\n
  • 购买散热器时注意机箱散热器限高
  • \\n
  • 利民散热器默认附带的主板支架只能与上一代 Intel CPU 搭配使用,需要额外花费 18 元购买利民 LGA17xx 扣具
  • \\n
\\n

显卡

\\n

显卡需要着重讲一下,因为 MacOS 非常挑显卡,目前市售的 RTX 系列显卡基本都是驱动不了的,只要一些比较老的 Nvidia 显卡才能驱动。而大多数 AMD 显卡都能驱动,比如最近出的 6600 和 6700,但是 6400 除外,详细的 GPU 列表可以查看这里

\\n

由于我们并不需要特别强劲的显卡性能,所以只需要选择能够正常驱动的低功耗亮机卡即可,我一开始就直接从比较老的 R7 和 R9 系列显卡开始看:

\\n

\\"image\\"

\\n

可以看到有非常多的选项,我在淘宝搜了一圈之后,发现最低端的 R7 240/250 显卡只要 100 块,于是就直接下单买了一块,结果入手后折腾了许久才发现,这张 2013 年发售的老显卡,最高只能运行在 MacOS 10.x 系统上,最新的 12.x 系统是没法完美驱动的。我花了大量的时间搜索国内外关于这张 Oland 核心的显卡驱动帖子,没有发现任何人能成功在 11.x 及以上的系统上成功运行过它,虽然可以使用仿冒显卡 ID 的方法成功安装并进入系统,但是却无法使用 Metal GPU 加速,导致系统所有的高斯模糊效果以及动效都是缺失的,完全无法满足日常使用。

\\n

于是我就又斥巨资花了 300 块买了一张 Rx550,这张显卡发售于 2017 年,网上有相当多的黑苹果视频表示这张卡可以完美工作在最新的 MacOS 13.0 系统上,而且完全不需要任何额外操作,插上就能用。当然,前提是买到了正确的版本的显卡,这张显卡有两种核心版本:Lexa 核心和 Buffin 核心(或称 Polaris 核心),其中 Buffin(Polaris) 核心是可以即插即用,但是 Lexa 核心则需要仿冒参数才能工作。

\\n

很不幸我买的这张 Rx550 2GB 联想拆机卡是 Lexa 核心,不过由于有了之前折腾 R7 240 的经历,对我来说还算简单。

\\n

此外这张显卡有 2GB 显存和 4GB 显存两个版本,价格基本一样,我个人建议购买 4GB 版本,因为我在实际使用时发现,MacOS 12.x 系统很容易就把 2GB 显存吃满了,所以能买大的还是买大点的。

\\n

\\"image\\"

\\n

盈通的这张 RX550 4GB 就很适合,虽然是二手,有翻车的风险,但是我们并不会用来打游戏,所以只要它能点亮就行,反正日常负载不会太高,暴毙的可能性比较小。

\\n

如果实在担心翻车,可以加点钱买全新版本,这张卡有全新正品在售,价格在 500 块到 600 块之间:

\\n

\\"image\\"

\\n

购买建议:

\\n
    \\n
  • 购买 RX550 4GB Polaris 核心版本
  • \\n
  • 省钱就买二手,图稳就加点钱买全新
  • \\n
  • 购买显卡时注意显卡尺寸是否兼容机箱
  • \\n
\\n

无线网卡

\\n

无线网卡也值得花点篇幅来说一说,目前黑苹果无线网卡可选 Intel NGFF 接口的一系列网卡,以及搭配转接板使用的博通 BCM 系列苹果拆机显卡:

\\n

\\"image\\"

\\n

如果从省事的角度来看,直接买博通的拆机网卡即可,插上就能用。如果想省点钱,就买 Intel 最低端的 3168 系列:

\\n

\\"image\\"

\\n

其实在几年前,Intel 的无线网卡是没办法在苹果系统上使用的,后来有个大佬搓了一个驱动出来,然后 Intel 几乎所有型号的无线网卡一夜之间焕发第二春,具体的支持列表可以查看下面的链接:Compatibility | OpenIntelWireless

\\n

此外无线网卡的天线,有内置和外置两种区别:

\\n\\"image\\"\\n\\"image\\"\\n

两者的区别:

\\n
    \\n
  • 外置天线是机箱后面的小尾巴,看起来可能不太美观,但是信号比较稳定,增益较强;
  • \\n
  • 内置天线则需要粘在机箱上,容易受机箱内部杂波干扰,增益较小,信号不太稳定,但由于不需要伸出机箱,所以会比较美观一点。
  • \\n
\\n

我选择了 Intel 9560 内置天线版本,花费 48 块,实装之后可以完美使用 WIFI 和蓝牙,隔空投送也可以正常使用,但是内置天线信号不稳定,蓝牙连接妙控板非常卡顿,基本无法日常使用。而且由于网卡规格较低,虽然标称 2.4G 300Mbps / 5G 1733Mbps,但实测只能跑到 50Mbps,可能最大的原因还是这个内置天线的信号太差,干扰太多。

\\n

不过问题不大,这个主机的 WIFI 纯粹是为了应付公司的入网认证,认证完了之后就直接关掉用有线连接了,如果你对无线网络有需求,建议购买高规格的外置天线版本。

\\n

购买建议:

\\n
    \\n
  • 购买博通 BCM 94360CS + 正向转接卡 + 外置天线版本,不用折腾,信号更好。
  • \\n
\\n

装机

\\n

十分推荐先观看此视频:[DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!](【DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!】 https://www.bilibili.com/video/BV1v54y1Z7Er/?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9),视频中的配置与本文配置基本一致,可以参考。

\\n\\"image\\"\\n

先上所有硬件的全家福,图中不包含显卡和网卡,因为拍摄照片的时候显卡和网卡还没到。

\\n

安装 CPU

\\n\\"image\\"\\n

主板说明书会提示如何安装 CPU,一定要小心不要弄坏了针脚,主板上的金属压杆会比较紧,确认 CPU 按照防呆口安装妥当后,稍加用力按下压杆,将 CPU 固定好即可。

\\n

上电测试

\\n

先别急着往机箱里塞,把内存条和硬盘插好之后,连接电源线和键鼠,用螺丝刀碰触主板上的开机引脚,显示器能够显示 BIOS 界面,则表示成功点亮。

\\n

安装散热器

\\n

按照散热器说明书进行安装即可,注意需要额外购买利民 LGA17xx 扣具。

\\n

安装机箱

\\n

按照机箱说明书安装即可,建议先安装电源,再安装主板,然后接电源线,安装显卡延长线,安装显卡。

\\n\\"image\\"\\n

成品展示

\\n\\"image\\"\\n\\"image\\"\\n

最终的成品大概比 Mac Studio 大了一圈,当然精致程度是没法比的,如果想要精致,可以加钱买更贵的全铝合金一体成型机箱(5.6L 体积):

\\n

\\"image\\"

\\n

系统

\\n

可以直接跳转到流程清单部分,如果你不需要安装 MacOS,可以无视下列内容。

\\n

为什么不用 Windows 和 Linux?

\\n

在最开始决定装机的时候,我就在考虑要使用什么操作系统作为主力开发环境,前文已经提到我需要频繁编译,所以编译性能是我首要考虑的问题,这样 Window 肯定就不能用了,Windows 下 C++ 编译性能一向令人诟病,即便有了 WSL,由于它运行在 Hyper-V 虚拟机中,导致 IO 性能跟不上,实际的性能大概是 Linux 原生的 80% 左右,直接损耗了 20% 的性能,要知道 Intel 的 CPU 一次代际升级的性能也才差不多 25%,Window 直接就让 CPU 性能倒退一代,这绝对是无法接受的。

\\n

我随后的想法是使用 Linux 作为日常开发系统,并使用 KVM 安装 Windows 虚拟机来解决常用软件问题,我也确实尝试了这种做法,并依次使用了下列 Linux 发行版:

\\n
    \\n
  • 使用 KDE 桌面的 Debian,由于 Stable 版本的 Linux 内核太老,导致无法在 12 代 Intel 平台上正常安装,尝试几次后均未果,作罢;
  • \\n
  • 使用 KDE 桌面的 Ubuntu,也就是 KUbuntu,这个发行版确实很不错,KDE 对高分屏的支持非常不错,而且窗口主题和动效都很细腻,我用了大概一周时间,在上面使用 KVM 分别安装了 Windows 10 LTSC 以及 Mac OS 12 的虚拟机,前者用于运行企业微信,后者则是装来玩的,由于没有独显直通,Mac OS 虚拟机的运行流畅度十分感人。在 Linux 上运行 Windows 软件的另一个方法是使用 Wine,但无奈兼容性欠佳,无法正常运行最新版本的企业微信,微信倒是可以正常使用;
  • \\n
  • 使用 Gnome 的 Ubuntu,Gnome 对高分屏的支持太差劲,大多数应用没有对 Wayland 做兼容,甚至浏览器在分数倍缩放下都很糊,而且同样无法通过 Wine 运行企业微信,作罢;
  • \\n
  • 最后是国产的 Deepin,应该是最贴近日常使用的 Linux 发行版了,应用商店里有 Deepin 团队维护的 Wine 版微信和企业微信,但是企业微信还有点问题,虽然可以正常聊天,但是邮箱界面始终白屏,另外 Tim 也是卧龙凤雏,可以安装但无法启动。
  • \\n
\\n

我大概用了一周多的 Deepin,并成功在上面编译运行了公司的项目,编译时间大概是开发机的 2 倍速度,可以看到 i7 12700 的多核性能相当强悍。

\\n

但是网络问题始终没有办法很好的解决,公司的内网认证软件是阿里出品的阿里郎的阉割版本,其入网认证条件非常严格,无法在 Windows 虚拟机中完成认证,更不用提 Wine 了,也没有提供 Linux 版本,导致我的主机就算插上网线也无法访问内网资源,而且这个软件入网必须要有无线网卡,大多数 USB 网卡都无法在 Linux 下免驱运行。

\\n

为了解决这个问题,我琢磨了很久计算机网络相关的知识,大概有这么几种方法:

\\n
    \\n
  • 最容易想到的方法自然是用公司的苹果笔记本入网,然后当跳板机开代理给主机用,确实可以用,但是基于代理的方法,无法保证主机的所有流量都走代理,某些应用可能会绕开系统的代理策略,自行建立连接,这种方法不太稳定;
  • \\n
  • 然后比较容易想到的是使用 Mac 的网络共享,无奈在入网认证使用 802.1x 的安全策略,系统会直接禁用掉网络共享,这条路也走不通;
  • \\n
  • 最后想到的方法时把 Mac 模拟成一台路由器,直接网线直连主机,然后手动分配 IP 地址即可,经过一番搜索,其实只要开启 Mac 的 IP 路由转发即可,然后将来自 USB 网卡的所有流量都转发到无线网卡上,即可大功告成。
  • \\n
\\n

关于折腾 Linux 日用开发环境的记录,可以看这篇文章:

\\n

Linux 开发环境备忘录

\\n

最后由于 Linux 上无法找到顺手的快速切换窗口的软件(类似 Mac Spotlight 的功能),还是决定安装黑苹果,可以直接无缝复用 MBP 的那一套工作流。

\\n

主要工具:OpenCore

\\n

安装黑苹果是一件非常需要耐心的事情,目前主流的安装黑苹果的工具是 OpenCore,其官方教程非常详细,流程也非常长,新手看了也非常头大:

\\n

OpenCore Install Guide

\\n

在翻阅许久之后,我决定放弃跟着官方教程走,官方手册的内容只适合当作速查表来使用,如果跟着从零开始做,不知道搞到猴年马月。

\\n

先列举一下我认为比较有用的内容,首先是显卡购买指南:

\\n

Introduction | GPU Buyers Guide

\\n

里面列举了各种可以用于黑苹果的显卡型号,以及它们支持的系统版本,当然这个指南中的某些老显卡的信息是错的,比如我前面提到的 R7 240,手册上说可以在最新的系统中通过仿冒 ID 的方式运行,但其实只能在 OS 10.x 以前的系统上运行,不过大多数信息都是准确的,可以放心参考。

\\n

其次是 GPU 仿冒 ID 指南,如果你想折腾一下不直接免驱的显卡,可以参考它来仿冒 ID:

\\n

Renaming GPUs (SSDT-GPU-SPOOF) | Getting Started With ACPI

\\n

我个人不推荐用这种方式来搞,太费时间,而且不一定有用,还是建议直接购买免驱显卡。

\\n

流程清单

\\n

如果自己搞不定,可以直接在淘宝购买黑苹果装机服务,远程手把手指导,价格在 100 元到几百元不等。

\\n

可以看这个视频,了解大概步骤:刷新记录!12代平台12400F仅需24秒即可完成黑苹果系统安装,请问还有谁?

\\n

通用流程:

\\n
    \\n
  • 硬件准备:\\n
      \\n
    • 16GB 及以上容量的内存卡或者移动硬盘,并格式化为 FAT32 格式;
    • \\n
    • 一台 Windows 电脑(Linux 或 Mac 也行);
    • \\n
    • 组装好的等待安装系统的主机;
    • \\n
    • 良好的有线网络环境;
    • \\n
    • 耐心。
    • \\n
    \\n
  • \\n
  • 准备 EFI 引导文件:各版本 EFI 引导文件\\n
      \\n
    • 如果你的硬件配置是本文推荐的配置,那么直接下载上面列表中的安装包,解压即可;
    • \\n
    • 或者你可以在 Github 上搜索自己的主板名称,比如我就参考了这个仓库(h610itx + i5 12400 + rx560 + bcm943602cs 网卡)和这个仓库(h610itx + i5 12490f + rx560 + intel ax201 网卡),在他们的基础上进行了少量修改,就适配了本文的硬件;
    • \\n
    • 经过上述步骤,你会获得一个 EFI 文件夹,将其拷贝到 U 盘或者移动硬盘中即可,如果你的硬件与上述现有的配置都不一样,请查阅常见问题章节。
    • \\n
    \\n
  • \\n
  • 准备基础安装镜像:\\n
      \\n
    • OpenCore 提供了一个很细致的教程来下载安装镜像,基础镜像大概 600MB 左右,按照这个教程(在 Windows 上下载安装镜像)下载镜像文件并复制到 U 盘即可。
    • \\n
    • 将 U 盘插入主机,按下 DEL 键进入主板的 BIOS 界面,如果你使用了其他主板,请自行搜索如何进入 BIOS,然后将 U 盘设置为第一启动项即可;
    • \\n
    \\n
  • \\n
  • 启动启动后进入安装界面:\\n
      \\n
    • 选择磁盘工具,将 SSD 所在盘符格式化为 APFS 格式,并命名为 MacOS(或者其他任何英文名);
    • \\n
    • 退出磁盘工具,点击安装 MacOS,进入常规安装流程;
    • \\n
    \\n
  • \\n
  • 在网络良好情况下,自动重启若干次后,大概 40 分钟即可安装成功;
  • \\n
  • 安装成功后,自动重启进入系统选择界面,选择 MacOS 盘符进入即可;
  • \\n
  • 将 EFI 文件夹复制到 SSD 硬盘上,可参考这个教程:完善引导
  • \\n
  • 到现在,你已经可以正常使用 MacOS,但是还无法使用 Apple ID 和 iCloud,你还需要生成自己的硬件序列号,请参考这个教程:为自己的黑苹果生成随机三码
  • \\n
  • 至此,大功告成。
  • \\n
\\n

常见问题

\\n

如何查看我买到的显卡的核心?

\\n

搜索

\\n
    \\n
  1. 优先询问卖家,其次在搜索引擎中搜索显卡型号 + 核心即可。
  2. \\n
\\n

使用 PE 工具

\\n
    \\n
  1. 下载安装微 PE 工具箱,并安装到 U 盘上;
  2. \\n
  3. 下载 GPU-Z:https://www.techpowerup.com/gpuz/,复制到已经安装了 PE 工具的 U 盘中;
  4. \\n
  5. 进入 BIOS,将 U 盘设置为第一引导,进入 PE 系统,运行 GPU-Z,即可看到显卡核心。
  6. \\n
\\n

\\"image\\"\\n第一个红框处,即是显卡核心名称。

\\n

使用现有的 Windows 主机

\\n
    \\n
  1. 把显卡插到现有的 Windows 主机上;
  2. \\n
  3. 下载安装 GPU-Z,即可看到显卡核心。
  4. \\n
\\n

买到了 Lexa 核心的 Rx550,该怎么办?

\\n

参考这个教程,仿冒 ID 即可:

\\n

【黑苹果】通过仿冒ID驱动Lexa核心的Radeon RX 550

\\n

12 代处理器如何开启小核心支持?

\\n

如果你是在别人的 EFI 文件基础上修改,并且别人的 EFI 文件基于 12 代 i3 或者 i5 处理器,而你的处理器是 12 代 i7 及以上,则需要开启小核心支持,查阅:

\\n

黑苹果开启十二代酷睿能效核心的驱动_豆豆本豆儿的博客-CSDN博客

\\n

装机单

\\n
\\n

价格可能略有浮动。

\\n
\\n
\\n

M1 Max 和 M1 Pro 性能几乎一致,不作区分。

\\n
\\n

M1 Mac Mini 同等性能(3155 元,32GB + 512GB SSD)

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
部件产品价格备注
CPUIntel i3 12100f 散片668
主板铭瑄 h610itx 挑战者568
内存条酷兽夜枭 16GB 3200Mhz 两根共 32GB470京东自营即可
硬盘光威弈 Pro 512GB nvme ssd295按自己喜好选即可
散热器利民 Axp90 x47 + LGA17xx 扣具139不用太强力的散热器,随便压一压
机箱傻瓜超人 K39 + 定制显卡延长线200一定要买定制显卡延长线
电源益衡 7030B 300W Flex 1U 电源398记得买全模组定制版本
显卡盈通 Rx550 4GB299淘宝二手,记得询问客服是否是 Polaris 核心
网卡博通 BCM94360CS2 + m2 ngff 正向转接卡118记得买外置天线
\\n

M1 Max Mac Studio 同等性能(3537 元,32GB + 512GB SSD)

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
部件产品价格备注
CPUIntel i5 12400f 散片1050
主板铭瑄 h610itx 挑战者568
内存条酷兽夜枭 16GB 3200Mhz 两根共 32GB470京东自营即可
硬盘光威弈 Pro 512GB nvme ssd295按自己喜好选即可
散热器利民 Axp90 x47 + LGA17xx 扣具139不用太强力的散热器,随便压一压
机箱傻瓜超人 K39 + 定制显卡延长线200一定要买定制显卡延长线
电源益衡 7030B 300W Flex 1U 电源398记得买全模组定制版本
显卡盈通 Rx550 4GB299淘宝二手,记得询问客服是否是 Polaris 核心
网卡博通 BCM94360CS2 + m2 ngff 正向转接卡118记得买外置天线
\\n

M1 Ultra Mac Studio 同等性能(5159 元,64GB + 1TB SSD)

\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
部件产品价格备注
CPUIntel i7 12700f 散片2110淘宝找个人多的店买就行
主板铭瑄 h610itx 挑战者568官方旗舰店
内存条酷兽夜枭 32GB 3200Mhz 两根共 64GB798京东自营即可
硬盘光威弈 Pro 1TB nvme ssd499如果不需要 1TB,可以换成 512GB,酌情购买
散热器利民 Axp90 x53 + LGA17xx 扣具169买 53mm 版本,记得买 17xx 扣具
机箱傻瓜超人 K39 + 定制显卡延长线200一定要买定制显卡延长线
电源益衡 7030B 300W Flex 1U 电源398记得买全模组定制版本
显卡盈通 Rx550 4GB299淘宝二手,记得询问客服是否是 Polaris 核心
网卡博通 BCM94360CS2 + m2 ngff 转接卡118记得买 ngff 转接卡,带外置天线版本
\\n"}}')},285:function(n,t,e){"use strict";e(266)},286:function(n,t,e){var l=e(105)(!1);l.push([n.i,'.clearfix:after,.vssue .vssue-new-comment .vssue-new-comment-footer:after{display:block;clear:both;content:""}.vssue{width:100%;color:#2c3e50;font-size:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";padding:10px}.vssue .vssue-button{outline:none;cursor:pointer;padding:10px 20px;font-size:1.05;font-weight:700;color:#900;background-color:transparent;border:2px solid #900;border-radius:10px}.vssue .vssue-button:disabled{cursor:not-allowed;color:#eaecef;border-color:#eaecef}.vssue .vssue-button:disabled .vssue-icon{fill:#eaecef}.vssue .vssue-button:not(:disabled).vssue-button-default{color:#a3aab1;border-color:#a3aab1}.vssue .vssue-button:not(:disabled).vssue-button-primary{color:#900;border-color:#900}.vssue .vssue-icon{width:1em;height:1em;vertical-align:-.15em;fill:#900;overflow:hidden}.vssue .vssue-icon-loading{animation:vssue-keyframe-rotation 1s linear infinite}@keyframes vssue-keyframe-rotation{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.vssue .fade-appear-active,.vssue .fade-enter-active{transition:all .3s ease}.vssue .fade-leave-active{transition:all .3s cubic-bezier(1,.5,.8,1)}.vssue .fade-appear,.vssue .fade-enter,.vssue .fade-leave-to{opacity:0}.vssue .vssue-notice{position:relative;z-index:100;transform:translateY(-11px)}.vssue .vssue-notice .vssue-alert{position:absolute;z-index:101;cursor:pointer;top:0;padding:10px 20px;width:100%;color:#900;border:2px solid #ff9494;border-radius:5px;background-color:#ffeded}.vssue .vssue-notice .vssue-progress{position:absolute;top:0;left:0;height:2px;background-color:#900}.vssue .vssue-status{text-align:center;padding-top:20px;padding-bottom:10px;color:#900}.vssue .vssue-status .vssue-icon{font-size:1.4em}.vssue .vssue-status .vssue-status-info{margin-top:10px;margin-bottom:10px}.vssue .vssue-header{padding-bottom:10px;border-bottom:1px solid #eaecef;margin-bottom:10px;overflow:hidden}.vssue .vssue-header .vssue-header-powered-by{float:right}.vssue .vssue-new-comment{border-bottom:1px solid #eaecef;margin-top:10px;margin-bottom:10px}.vssue .vssue-new-comment .vssue-comment-avatar{float:left;width:50px;height:50px}.vssue .vssue-new-comment .vssue-comment-avatar img{width:50px;height:50px}.vssue .vssue-new-comment .vssue-comment-avatar .vssue-icon{cursor:pointer;padding:5px;font-size:50px;fill:#757f8a}.vssue .vssue-new-comment .vssue-new-comment-body{position:relative}@media screen and (max-width:576px){.vssue .vssue-new-comment .vssue-new-comment-body{margin-left:60px}}@media screen and (min-width:577px){.vssue .vssue-new-comment .vssue-new-comment-body{margin-left:70px}}.vssue .vssue-new-comment .vssue-new-comment-body .vssue-new-comment-loading{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.vssue .vssue-new-comment .vssue-new-comment-footer{margin-top:10px;margin-bottom:10px}.vssue .vssue-new-comment .vssue-new-comment-footer .vssue-current-user{color:#a3aab1}.vssue .vssue-new-comment .vssue-new-comment-footer .vssue-current-user .vssue-logout{cursor:pointer;text-decoration:underline;color:#a3aab1;font-weight:400}@media screen and (max-width:576px){.vssue .vssue-new-comment .vssue-new-comment-footer{text-align:center}.vssue .vssue-new-comment .vssue-new-comment-footer .vssue-new-comment-operations{margin-top:10px}}@media screen and (min-width:577px){.vssue .vssue-new-comment .vssue-new-comment-footer{margin-left:70px;text-align:right}.vssue .vssue-new-comment .vssue-new-comment-footer .vssue-current-user{float:left}}.vssue .vssue-new-comment .vssue-new-comment-input{resize:none;outline:none;width:100%;padding:15px;font-size:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";background-color:#ffeded;border:1px solid #eaecef;border-radius:5px}.vssue .vssue-new-comment .vssue-new-comment-input:disabled{cursor:not-allowed;background-color:#f0f2f4}.vssue .vssue-new-comment .vssue-new-comment-input:focus{background-color:#fff;border-color:#ff4d4d;box-shadow:0 0 1px 1px #ff4d4d}.vssue .vssue-new-comment .vssue-new-comment-input::-moz-placeholder{color:#a3aab1}.vssue .vssue-new-comment .vssue-new-comment-input::placeholder{color:#a3aab1}.vssue .vssue-comments .vssue-comment{margin:15px 0}.vssue .vssue-comments .vssue-comment.vssue-comment-edit-mode .vssue-comment-main{border-color:#ff4d4d;box-shadow:0 0 1px 1px #ff4d4d}.vssue .vssue-comments .vssue-comment.vssue-comment-disabled{pointer-events:none}.vssue .vssue-comments .vssue-comment.vssue-comment-disabled .vssue-comment-body{background-color:#f9f9fa}.vssue .vssue-comments .vssue-comment .vssue-comment-avatar{float:left;width:50px;height:50px}.vssue .vssue-comments .vssue-comment .vssue-comment-avatar img{width:50px;height:50px}@media screen and (max-width:576px){.vssue .vssue-comments .vssue-comment .vssue-comment-body{margin-left:60px}}@media screen and (min-width:577px){.vssue .vssue-comments .vssue-comment .vssue-comment-body{margin-left:70px}}.vssue .vssue-comments .vssue-comment .vssue-comment-header{padding:10px 15px;overflow:hidden;border-top-left-radius:5px;border-top-right-radius:5px;border:1px solid #eaecef;border-bottom:none}.vssue .vssue-comments .vssue-comment .vssue-comment-header .vssue-comment-created-at{float:right;cursor:default;color:#a3aab1}.vssue .vssue-comments .vssue-comment .vssue-comment-main{padding:15px;border:1px solid #eaecef}.vssue .vssue-comments .vssue-comment .vssue-comment-main .vssue-edit-comment-input{resize:none;outline:none;border:none;width:100%;background:transparent}.vssue .vssue-comments .vssue-comment .vssue-comment-footer{padding:10px 15px;overflow:hidden;border-bottom-left-radius:5px;border-bottom-right-radius:5px;border:1px solid #eaecef;border-top:none}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-hint{cursor:default;color:#a3aab1}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-reactions .vssue-comment-reaction{cursor:pointer;display:inline-block;margin-right:8px;color:#900}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-operations{float:right;color:#900}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-operations .vssue-comment-operation{cursor:pointer;margin-left:8px}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-operations .vssue-comment-operation.vssue-comment-operation-muted{color:#a3aab1}.vssue .vssue-comments .vssue-comment .vssue-comment-footer .vssue-comment-operations .vssue-comment-operation.vssue-comment-operation-muted .vssue-icon{fill:#a3aab1}.vssue .vssue-pagination{cursor:default;display:flex;padding:5px;color:#a3aab1}@media screen and (max-width:576px){.vssue .vssue-pagination{flex-direction:column;justify-content:center;text-align:center}}.vssue .vssue-pagination .vssue-pagination-loading,.vssue .vssue-pagination .vssue-pagination-page,.vssue .vssue-pagination .vssue-pagination-per-page{flex:1}@media screen and (max-width:576px){.vssue .vssue-pagination .vssue-pagination-page{margin-top:10px}}@media screen and (min-width:577px){.vssue .vssue-pagination .vssue-pagination-page{text-align:right}}.vssue .vssue-pagination .vssue-pagination-select{outline:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;border:1px solid #ff4d4d;padding-left:.2rem;padding-right:1rem;background-color:transparent;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg%20class=%27icon%27%20viewBox=%270%200%201024%201024%27%20xmlns=%27http://www.w3.org/2000/svg%27%3E%3Cdefs%3E%3Cstyle/%3E%3C/defs%3E%3Cpath%20d=%27M676.395%20432.896a21.333%2021.333%200%200%200-30.166%200L511.061%20568.021%20377.728%20434.645a21.333%2021.333%200%200%200-30.165%2030.166l148.394%20148.48a21.419%2021.419%200%200%200%2030.208%200l150.23-150.187a21.333%2021.333%200%200%200%200-30.208%27/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:100%}.vssue .vssue-pagination .vssue-pagination-select:disabled{cursor:not-allowed}.vssue .vssue-pagination .vssue-pagination-select:focus{background-color:#fff;box-shadow:0 0 .2px .2px #ff4d4d}.vssue .vssue-pagination .vssue-pagination-link{display:inline-block;min-width:1em;text-align:center}.vssue .vssue-pagination .vssue-pagination-link.disabled{pointer-events:none}.vssue .vssue-pagination .vssue-pagination-link:not(.disabled){color:#900;font-weight:500;cursor:pointer}.vssue,.vssue *{box-sizing:border-box}.vssue :not(.vssue-comment-content) a{cursor:pointer;font-weight:500;color:#900;text-decoration:none}.vssue :not(.vssue-comment-content) hr{display:block;height:1px;border:0;border-top:1px solid #eaecef;margin:1.2rem 0;padding:0}body{margin:0;padding:0;font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif}.page{box-sizing:border-box;width:90%;max-width:900px;margin:auto}.loading-light{position:relative}.loading-light:after{content:"";position:absolute;height:20px;width:1000px;background-color:#fff;opacity:.4;left:-500px;animation:fly-to-right 2s ease infinite}@keyframes fly-to-right{0%{transform:translateX(0) rotate(45deg)}to{transform:translateX(1000px) rotate(45deg)}}.post-page{margin-bottom:50px}.title{display:flex;justify-content:center}.post-title{margin-top:120px;color:#333;font-weight:700;font-size:30px;position:relative;text-align:center;max-width:80%}.post-title:before{content:"“";left:-60px}.post-title:after,.post-title:before{position:absolute;font-size:55px;color:#eee}.post-title:after{content:"”";right:-60px}.post-content{word-break:break-word}.post-content .katex{font-size:16px}a{font-weight:500;color:#900;text-decoration:none}p a code{font-weight:400;color:#900}p{font-family:Georgia,"Nimbus Roman No9 L","Songti SC","Noto Serif CJK SC","Source Han Serif SC","Source Han Serif CN",STSong,"AR PL New Sung","AR PL SungtiL GB",NSimSun,SimSun,"TW\\-Sung","WenQuanYi Bitmap Song","AR PL UMing CN","AR PL UMing HK","AR PL UMing TW","AR PL UMing TW MBE",PMingLiU,MingLiU,serif}kbd{border:.15rem solid #ddd;border-bottom:.25rem solid #ddd;border-radius:.15rem;padding:0 .15em}blockquote,kbd{background:#eee}blockquote{font-size:1rem;color:#999;border-left:.2rem solid #dfe2e5;margin:1rem 0;padding:5px 10px}blockquote>p{margin:0}.post-content ol,.post-content ul,blockquote>p{font-family:Baskerville,"Times New Roman","Liberation Serif",STFangsong,FangSong,FangSong_GB2312,"CWTEX\\-F",serif}.post-content ol,.post-content ul{padding-left:1.2em}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25;font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif;margin-top:50px}h1:hover .header-anchor,h2:hover .header-anchor,h3:hover .header-anchor,h4:hover .header-anchor,h5:hover .header-anchor,h6:hover .header-anchor{opacity:1}h1{font-size:2rem;display:none}h2{font-size:1.45rem;padding-bottom:.3rem}h3{font-size:1.35rem}a.header-anchor{font-size:.85em;float:left;margin-left:-.87em;padding-right:.23em;margin-top:.125em;opacity:0;color:#ddd}a.header-anchor:hover{text-decoration:none}.line-number,code,kbd{font-family:Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif}pre{border:1px solid #aaa;border-radius:5px}pre>code{background:transparent;padding:0;margin:0}code,pre{font-size:14px}code{background:#eee;padding:2px 4px;margin:0 2px;border-radius:4px}ol,p,ul{line-height:1.7}li.task-list-item{list-style:none;display:flex}li.task-list-item input[type^=checkbox]{margin:7px 9px 0 -18px}hr{border:0;border-top:1px solid #eee}table{border-collapse:collapse;margin:1rem 0;display:block;overflow-x:auto}tr{border-top:1px solid #dfe2e5}tr:nth-child(2n){background-color:#f6f8fa}td,th{border:1px solid #dfe2e5;padding:.6em 1em}img{max-width:720px;width:100%;margin:auto;display:block;border:1px solid rgba(0,0,0,.1)}@media screen{.info{display:flex;justify-content:center;align-content:center;margin:30px 0 60px;font-family:Baskerville,"Times New Roman","Liberation Serif",STFangsong,FangSong,FangSong_GB2312,"CWTEX\\-F",serif}.info .author,.info .count,.info .date{margin:0 7px}}.vssue{font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif;margin-top:100px;padding:0}.vssue .vssue-header-powered-by,.vssue .vssue-pagination-page,.vssue .vssue-pagination-per-page{display:none}.vssue .vssue-new-comment{border:0}.vssue .vssue-current-user{line-height:2.5}.vssue .vssue-button-submit-comment:not(:disabled):hover{background-color:#eee}.vssue p{font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif}.vssue .vssue-button-login{border-color:transparent!important}@media screen and (max-width:576px){p{font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif}.post-title{max-width:100%;overflow:hidden;font-size:26px}.katex-display{overflow-x:scroll;overflow-y:hidden}.info{font-family:-apple-system,"Noto Sans","Helvetica Neue",Helvetica,"Nimbus Sans L",Arial,"Liberation Sans","PingFang SC","Hiragino Sans GB","Noto Sans CJK SC","Source Han Sans SC","Source Han Sans CN","Microsoft YaHei","Wenquanyi Micro Hei","WenQuanYi Zen Hei","ST Heiti",SimHei,"WenQuanYi Zen Hei Sharp",sans-serif;font-size:14px}}@media print{.page{width:100%}.title{margin-top:30%;font-family:Baskerville,Georgia,"Liberation Serif","Kaiti SC",STKaiti,"AR PL UKai CN","AR PL UKai HK","AR PL UKai TW","AR PL UKai TW MBE","AR PL KaitiM GB",KaiTi,KaiTi_GB2312,DFKai-SB,"TW\\-Kai",serif;font-weight:700}.post-title:after,.post-title:before{content:""}.info{text-align:center;margin-top:40%;page-break-after:always;font-family:Baskerville,Georgia,"Liberation Serif","Kaiti SC",STKaiti,"AR PL UKai CN","AR PL UKai HK","AR PL UKai TW","AR PL UKai TW MBE","AR PL KaitiM GB",KaiTi,KaiTi_GB2312,DFKai-SB,"TW\\-Kai",serif}.info .author{font-size:20px;line-height:2}.count{display:none}code{word-break:break-all}pre{page-break-inside:avoid}.footer,.vssue{display:none}}',""]),n.exports=l},290:function(n,t,e){"use strict";e.r(t);var l=e(12),r=(e(79),e(158),e(28),Object.assign||function(n){for(var i=1;i1&&void 0!==arguments[1]?arguments[1]:{},l=window.Promise||function(n){function t(){}n(t,t)},m=function(n){var t=n.target;t!==D?-1!==$.indexOf(t)&&L({target:t}):z()},f=function(){if(!B&&I.original){var n=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0;Math.abs(P-n)>E.scrollOffset&&setTimeout(z,150)}},x=function(n){var t=n.key||n.keyCode;"Escape"!==t&&"Esc"!==t&&27!==t||z()},v=function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=n;if(n.background&&(D.style.background=n.background),n.container&&n.container instanceof Object&&(t.container=r({},E.container,n.container)),n.template){var template=o(n.template)?n.template:document.querySelector(n.template);t.template=template}return E=r({},E,t),$.forEach((function(image){image.dispatchEvent(y("medium-zoom:update",{detail:{zoom:G}}))})),G},k=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return n(r({},E,t))},j=function(){for(var n=arguments.length,t=Array(n),e=0;e0?t.reduce((function(n,t){return[].concat(n,h(t))}),[]):$;return l.forEach((function(image){image.classList.remove("medium-zoom-image"),image.dispatchEvent(y("medium-zoom:detach",{detail:{zoom:G}}))})),$=$.filter((function(image){return-1===l.indexOf(image)})),G},_=function(n,t){var e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return $.forEach((function(image){image.addEventListener("medium-zoom:"+n,t,e)})),R.push({type:"medium-zoom:"+n,listener:t,options:e}),G},S=function(n,t){var e=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return $.forEach((function(image){image.removeEventListener("medium-zoom:"+n,t,e)})),R=R.filter((function(e){return!(e.type==="medium-zoom:"+n&&e.listener.toString()===t.toString())})),G},C=function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=n.target,e=function(){var n={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},t=void 0,e=void 0;if(E.container)if(E.container instanceof Object)t=(n=r({},n,E.container)).width-n.left-n.right-2*E.margin,e=n.height-n.top-n.bottom-2*E.margin;else{var l=(o(E.container)?E.container:document.querySelector(E.container)).getBoundingClientRect(),m=l.width,h=l.height,d=l.left,w=l.top;n=r({},n,{width:m,height:h,left:d,top:w})}t=t||n.width-2*E.margin,e=e||n.height-2*E.margin;var y=I.zoomedHd||I.original,f=c(y)?t:y.naturalWidth||t,x=c(y)?e:y.naturalHeight||e,v=y.getBoundingClientRect(),k=v.top,j=v.left,M=v.width,_=v.height,S=Math.min(f,t)/M,C=Math.min(x,e)/_,z=Math.min(S,C),L="scale("+z+") translate3d("+((t-M)/2-j+E.margin+n.left)/z+"px, "+((e-_)/2-k+E.margin+n.top)/z+"px, 0)";I.zoomed.style.transform=L,I.zoomedHd&&(I.zoomedHd.style.transform=L)};return new l((function(n){if(t&&-1===$.indexOf(t))n(G);else{if(I.zoomed)n(G);else{if(t)I.original=t;else{if(!($.length>0))return void n(G);var l=$;I.original=l[0]}if(I.original.dispatchEvent(y("medium-zoom:open",{detail:{zoom:G}})),P=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,B=!0,I.zoomed=w(I.original),document.body.appendChild(D),E.template){var template=o(E.template)?E.template:document.querySelector(E.template);I.template=document.createElement("div"),I.template.appendChild(template.content.cloneNode(!0)),document.body.appendChild(I.template)}if(document.body.appendChild(I.zoomed),window.requestAnimationFrame((function(){document.body.classList.add("medium-zoom--opened")})),I.original.classList.add("medium-zoom-image--hidden"),I.zoomed.classList.add("medium-zoom-image--opened"),I.zoomed.addEventListener("click",z),I.zoomed.addEventListener("transitionend",(function t(){B=!1,I.zoomed.removeEventListener("transitionend",t),I.original.dispatchEvent(y("medium-zoom:opened",{detail:{zoom:G}})),n(G)})),I.original.getAttribute("data-zoom-src")){I.zoomedHd=I.zoomed.cloneNode(),I.zoomedHd.removeAttribute("srcset"),I.zoomedHd.removeAttribute("sizes"),I.zoomedHd.src=I.zoomed.getAttribute("data-zoom-src"),I.zoomedHd.onerror=function(){clearInterval(r),console.warn("Unable to reach the zoom image target "+I.zoomedHd.src),I.zoomedHd=null,e()};var r=setInterval((function(){I.zoomedHd.complete&&(clearInterval(r),I.zoomedHd.classList.add("medium-zoom-image--opened"),I.zoomedHd.addEventListener("click",z),document.body.appendChild(I.zoomedHd),e())}),10)}else if(I.original.hasAttribute("srcset")){I.zoomedHd=I.zoomed.cloneNode(),I.zoomedHd.removeAttribute("sizes"),I.zoomedHd.removeAttribute("loading");var m=I.zoomedHd.addEventListener("load",(function(){I.zoomedHd.removeEventListener("load",m),I.zoomedHd.classList.add("medium-zoom-image--opened"),I.zoomedHd.addEventListener("click",z),document.body.appendChild(I.zoomedHd),e()}))}else e()}}}))},z=function(){return new l((function(n){if(!B&&I.original){B=!0,document.body.classList.remove("medium-zoom--opened"),I.zoomed.style.transform="",I.zoomedHd&&(I.zoomedHd.style.transform=""),I.template&&(I.template.style.transition="opacity 150ms",I.template.style.opacity=0),I.original.dispatchEvent(y("medium-zoom:close",{detail:{zoom:G}})),I.zoomed.addEventListener("transitionend",(function t(){I.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(I.zoomed),I.zoomedHd&&document.body.removeChild(I.zoomedHd),document.body.removeChild(D),I.zoomed.classList.remove("medium-zoom-image--opened"),I.template&&document.body.removeChild(I.template),B=!1,I.zoomed.removeEventListener("transitionend",t),I.original.dispatchEvent(y("medium-zoom:closed",{detail:{zoom:G}})),I.original=null,I.zoomed=null,I.zoomedHd=null,I.template=null,n(G)}))}else n(G)}))},L=function(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=n.target;return I.original?z():C({target:t})},A=function(){return E},N=function(){return $},F=function(){return I.original},$=[],R=[],B=!1,P=0,E=e,I={original:null,zoomed:null,zoomedHd:null,template:null};"[object Object]"===Object.prototype.toString.call(t)?E=t:(t||"string"==typeof t)&&j(t),E=r({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},E);var D=d(E.background);document.addEventListener("click",m),document.addEventListener("keyup",x),document.addEventListener("scroll",f),window.addEventListener("resize",z);var G={open:C,close:z,toggle:L,update:v,clone:k,attach:j,detach:M,on:_,off:S,getOptions:A,getImages:N,getZoomedImage:F};return G},x=e(107),v=e.n(x),k=(e(146),e(258)),j=e(259),M=e(284),_={components:{Header:k.a,Footer:j.a},data:function(){return{zoom:null,pageCount:v.a.themeConfig.pageCount,title:"",author:"",date:"",content:"",should404:!1,id:""}},fetch:function(){var n=this;if(!this.$route.params.id)return this.should404=!0;var t=this.$route.params.id.split(".")[0];if(!M[t])return this.should404=!0;Object.entries(M[t]).forEach((function(t){var e=Object(l.a)(t,2),r=e[0],m=e[1];return n[r]=m}))},mounted:function(){this.should404&&(location.href="/404"),this.zoom=f("p img")},head:function(){var n=["//cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css","//cdn.jsdelivr.net/npm/highlight.js@10.5.0/styles/github.css"].map((function(n){return{rel:"stylesheet",href:n}}));return{title:this.title,link:n}}},S=(e(285),e(41)),component=Object(S.a)(_,(function(){var n=this,t=n._self._c;return t("div",[t("Header"),n._v(" "),t("div",{staticClass:"page post-page"},[t("div",{staticClass:"title"},[t("div",{staticClass:"post-title"},[n._v(n._s(n.title))])]),n._v(" "),t("div",{staticClass:"info"},[t("div",{staticClass:"author"},[n._v(n._s(n.author))]),n._v(" "),t("div",{staticClass:"date"},[n._v(n._s(n.date))]),n._v(" "),n.pageCount?t("div",{staticClass:"count"},[t("span",{attrs:{id:"busuanzi_value_page_pv"}}),n._v(" "),t("span",[n._v("views")])]):n._e()]),n._v(" "),t("div",{staticClass:"post-content",domProps:{innerHTML:n._s(n.content)}}),n._v(" "),t("ClientOnly",[t("Vssue",{attrs:{title:"Vssue Demo",issueId:n.id}})],1)],1),n._v(" "),t("Footer")],1)}),[],!1,null,null,null);t.default=component.exports}}]); \ No newline at end of file diff --git a/_nuxt/static/1667843362/manifest.js b/_nuxt/static/1667843362/manifest.js deleted file mode 100644 index 3323eec..0000000 --- a/_nuxt/static/1667843362/manifest.js +++ /dev/null @@ -1 +0,0 @@ -__NUXT_JSONP__("manifest.js", {routes:["\u002F","\u002Fcv","\u002Fcategories\u002F2","\u002Fposts\u002F3","\u002Fcategories\u002F3","\u002Fcategories\u002F4","\u002Fcategories\u002F5","\u002Fposts\u002F4","\u002Fposts\u002F5","\u002Fposts\u002F6","\u002Fposts\u002F8","\u002Fposts\u002F9","\u002Fposts\u002F11","\u002Fposts\u002F12","\u002Fposts\u002F13","\u002Fposts\u002F14","\u002Fposts\u002F15","\u002Fposts\u002F16","\u002Fposts\u002F17","\u002Fposts\u002F20","\u002Fposts\u002F21","\u002Fposts\u002F22","\u002Fposts\u002F25","\u002Fposts\u002F26","\u002Fposts\u002F27","\u002Fposts\u002F28","\u002Fposts\u002F29","\u002Fposts\u002F30","\u002Fposts\u002F31","\u002Fposts\u002F34"]}) \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/34/payload.js b/_nuxt/static/1667843362/posts/34/payload.js deleted file mode 100644 index dabcbb4..0000000 --- a/_nuxt/static/1667843362/posts/34/payload.js +++ /dev/null @@ -1 +0,0 @@ -__NUXT_JSONP__("/posts/34", {data:[{}],fetch:{"0":{zoom:null,pageCount:true,title:"如何使用 5000 块组装一台顶配 Mac Studio",author:"Yidadaa",date:"2022\u002F11\u002F07",content:"\u003Cblockquote\u003E\n\u003Cp\u003E写给程序员的小尺寸高性能黑苹果主机装配指南,全文约 10000 字。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E本文同步发表至:\u003Ca href=\"https:\u002F\u002Fflowus.cn\u002Fyifei\u002Fshare\u002Fcb8f8b2f-591f-4a34-a901-b714a4c81bcc\"\u003EFlowUs\u003C\u002Fa\u003E \u002F \u003Ca href=\"https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F580506404\"\u003E知乎\u003C\u002Fa\u003E \u002F \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FYidadaa\u002FYidadaa.github.io\u002Fissues\u002F34\"\u003EGithub\u003C\u002Fa\u003E \u002F \u003Ca href=\"https:\u002F\u002Fblog.simplenaive.cn\u002F34\"\u003E博客\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374112-bac58562-f71f-4f32-b674-b012a275d56a.png\" alt=\"image\"\u003E\n左:Mac Studio,右:穷逼版\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374176-9667385a-dc3d-414e-86b6-f6c88cb4afc7.png\" alt=\"image\"\u003E\n与 24 寸显示器的对比\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374261-61910710-fe87-40c8-91a8-c59bcd20b5c0.png\" alt=\"image\"\u003E\n与 330ml 杯子的对比\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374347-dadcac5b-fde2-4b7f-a74c-c0e9da1b257b.png\" alt=\"image\"\u003E\n日常使用负载\u003C\u002Fp\u003E\n\u003Ch2\u003E长话短说\u003C\u002Fh2\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E性能对比\u003C\u002Fth\u003E\n\u003Cth\u003ECPU\u003C\u002Fth\u003E\n\u003Cth\u003E内存\u003C\u002Fth\u003E\n\u003Cth\u003E硬盘\u003C\u002Fth\u003E\n\u003Cth\u003E性能(R23)\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E体积\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMac Studio\u003C\u002Ftd\u003E\n\u003Ctd\u003EM1 Ultra\u003C\u002Ftd\u003E\n\u003Ctd\u003E64GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E1TB 固态\u003C\u002Ftd\u003E\n\u003Ctd\u003E多核 23705\u003C\u002Ftd\u003E\n\u003Ctd\u003E29999\u003C\u002Ftd\u003E\n\u003Ctd\u003E197mm * 197mm * 95mm = 3.68L\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E穷逼版\u003C\u002Ftd\u003E\n\u003Ctd\u003Ei7 12700\u003C\u002Ftd\u003E\n\u003Ctd\u003E64GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E1TB 固态\u003C\u002Ftd\u003E\n\u003Ctd\u003E多核 21568\u003C\u002Ftd\u003E\n\u003Ctd\u003E4946\u003C\u002Ftd\u003E\n\u003Ctd\u003E196mm * 218mm * 117mm = 4.9L\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Cul\u003E\n\u003Cli\u003E本文只面向程序员群体,优先保证编译性能和开发舒适度,不面向视频剪辑等创意工作者,不考虑视频剪辑性能;\u003C\u002Fli\u003E\n\u003Cli\u003E重点考虑 CPU 单核和多核性能,优先内存和硬盘容量,考虑机箱体积,不太考虑内存频率,不追求硬盘速度,不太考虑显卡性能,不太考虑噪音,完全不考虑外观精致程度;\u003C\u002Fli\u003E\n\u003Cli\u003E优点:便宜,量大管饱,装机完成后,可当做白苹果使用,无痛升级后续系统,可支持 MacOS、Windows 和 Linux 三系统共存;\u003C\u002Fli\u003E\n\u003Cli\u003E缺点:折腾,组装机箱和安装系统需要有一定动手能力,不适用于日薪大于 1w 的用户,不适用于无计算机硬件常识用户;\u003C\u002Fli\u003E\n\u003Cli\u003E硬件到位之后,在网络良好情况下,半天即可完成装机;\u003C\u002Fli\u003E\n\u003Cli\u003E多核编译同一个项目,M1 Max 需 6min,本文主机需 4min,多核性能对比与 R23 跑分对比大致保持一致。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch2\u003E阅读须知\u003C\u002Fh2\u003E\n\u003Cul\u003E\n\u003Cli\u003E适用群体:\n\u003Cul\u003E\n\u003Cli\u003E写代码的\u003C\u002Fli\u003E\n\u003Cli\u003E买不起 Mac Studio 的\u003C\u002Fli\u003E\n\u003Cli\u003E有一定的折腾能力的\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E不适用群体:\n\u003Cul\u003E\n\u003Cli\u003E打游戏的\u003C\u002Fli\u003E\n\u003Cli\u003E富哥富姐请直接使用钞能力\u003C\u002Fli\u003E\n\u003Cli\u003E剪视频的\u003C\u002Fli\u003E\n\u003Cli\u003E懒得折腾的(可以购置硬件后,购买淘宝黑苹果装机服务)\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E本文包含以下内容:\n\u003Cul\u003E\n\u003Cli\u003E提供了购买硬件和装机过程中的一些需要考虑的事项\u003C\u002Fli\u003E\n\u003Cli\u003E提供了分别对标 M1 Mac mini、M1 max Mac studio 和 M1 ultra Mac studio 性能(不含 GPU 和磁盘性能)且同等体积的装机购置清单\u003C\u002Fli\u003E\n\u003Cli\u003E提供了安装黑苹果所需的硬件考虑事项以及系统安装须知\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch2\u003E为什么需要装这样一台机器\u003C\u002Fh2\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E可以跳过此章节。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cp\u003E总而言之,对于我来说,主要有两个原因:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E\n\u003Cp\u003E主要原因:公司电脑性能拉跨,满足不了开发需要;\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003E次要原因:目前在用的 MacOS 13 Ventura 系统拉跨,降级麻烦。\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E所以,我需要搞一台高性能主机来替换掉公司发的老旧 MBP。\u003C\u002Fp\u003E\n\u003Cp\u003E其实公司配的 2020 款 MBP 并不算老,就是性能有点差,10 代标压 i5-1038NG7 处理器加 16GB 板载内存,多核性能大概是 M1 的一半,省着点还是可以用的。\u003C\u002Fp\u003E\n\u003Cp\u003E但我在 MacOS 13 技术预览版刚放出来时候,就迫不及待地手贱升级了一波,然后成功被新系统的各种卡顿 Bug 和内存占用虚高问题治好了低血压。即便到如今已经发布了正式版,这些问题仍旧存在,以至于日常开发时随便开个 VS Code 再加几个网页,内存就基本见底了。\u003C\u002Fp\u003E\n\u003Cp\u003E后来为了能保证正常开发,我不得不花一周时间把 Vim 打造成主力 IDE,以便直接在开发机上写代码。\u003C\u002Fp\u003E\n\u003Cp\u003E此外,除了 MacOS 拉跨,公司的项目本身也对设备性能要求很高。\u003C\u002Fp\u003E\n\u003Cp\u003E身后的同事老哥前段时间斥巨资入手了一款中配 M1 Max 款 Mac Studio,原因就是公司发放的设备很难满足开发需求。我们日常的技术栈是 C++ 和 React,前者在我那台十代标压 i5 上,全量编译整个项目需要花费 20 分钟到 30 分钟的时间,编译个几次项目,半天就没了,倒是非常适合划水。虽然大多数时间并不会全量编译,但每次增量编译耗时也在分钟级别,非常可观,十分难受。\u003C\u002Fp\u003E\n\u003Cp\u003E后者 React 更是重量级,NodeJS + HMR + 浏览器堪称内存黑洞,再配合 C++ 建立的 Clang 符号表索引,16GB 板载内存直接被榨干,一天下来大多数时间都在等编译和页面响应。\u003C\u002Fp\u003E\n\u003Cp\u003E当然这也并不是完全没有好处,由于等待的成本过于高昂,我们写下每行代码前都要深思熟虑,每次编译前都要检查个半天,所以那段时间的代码质量总感觉也高了不少 😆。\u003C\u002Fp\u003E\n\u003Cp\u003E后来我们安排了一台 36 核 72 线程(含两颗 18C36T Intel Xeon Gold 6240 CPU)的开发机,第一次用它编译项目的时候,有种便秘两周之后突然窜稀的畅快感觉,以前 20 多分钟的编译,现在两分钟就完事了,比以前跑单测都快。\u003C\u002Fp\u003E\n\u003Cp\u003E然而即便用了开发机和 Vim 之后,我还是经常被系统卡顿和内存不足烦得要死,随便开几个网页,该卡还是卡,就这样将就了两个多月,在某次系统彻底无响应之后,我一咬牙一狠心,决定加亿点钱解决这个问题。\u003C\u002Fp\u003E\n\u003Cp\u003E于是我便问了下身后老哥的 M1 Max 的编译时间,居然只要 6 分钟,已经完全够用了,怪不得他不怎么用开发机,我马上点开了苹果官网:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374857-d6067e58-1765-4da2-8929-a4b58b24b145.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E然后看到这个价格我火速关闭,那没事了,告辞。\u003C\u002Fp\u003E\n\u003Cp\u003E看来富哥的路是走不通了,只能使用穷逼的道路曲线救国了。\u003C\u002Fp\u003E\n\u003Ch2\u003E性能\u003C\u002Fh2\u003E\n\u003Cp\u003E苹果在发布会上把 M1 系列芯片吹得天花乱坠,拳打 Intel i9-11900,脚踢 RTX 3090,让我们来看看它的真面目:\u003C\u002Fp\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E芯片名称\u003C\u002Fth\u003E\n\u003Cth\u003E规格\u003C\u002Fth\u003E\n\u003Cth\u003ER23 单核跑分\u003C\u002Fth\u003E\n\u003Cth\u003ER23 多核跑分\u003C\u002Fth\u003E\n\u003Cth\u003E功耗\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIntel Core i5 1038NG7\u003C\u002Ftd\u003E\n\u003Ctd\u003E4 核 8 线程\u003C\u002Ftd\u003E\n\u003Ctd\u003E1116\u003C\u002Ftd\u003E\n\u003Ctd\u003E4979\u003C\u002Ftd\u003E\n\u003Ctd\u003E28W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EApple M1\u003C\u002Ftd\u003E\n\u003Ctd\u003E8 核\u003C\u002Ftd\u003E\n\u003Ctd\u003E1510\u003C\u002Ftd\u003E\n\u003Ctd\u003E7687\u003C\u002Ftd\u003E\n\u003Ctd\u003E6.8W ~ 39W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIntel Core i5 12400\u003C\u002Ftd\u003E\n\u003Ctd\u003E6 核 12 线程\u003C\u002Ftd\u003E\n\u003Ctd\u003E1697\u003C\u002Ftd\u003E\n\u003Ctd\u003E11905\u003C\u002Ftd\u003E\n\u003Ctd\u003E65W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EApple M1 Max\u003C\u002Ftd\u003E\n\u003Ctd\u003E10 核\u003C\u002Ftd\u003E\n\u003Ctd\u003E1533\u003C\u002Ftd\u003E\n\u003Ctd\u003E12337\u003C\u002Ftd\u003E\n\u003Ctd\u003E11W ~ 115W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIntel Core i7 12700\u003C\u002Ftd\u003E\n\u003Ctd\u003E12 核 20 线程\u003C\u002Ftd\u003E\n\u003Ctd\u003E1879\u003C\u002Ftd\u003E\n\u003Ctd\u003E21568\u003C\u002Ftd\u003E\n\u003Ctd\u003EPL1: 65W, PL2 180W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EApple M1 Ultra\u003C\u002Ftd\u003E\n\u003Ctd\u003E20 核\u003C\u002Ftd\u003E\n\u003Ctd\u003E1506\u003C\u002Ftd\u003E\n\u003Ctd\u003E23705\u003C\u002Ftd\u003E\n\u003Ctd\u003E13W ~ 215W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIntel Core i9 12900\u003C\u002Ftd\u003E\n\u003Ctd\u003E16 核 24 线程\u003C\u002Ftd\u003E\n\u003Ctd\u003E1988\u003C\u002Ftd\u003E\n\u003Ctd\u003E26455\u003C\u002Ftd\u003E\n\u003Ctd\u003EPL1: 65W, PL2 200W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Cp\u003E确实很强,比 10 代 intel 强多了,但是面对 12 代 intel 还是不太行,毕竟功耗在这摆着,i9 12900 勉强守住了桌面 CPU 的底裤。\u003C\u002Fp\u003E\n\u003Cp\u003E苹果 M1 系列的能耗比确实惊人,但是我要放在公司使用,完全不用担心电费问题,功耗都是图一乐,于是经过一周的调研,确定了硬件配置单。\u003C\u002Fp\u003E\n\u003Ch2\u003E硬件配置单\u003C\u002Fh2\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E下列配置中的硬件,除显卡和网卡外,均为全新价格。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E如需查看更多配置单(M1 \u002F M1 Max 级别),请跳转到文章末尾。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E部件\u003C\u002Fth\u003E\n\u003Cth\u003E产品\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E备注\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECPU\u003C\u002Ftd\u003E\n\u003Ctd\u003EIntel i7 12700f 散片\u003C\u002Ftd\u003E\n\u003Ctd\u003E2110\u003C\u002Ftd\u003E\n\u003Ctd\u003Ef 后缀代表不集成核显,比带核显版本便宜 100 块左右\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E主板\u003C\u002Ftd\u003E\n\u003Ctd\u003E铭瑄 H610itx 挑战者\u003C\u002Ftd\u003E\n\u003Ctd\u003E569\u003C\u002Ftd\u003E\n\u003Ctd\u003E最便宜的能带得动 i7 12700 的 itx 主板\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E内存条\u003C\u002Ftd\u003E\n\u003Ctd\u003E酷兽 32GB 3000Mhz 两根共 64GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E798\u003C\u002Ftd\u003E\n\u003Ctd\u003E最便宜的 DDR4 内存,有 2666mhz \u002F 3000mhz \u002F 3200Mhz 三个版本,价格一样,哪个便宜有货买哪个\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E硬盘\u003C\u002Ftd\u003E\n\u003Ctd\u003E光威弈 Pro 512GB nvme ssd\u003C\u002Ftd\u003E\n\u003Ctd\u003E295\u003C\u002Ftd\u003E\n\u003Ctd\u003E随便选的,可以加一百多块上 1TB 固态,反正便宜\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E散热器\u003C\u002Ftd\u003E\n\u003Ctd\u003E利民 Axp90 x53 + LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003Ctd\u003E159\u003C\u002Ftd\u003E\n\u003Ctd\u003E选购时需要考虑机箱适配的散热器高度,需要加 18 元额外购买利民 LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E机箱\u003C\u002Ftd\u003E\n\u003Ctd\u003E傻瓜超人 K39 4.9L + 显卡延长线\u003C\u002Ftd\u003E\n\u003Ctd\u003E199\u003C\u002Ftd\u003E\n\u003Ctd\u003E196mm * 117mm * 218mm = 4.9L 体积,当前配置的体积最小最便宜的选择\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E益衡 7030B 300W Flex 1U 电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E398\u003C\u002Ftd\u003E\n\u003Ctd\u003E这个价格是加 100 块让店家换全模组电源线和静音风扇之后的版本,组装的时候理线会更方便\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E显卡\u003C\u002Ftd\u003E\n\u003Ctd\u003EAMD RX550 2GB\u002F4GB Polaris 核心\u003C\u002Ftd\u003E\n\u003Ctd\u003E300\u003C\u002Ftd\u003E\n\u003Ctd\u003E二手亮机卡,有 Lexa 和 Polaris 核心两个版本,建议买 4GB 显存 Polaris 核心版本,各版本价格无区别\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E网卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E博通 BCM94360CS2 + m2 ngff 正向转接卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E118\u003C\u002Ftd\u003E\n\u003Ctd\u003E也可以选择 Intel AX201 或者更低端的 Intel 9560,能省个几十块钱,但需要额外配置驱动,没必要\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Cp\u003E我们一项一项来看,为什么要选用这些配置。\u003C\u002Fp\u003E\n\u003Ch3\u003ECPU 和主板\u003C\u002Fh3\u003E\n\u003Cp\u003E首先,要再次明确一下目标,我们想要 M1 Ultra 级别的性能,那就只能在 i7 12700 及以上的 CPU 中选,大概有这么几个选项:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E\n\u003Cp\u003EIntel i7 12700f 和 12700\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003EIntel i7 12700kf 和 12700k\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003EIntel i9 12900f\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003EIntel i9 12900kf 和 12900k\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E**为什么不选 AMD 的 CPU?**因为性价比不高,12 代 CPU 同价位性能比 AMD 5000 系列同价位性能稍强,价格稍便宜,而且目前 AMD 装黑苹果需要额外配置,有点麻烦,所以完全没有理由选 AMD。\u003C\u002Fp\u003E\n\u003Cp\u003E**为什么不选 13 代 Intel CPU?**因为性价比不高,目前只发布了带 K 后缀的高性能版本,需要搭配更好的主板和电源,而且 13 代 CPU 也相对更贵。\u003C\u002Fp\u003E\n\u003Cp\u003E然后,i7 和 i9 分别都有 f \u002F k \u002F kf 几个版本:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E\n\u003Cp\u003E其中凡是带 f 的,都是不带集成显卡的版本,考虑到我们是装黑苹果,12 代集成显卡不能被驱动,所以必须得配个独立显卡,选择 f 版本就行,可以省下 100 块;\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003E其中凡是带 k 的,都代表着性能增强版,大概比不带 k 的版本性能强 10% - 15%,价格也要贵 5% - 10%,酌情购买。\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E由于 i9 处理器需要主板有更强的供电能力,主板的价格就水涨船高,所以我们退而求其次,选择 i7 12700,性能只比 M1 Ultra 弱 10%。当然,也可以加点钱上 12700k,这样性能和 M1 Ultra 持平,不过这点差距在实际使用时基本没什么区别,12700k 对主板供电能力也有一定的要求,丐板估计带不动。\u003C\u002Fp\u003E\n\u003Cp\u003E主板就不提了,铭瑄 H610itx 在 600 块价位基本无敌,找不到其他能一起打的。唯一缺点就是 itx 主板接口有点少,只能插两根内存条,目前消费级内存条单条最大也就 32GB,所以最多只能上 64GB 内存。\u003C\u002Fp\u003E\n\u003Cp\u003E而且这种低端丐板没有额外的 M.2 接口,只能插一条 Nvme 固态,要扩容的话,要么换固态,要么加 SATA 硬盘。不过还好 512GB SSD 够用了,即便是 1TB 的 SSD 也只要 400 出头,可以自己决定要不要加钱。\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E购买散片板 U 套装即可\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch3\u003E内存条和硬盘\u003C\u002Fh3\u003E\n\u003Cp\u003E酷兽和光威是同一个厂,不同的产品线,酷兽主打性价比,399 块的 32GB 内存条要啥自行车,直接来两条插满。\u003C\u002Fp\u003E\n\u003Cp\u003E此外这个内存条有 2666mhz \u002F 3000mhz \u002F 3200mhz 三个频率版本,理论上铭瑄 h610itx 主板是支持 3200mhz 高频内存的,只需要在 BIOS 里设置一下就行,但是说实话内存频率感知不强,只有核显打游戏才对内存频率有要求,如果用来开发,随便买就行,高频内存属实浪费预算,哪个有货买哪个。\u003C\u002Fp\u003E\n\u003Cp\u003E最近固态硬盘也是白菜价,1tb 固态已经降到了 400 以下,当然也有更贵的性能更好的版本,但是对于开发来说感知不强,3000mb\u002Fs 和 7000mb\u002Fs 虽然看起来差距挺大,但是实际用的时候也就几百毫秒的差距,感知不出来,所以随便买个便宜的就行,重点还是看容量。\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E哪个便宜买哪个,频率和读写速度并不重要;\u003C\u002Fli\u003E\n\u003Cli\u003EMac Studio 的统一内存,官方读速是 800GB\u002Fs,与 3200mhz DDR4 内存条读速差不多;\u003C\u002Fli\u003E\n\u003Cli\u003E如果磁盘性能也想对标一下 Mac Studio,Mac Studio 硬盘速度为:读 5000MB\u002Fs,写 4000MB\u002Fs,大致与 PCIE 4.0 固态速度接近,所以可以酌情购买 PCIE 4.0 固态硬盘(铭瑄的这块主板支持,精粤的不支持),同体积大概是 PCIE 3.0 硬盘的 1.5 倍价格,512GB 版本大概 400 块可以搞定。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch3\u003E散热器、机箱和电源\u003C\u002Fh3\u003E\n\u003Cp\u003E这三兄弟要一起看,其实显卡也应该拿过来一起看,但是开发场景,显卡不是很重要,所以显卡另说吧。\u003C\u002Fp\u003E\n\u003Cp\u003E之所以要放一起看,主要是尺寸问题,我想要体积尽可能接近 Mac Studio,Mac Studio 的体积是 197mm * 197mm * 9.5mm = 3.68L,一开始我买的是下面这个机箱:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375317-17a17f5d-b8ae-424d-aab5-3cf3054e979e.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E整体尺寸是 187mm * 230mm * 97mm = 4.2L,非常小巧。\u003C\u002Fp\u003E\n\u003Cp\u003E我选择了其中的 A 背板版本,没有显卡位,把空间全部让给散热器和电源,散热器也是买的 Axp90 x53(90mm 宽,53mm 高)。\u003C\u002Fp\u003E\n\u003Cp\u003E因为我一开始并没有打算装黑苹果,想直接用 Linux 来开发,但是后来发现 Linux 没办法入公司内网,而且企业微信也是残废状态,所以才考虑装黑苹果,于是换成了下面的机箱:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375363-2379fe91-e33e-45f3-816a-1c58aad6c11a.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E体积大了一圈,来到了 196mm * 117mm * 218mm = 4.9L,不过好在便宜,加显卡延长线才 200 块,比上一个 300 块的机箱便宜很多,而且可以加装一个 19cm 以内的双槽短卡,非常适合本文的需求。\u003C\u002Fp\u003E\n\u003Cp\u003E从上面机箱的商品介绍就可以知道,这个机箱支持 56mm 以内的散热器,一般来讲,散热器的尺寸与散热能力正相关,基本上体积越大,散热能力越好,在看了一圈视频之后,出现次数的就是利民的axp90 系列的几个散热器,价格在 100 元到 150 元之间:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375397-361c99d5-7cf0-489b-837e-f4fb5244390f.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E可以看到主要有 36mm \u002F 47mm \u002F 53mm 三种高度,名字 AXP90 则代表 90mm * 90mm 的尺寸,为了最大化机箱利用率,我选用了 53mm 高度版本,实测整机满载温度可以控制在 80 摄氏度以内,日常使用则只有 27 摄氏度左右,好悬没给 CPU 给冻感冒。\u003C\u002Fp\u003E\n\u003Cp\u003E此外,如果你想选用一些更小尺寸的机箱,在选散热器的时候一定要注意,最好预留 3mm 以上的空间余量,比如如果限高 56mm 的机箱,如果配了一个 55mm 高度的散热器,风扇叶片和机箱离得太近,会出现十分明显的啸叫。\u003C\u002Fp\u003E\n\u003Cp\u003E然后是电源,前面了解到 i7 12700 的满载功耗是 180w,再加上 50w 亮机显卡的功耗,总计 230w 的实际载荷,按照 80% 的电源转化率,我们只需要选择 290w 以上的电源即可满足需求。在尺寸方面,由于我们想让机箱的体积尽可能的小,而小尺寸 itx 机箱往往搭配使用小 1u 尺寸的 Flex 电源,这类电源的价格要比常规尺寸的电源稍贵一点,如果想要省钱,可以选购支持 SFX 电源的更大尺寸的机箱。\u003C\u002Fp\u003E\n\u003Cp\u003E最终我选择了益衡的 7030B 300w 电源:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375452-b24da35f-415b-48cc-8953-af0ebe584500.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375491-e805645b-fc76-4d2c-957f-03bb3a463178.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E小 1u 电源还有个缺点是会比较吵,因为受体积限制,风扇必须提高转速才能保证散热,运行时会有比较明显的高频噪声,所以我选择加了 100 块,买了店家改装之后的版本(上图右侧)。\u003C\u002Fp\u003E\n\u003Cp\u003E改装版本换了更静音的风扇,并且配备了全模组电源线,大概长下面这个样子:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375578-2df51c37-0c56-486c-ba22-d36fd79793b7.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E电源线统一成了黑色的软线,比原装的五颜六色的硬线好看多了,而且装机的时候理线会非常简单,这一点在小机箱里尤其关键,所以这 100 块还是挺值的。\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E购买全模组小 1u 电源,考虑购买全模组 + 静音风扇的改装版本\u003C\u002Fli\u003E\n\u003Cli\u003E购买散热器时注意机箱散热器限高\u003C\u002Fli\u003E\n\u003Cli\u003E利民散热器默认附带的主板支架只能与上一代 Intel CPU 搭配使用,需要额外花费 18 元购买利民 LGA17xx 扣具\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch3\u003E显卡\u003C\u002Fh3\u003E\n\u003Cp\u003E显卡需要着重讲一下,因为 MacOS 非常挑显卡,目前市售的 RTX 系列显卡基本都是驱动不了的,只要一些比较老的 Nvidia 显卡才能驱动。而大多数 AMD 显卡都能驱动,比如最近出的 6600 和 6700,但是 6400 除外,详细的 GPU 列表可以\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FGPU-Buyers-Guide\u002Fmodern-gpus\u002Famd-gpu.html#native-amd-gpus\"\u003E查看这里\u003C\u002Fa\u003E。\u003C\u002Fp\u003E\n\u003Cp\u003E由于我们并不需要特别强劲的显卡性能,所以只需要选择能够正常驱动的低功耗亮机卡即可,我一开始就直接从比较老的 R7 和 R9 系列显卡开始看:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375686-da4a53e8-daf6-4927-87dd-b5586b5628cc.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E可以看到有非常多的选项,我在淘宝搜了一圈之后,发现最低端的 R7 240\u002F250 显卡只要 100 块,于是就直接下单买了一块,结果入手后折腾了许久才发现,这张 2013 年发售的老显卡,最高只能运行在 MacOS 10.x 系统上,最新的 12.x 系统是没法完美驱动的。我花了大量的时间搜索国内外关于这张 Oland 核心的显卡驱动帖子,没有发现任何人能成功在 11.x 及以上的系统上成功运行过它,虽然可以使用仿冒显卡 ID 的方法成功安装并进入系统,但是却无法使用 Metal GPU 加速,导致系统所有的高斯模糊效果以及动效都是缺失的,完全无法满足日常使用。\u003C\u002Fp\u003E\n\u003Cp\u003E于是我就又斥巨资花了 300 块买了一张 Rx550,这张显卡发售于 2017 年,网上有相当多的黑苹果视频表示这张卡可以完美工作在最新的 MacOS 13.0 系统上,而且完全不需要任何额外操作,插上就能用。当然,前提是买到了正确的版本的显卡,这张显卡有两种核心版本:Lexa 核心和 Buffin 核心(或称 Polaris 核心),其中 Buffin(Polaris) 核心是可以即插即用,但是 Lexa 核心则需要\u003Ca href=\"https:\u002F\u002Fwww.bilibili.com\u002Fread\u002Fcv15800495\"\u003E仿冒参数\u003C\u002Fa\u003E才能工作。\u003C\u002Fp\u003E\n\u003Cp\u003E很不幸我买的这张 Rx550 2GB 联想拆机卡是 Lexa 核心,不过由于有了之前折腾 R7 240 的经历,对我来说还算简单。\u003C\u002Fp\u003E\n\u003Cp\u003E此外这张显卡有 2GB 显存和 4GB 显存两个版本,价格基本一样,我个人建议购买 4GB 版本,因为我在实际使用时发现,MacOS 12.x 系统很容易就把 2GB 显存吃满了,所以能买大的还是买大点的。\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375752-81118659-e6fe-443e-bf82-bad353cf5940.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E盈通的这张 RX550 4GB 就很适合,虽然是二手,有翻车的风险,但是我们并不会用来打游戏,所以只要它能点亮就行,反正日常负载不会太高,暴毙的可能性比较小。\u003C\u002Fp\u003E\n\u003Cp\u003E如果实在担心翻车,可以加点钱买全新版本,这张卡有全新正品在售,价格在 500 块到 600 块之间:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375812-0c4657dc-fded-407c-836b-132ded517957.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E购买 RX550 4GB Polaris 核心版本\u003C\u002Fli\u003E\n\u003Cli\u003E省钱就买二手,图稳就加点钱买全新\u003C\u002Fli\u003E\n\u003Cli\u003E购买显卡时注意显卡尺寸是否兼容机箱\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch3\u003E无线网卡\u003C\u002Fh3\u003E\n\u003Cp\u003E无线网卡也值得花点篇幅来说一说,目前黑苹果无线网卡可选 Intel NGFF 接口的一系列网卡,以及搭配转接板使用的博通 BCM 系列苹果拆机显卡:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375892-727c87ae-e39e-48eb-8dfb-fe4495c209cf.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E如果从省事的角度来看,直接买博通的拆机网卡即可,插上就能用。如果想省点钱,就买 Intel 最低端的 3168 系列:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375922-f435efa8-1651-462d-bfb6-8cddf0228072.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E其实在几年前,Intel 的无线网卡是没办法在苹果系统上使用的,后来有个大佬搓了一个驱动出来,然后 Intel 几乎所有型号的无线网卡一夜之间焕发第二春,具体的支持列表可以查看下面的链接:\u003Ca href=\"https:\u002F\u002Fopenintelwireless.github.io\u002Fitlwm\u002FCompat.html#dvm-iwn\"\u003ECompatibility | OpenIntelWireless\u003C\u002Fa\u003E。\u003C\u002Fp\u003E\n\u003Cp\u003E此外无线网卡的天线,有内置和外置两种区别:\u003C\u002Fp\u003E\n\u003Cimg width=\"202\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376150-0858b518-afca-46c7-80fa-5b5381c66631.png\"\u003E\n\u003Cimg width=\"202\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376191-3dcfe932-0251-4d42-a9d3-b4261c297aa7.png\"\u003E\n\u003Cp\u003E两者的区别:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E外置天线是机箱后面的小尾巴,看起来可能不太美观,但是信号比较稳定,增益较强;\u003C\u002Fli\u003E\n\u003Cli\u003E内置天线则需要粘在机箱上,容易受机箱内部杂波干扰,增益较小,信号不太稳定,但由于不需要伸出机箱,所以会比较美观一点。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E我选择了 Intel 9560 内置天线版本,花费 48 块,实装之后可以完美使用 WIFI 和蓝牙,隔空投送也可以正常使用,但是内置天线信号不稳定,蓝牙连接妙控板非常卡顿,基本无法日常使用。而且由于网卡规格较低,虽然标称 2.4G 300Mbps \u002F 5G 1733Mbps,但实测只能跑到 50Mbps,可能最大的原因还是这个内置天线的信号太差,干扰太多。\u003C\u002Fp\u003E\n\u003Cp\u003E不过问题不大,这个主机的 WIFI 纯粹是为了应付公司的入网认证,认证完了之后就直接关掉用有线连接了,如果你对无线网络有需求,建议购买高规格的外置天线版本。\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E购买博通 BCM 94360CS + 正向转接卡 + 外置天线版本,不用折腾,信号更好。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch2\u003E装机\u003C\u002Fh2\u003E\n\u003Cp\u003E十分推荐先观看此视频:[DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!](【DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!】 https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1v54y1Z7Er\u002F?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9),视频中的配置与本文配置基本一致,可以参考。\u003C\u002Fp\u003E\n\u003Cimg width=\"548\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376409-19d4c0eb-2acc-42f7-a80b-036677c214cb.png\"\u003E\n\u003Cp\u003E先上所有硬件的全家福,图中不包含显卡和网卡,因为拍摄照片的时候显卡和网卡还没到。\u003C\u002Fp\u003E\n\u003Ch3\u003E安装 CPU\u003C\u002Fh3\u003E\n\u003Cimg width=\"274\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376495-af4db007-b6da-4a49-8a92-bb302c9d70ec.png\"\u003E\n\u003Cp\u003E主板说明书会提示如何安装 CPU,一定要小心不要弄坏了针脚,主板上的金属压杆会比较紧,确认 CPU 按照防呆口安装妥当后,稍加用力按下压杆,将 CPU 固定好即可。\u003C\u002Fp\u003E\n\u003Ch3\u003E上电测试\u003C\u002Fh3\u003E\n\u003Cp\u003E先别急着往机箱里塞,把内存条和硬盘插好之后,连接电源线和键鼠,用螺丝刀碰触主板上的开机引脚,显示器能够显示 BIOS 界面,则表示成功点亮。\u003C\u002Fp\u003E\n\u003Ch3\u003E安装散热器\u003C\u002Fh3\u003E\n\u003Cp\u003E按照散热器说明书进行安装即可,注意需要额外购买利民 LGA17xx 扣具。\u003C\u002Fp\u003E\n\u003Ch3\u003E安装机箱\u003C\u002Fh3\u003E\n\u003Cp\u003E按照机箱说明书安装即可,建议先安装电源,再安装主板,然后接电源线,安装显卡延长线,安装显卡。\u003C\u002Fp\u003E\n\u003Cimg width=\"609\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376596-5fc17184-3e23-40ff-921e-d07882522f97.png\"\u003E\n\u003Ch2\u003E成品展示\u003C\u002Fh2\u003E\n\u003Cimg width=\"543\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376705-5ab95e41-da17-42ec-bfbb-7ed44c9d5c17.png\"\u003E\n\u003Cimg width=\"538\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376804-8948e19c-6fdd-4c1d-be37-8a61d18684da.png\"\u003E\n\u003Cp\u003E最终的成品大概比 Mac Studio 大了一圈,当然精致程度是没法比的,如果想要精致,可以加钱买更贵的全铝合金一体成型机箱(5.6L 体积):\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376871-9d4540ef-a61e-44cf-b8f7-576fc212d69e.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Ch2\u003E系统\u003C\u002Fh2\u003E\n\u003Cp\u003E可以直接跳转到流程清单部分,如果你不需要安装 MacOS,可以无视下列内容。\u003C\u002Fp\u003E\n\u003Ch3\u003E为什么不用 Windows 和 Linux?\u003C\u002Fh3\u003E\n\u003Cp\u003E在最开始决定装机的时候,我就在考虑要使用什么操作系统作为主力开发环境,前文已经提到我需要频繁编译,所以编译性能是我首要考虑的问题,这样 Window 肯定就不能用了,Windows 下 C++ 编译性能一向令人诟病,即便有了 WSL,由于它运行在 Hyper-V 虚拟机中,导致 IO 性能跟不上,实际的性能大概是 Linux 原生的 80% 左右,直接损耗了 20% 的性能,要知道 Intel 的 CPU 一次代际升级的性能也才差不多 25%,Window 直接就让 CPU 性能倒退一代,这绝对是无法接受的。\u003C\u002Fp\u003E\n\u003Cp\u003E我随后的想法是使用 Linux 作为日常开发系统,并使用 KVM 安装 Windows 虚拟机来解决常用软件问题,我也确实尝试了这种做法,并依次使用了下列 Linux 发行版:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E使用 KDE 桌面的 Debian,由于 Stable 版本的 Linux 内核太老,导致无法在 12 代 Intel 平台上正常安装,尝试几次后均未果,作罢;\u003C\u002Fli\u003E\n\u003Cli\u003E使用 KDE 桌面的 Ubuntu,也就是 KUbuntu,这个发行版确实很不错,KDE 对高分屏的支持非常不错,而且窗口主题和动效都很细腻,我用了大概一周时间,在上面使用 KVM 分别安装了 Windows 10 LTSC 以及 Mac OS 12 的虚拟机,前者用于运行企业微信,后者则是装来玩的,由于没有独显直通,Mac OS 虚拟机的运行流畅度十分感人。在 Linux 上运行 Windows 软件的另一个方法是使用 Wine,但无奈兼容性欠佳,无法正常运行最新版本的企业微信,微信倒是可以正常使用;\u003C\u002Fli\u003E\n\u003Cli\u003E使用 Gnome 的 Ubuntu,Gnome 对高分屏的支持太差劲,大多数应用没有对 Wayland 做兼容,甚至浏览器在分数倍缩放下都很糊,而且同样无法通过 Wine 运行企业微信,作罢;\u003C\u002Fli\u003E\n\u003Cli\u003E最后是国产的 Deepin,应该是最贴近日常使用的 Linux 发行版了,应用商店里有 Deepin 团队维护的 Wine 版微信和企业微信,但是企业微信还有点问题,虽然可以正常聊天,但是邮箱界面始终白屏,另外 Tim 也是卧龙凤雏,可以安装但无法启动。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E我大概用了一周多的 Deepin,并成功在上面编译运行了公司的项目,编译时间大概是开发机的 2 倍速度,可以看到 i7 12700 的多核性能相当强悍。\u003C\u002Fp\u003E\n\u003Cp\u003E但是网络问题始终没有办法很好的解决,公司的内网认证软件是阿里出品的阿里郎的阉割版本,其入网认证条件非常严格,无法在 Windows 虚拟机中完成认证,更不用提 Wine 了,也没有提供 Linux 版本,导致我的主机就算插上网线也无法访问内网资源,而且这个软件入网必须要有无线网卡,大多数 USB 网卡都无法在 Linux 下免驱运行。\u003C\u002Fp\u003E\n\u003Cp\u003E为了解决这个问题,我琢磨了很久计算机网络相关的知识,大概有这么几种方法:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E最容易想到的方法自然是用公司的苹果笔记本入网,然后当跳板机开代理给主机用,确实可以用,但是基于代理的方法,无法保证主机的所有流量都走代理,某些应用可能会绕开系统的代理策略,自行建立连接,这种方法不太稳定;\u003C\u002Fli\u003E\n\u003Cli\u003E然后比较容易想到的是使用 Mac 的网络共享,无奈在入网认证使用 802.1x 的安全策略,系统会直接禁用掉网络共享,这条路也走不通;\u003C\u002Fli\u003E\n\u003Cli\u003E最后想到的方法时把 Mac 模拟成一台路由器,直接网线直连主机,然后手动分配 IP 地址即可,经过一番搜索,其实只要开启 Mac 的 IP 路由转发即可,然后将来自 USB 网卡的所有流量都转发到无线网卡上,即可大功告成。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E关于折腾 Linux 日用开发环境的记录,可以看这篇文章:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fflowus.cn\u002F5a11ca75-455d-4a67-840a-42939ef91c2c\"\u003ELinux 开发环境备忘录\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E最后由于 Linux 上无法找到顺手的快速切换窗口的软件(类似 Mac Spotlight 的功能),还是决定安装黑苹果,可以直接无缝复用 MBP 的那一套工作流。\u003C\u002Fp\u003E\n\u003Ch3\u003E主要工具:OpenCore\u003C\u002Fh3\u003E\n\u003Cp\u003E安装黑苹果是一件非常需要耐心的事情,目前主流的安装黑苹果的工具是 OpenCore,其官方教程非常详细,流程也非常长,新手看了也非常头大:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FOpenCore-Install-Guide\u002F\"\u003EOpenCore Install Guide\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E在翻阅许久之后,我决定放弃跟着官方教程走,官方手册的内容只适合当作速查表来使用,如果跟着从零开始做,不知道搞到猴年马月。\u003C\u002Fp\u003E\n\u003Cp\u003E先列举一下我认为比较有用的内容,首先是显卡购买指南:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FGPU-Buyers-Guide\u002F\"\u003EIntroduction | GPU Buyers Guide\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E里面列举了各种可以用于黑苹果的显卡型号,以及它们支持的系统版本,当然这个指南中的某些老显卡的信息是错的,比如我前面提到的 R7 240,手册上说可以在最新的系统中通过仿冒 ID 的方式运行,但其实只能在 OS 10.x 以前的系统上运行,不过大多数信息都是准确的,可以放心参考。\u003C\u002Fp\u003E\n\u003Cp\u003E其次是 GPU 仿冒 ID 指南,如果你想折腾一下不直接免驱的显卡,可以参考它来仿冒 ID:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FGetting-Started-With-ACPI\u002FUniversal\u002Fspoof.html\"\u003ERenaming GPUs (SSDT-GPU-SPOOF) | Getting Started With ACPI\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E我个人不推荐用这种方式来搞,太费时间,而且不一定有用,还是建议直接购买免驱显卡。\u003C\u002Fp\u003E\n\u003Ch3\u003E流程清单\u003C\u002Fh3\u003E\n\u003Cp\u003E如果自己搞不定,可以直接在淘宝购买黑苹果装机服务,远程手把手指导,价格在 100 元到几百元不等。\u003C\u002Fp\u003E\n\u003Cp\u003E可以看这个视频,了解大概步骤:\u003Ca href=\"https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1K341137pj\u002F?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9\"\u003E刷新记录!12代平台12400F仅需24秒即可完成黑苹果系统安装,请问还有谁?\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E通用流程:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E硬件准备:\n\u003Cul\u003E\n\u003Cli\u003E16GB 及以上容量的内存卡或者移动硬盘,并格式化为 FAT32 格式;\u003C\u002Fli\u003E\n\u003Cli\u003E一台 Windows 电脑(Linux 或 Mac 也行);\u003C\u002Fli\u003E\n\u003Cli\u003E组装好的等待安装系统的主机;\u003C\u002Fli\u003E\n\u003Cli\u003E良好的有线网络环境;\u003C\u002Fli\u003E\n\u003Cli\u003E耐心。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E准备 EFI 引导文件:\u003Ca href=\"https:\u002F\u002Fflowus.cn\u002F6bb40d7b-8da6-464b-a551-0d0f0d663746\"\u003E各版本 EFI 引导文件\u003C\u002Fa\u003E\n\u003Cul\u003E\n\u003Cli\u003E如果你的硬件配置是本文推荐的配置,那么直接下载上面列表中的安装包,解压即可;\u003C\u002Fli\u003E\n\u003Cli\u003E或者你可以在 Github 上搜索自己的主板名称,比如我就参考了这个仓库(\u003Ca href=\"https:\u002F\u002Fgithub.com\u002FCrack-DanShiFu\u002FHackintosh-MAXSUN--H610ITX-I512400-rx560\"\u003Eh610itx + i5 12400 + rx560 + bcm943602cs 网卡\u003C\u002Fa\u003E)和这个仓库(\u003Ca href=\"https:\u002F\u002Fgithub.com\u002FLimeVista\u002FHackintosh-H610-12490F-AX201\"\u003Eh610itx + i5 12490f + rx560 + intel ax201 网卡\u003C\u002Fa\u003E),在他们的基础上进行了少量修改,就适配了本文的硬件;\u003C\u002Fli\u003E\n\u003Cli\u003E经过上述步骤,你会获得一个 EFI 文件夹,将其拷贝到 U 盘或者移动硬盘中即可,如果你的硬件与上述现有的配置都不一样,请查阅常见问题章节。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E准备基础安装镜像:\n\u003Cul\u003E\n\u003Cli\u003EOpenCore 提供了一个很细致的教程来下载安装镜像,基础镜像大概 600MB 左右,按照这个教程(\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FOpenCore-Install-Guide\u002Finstaller-guide\u002Fwinblows-install.html#downloading-macos\"\u003E在 Windows 上下载安装镜像\u003C\u002Fa\u003E)下载镜像文件并复制到 U 盘即可。\u003C\u002Fli\u003E\n\u003Cli\u003E将 U 盘插入主机,按下 DEL 键进入主板的 BIOS 界面,如果你使用了其他主板,请自行搜索如何进入 BIOS,然后将 U 盘设置为第一启动项即可;\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E启动启动后进入安装界面:\n\u003Cul\u003E\n\u003Cli\u003E选择磁盘工具,将 SSD 所在盘符格式化为 APFS 格式,并命名为 MacOS(或者其他任何英文名);\u003C\u002Fli\u003E\n\u003Cli\u003E退出磁盘工具,点击安装 MacOS,进入常规安装流程;\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E在网络良好情况下,自动重启若干次后,大概 40 分钟即可安装成功;\u003C\u002Fli\u003E\n\u003Cli\u003E安装成功后,自动重启进入系统选择界面,选择 MacOS 盘符进入即可;\u003C\u002Fli\u003E\n\u003Cli\u003E将 EFI 文件夹复制到 SSD 硬盘上,可参考这个教程:\u003Ca href=\"https:\u002F\u002Fapple.sqlsec.com\u002F5-%E5%AE%9E%E6%88%98%E6%BC%94%E7%A4%BA\u002F5-6.html\"\u003E完善引导\u003C\u002Fa\u003E;\u003C\u002Fli\u003E\n\u003Cli\u003E到现在,你已经可以正常使用 MacOS,但是还无法使用 Apple ID 和 iCloud,你还需要生成自己的硬件序列号,请参考这个教程:\u003Ca href=\"https:\u002F\u002Fsleele.com\u002F2019\u002F03\u002F21\u002Fsmbios\u002F\"\u003E为自己的黑苹果生成随机三码\u003C\u002Fa\u003E;\u003C\u002Fli\u003E\n\u003Cli\u003E至此,大功告成。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch2\u003E常见问题\u003C\u002Fh2\u003E\n\u003Ch3\u003E如何查看我买到的显卡的核心?\u003C\u002Fh3\u003E\n\u003Ch4\u003E搜索\u003C\u002Fh4\u003E\n\u003Col\u003E\n\u003Cli\u003E优先询问卖家,其次在搜索引擎中搜索显卡型号 + 核心即可。\u003C\u002Fli\u003E\n\u003C\u002Fol\u003E\n\u003Ch4\u003E使用 PE 工具\u003C\u002Fh4\u003E\n\u003Col\u003E\n\u003Cli\u003E下载安装\u003Ca href=\"https:\u002F\u002Fwww.wepe.com.cn\u002Fdownload.html\"\u003E微 PE 工具箱\u003C\u002Fa\u003E,并安装到 U 盘上;\u003C\u002Fli\u003E\n\u003Cli\u003E下载 GPU-Z:\u003Ca href=\"https:\u002F\u002Fwww.techpowerup.com\u002Fgpuz\u002F\"\u003Ehttps:\u002F\u002Fwww.techpowerup.com\u002Fgpuz\u002F\u003C\u002Fa\u003E,复制到已经安装了 PE 工具的 U 盘中;\u003C\u002Fli\u003E\n\u003Cli\u003E进入 BIOS,将 U 盘设置为第一引导,进入 PE 系统,运行 GPU-Z,即可看到显卡核心。\u003C\u002Fli\u003E\n\u003C\u002Fol\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200377738-ce96c52f-d78e-43b8-8e02-922d58260553.png\" alt=\"image\"\u003E\n第一个红框处,即是显卡核心名称。\u003C\u002Fp\u003E\n\u003Ch4\u003E使用现有的 Windows 主机\u003C\u002Fh4\u003E\n\u003Col\u003E\n\u003Cli\u003E把显卡插到现有的 Windows 主机上;\u003C\u002Fli\u003E\n\u003Cli\u003E下载安装 GPU-Z,即可看到显卡核心。\u003C\u002Fli\u003E\n\u003C\u002Fol\u003E\n\u003Ch3\u003E买到了 Lexa 核心的 Rx550,该怎么办?\u003C\u002Fh3\u003E\n\u003Cp\u003E参考这个教程,仿冒 ID 即可:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fwww.bilibili.com\u002Fread\u002Fcv15800495\"\u003E【黑苹果】通过仿冒ID驱动Lexa核心的Radeon RX 550\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Ch3\u003E12 代处理器如何开启小核心支持?\u003C\u002Fh3\u003E\n\u003Cp\u003E如果你是在别人的 EFI 文件基础上修改,并且别人的 EFI 文件基于 12 代 i3 或者 i5 处理器,而你的处理器是 12 代 i7 及以上,则需要开启小核心支持,查阅:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fblog.csdn.net\u002FZ17362251225\u002Farticle\u002Fdetails\u002F125412246\"\u003E黑苹果开启十二代酷睿能效核心的驱动_豆豆本豆儿的博客-CSDN博客\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Ch2\u003E装机单\u003C\u002Fh2\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E价格可能略有浮动。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003EM1 Max 和 M1 Pro 性能几乎一致,不作区分。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Ch3\u003EM1 Mac Mini 同等性能(3155 元,32GB + 512GB SSD)\u003C\u002Fh3\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E部件\u003C\u002Fth\u003E\n\u003Cth\u003E产品\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E备注\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECPU\u003C\u002Ftd\u003E\n\u003Ctd\u003EIntel i3 12100f 散片\u003C\u002Ftd\u003E\n\u003Ctd\u003E668\u003C\u002Ftd\u003E\n\u003Ctd\u003E\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E主板\u003C\u002Ftd\u003E\n\u003Ctd\u003E铭瑄 h610itx 挑战者\u003C\u002Ftd\u003E\n\u003Ctd\u003E568\u003C\u002Ftd\u003E\n\u003Ctd\u003E\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E内存条\u003C\u002Ftd\u003E\n\u003Ctd\u003E酷兽夜枭 16GB 3200Mhz 两根共 32GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E470\u003C\u002Ftd\u003E\n\u003Ctd\u003E京东自营即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E硬盘\u003C\u002Ftd\u003E\n\u003Ctd\u003E光威弈 Pro 512GB nvme ssd\u003C\u002Ftd\u003E\n\u003Ctd\u003E295\u003C\u002Ftd\u003E\n\u003Ctd\u003E按自己喜好选即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E散热器\u003C\u002Ftd\u003E\n\u003Ctd\u003E利民 Axp90 x47 + LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003Ctd\u003E139\u003C\u002Ftd\u003E\n\u003Ctd\u003E不用太强力的散热器,随便压一压\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E机箱\u003C\u002Ftd\u003E\n\u003Ctd\u003E傻瓜超人 K39 + 定制显卡延长线\u003C\u002Ftd\u003E\n\u003Ctd\u003E200\u003C\u002Ftd\u003E\n\u003Ctd\u003E一定要买定制显卡延长线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E益衡 7030B 300W Flex 1U 电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E398\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买全模组定制版本\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E显卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E盈通 Rx550 4GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E299\u003C\u002Ftd\u003E\n\u003Ctd\u003E淘宝二手,记得询问客服是否是 Polaris 核心\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E网卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E博通 BCM94360CS2 + m2 ngff 正向转接卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E118\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买外置天线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Ch3\u003EM1 Max Mac Studio 同等性能(3537 元,32GB + 512GB SSD)\u003C\u002Fh3\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E部件\u003C\u002Fth\u003E\n\u003Cth\u003E产品\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E备注\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECPU\u003C\u002Ftd\u003E\n\u003Ctd\u003EIntel i5 12400f 散片\u003C\u002Ftd\u003E\n\u003Ctd\u003E1050\u003C\u002Ftd\u003E\n\u003Ctd\u003E\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E主板\u003C\u002Ftd\u003E\n\u003Ctd\u003E铭瑄 h610itx 挑战者\u003C\u002Ftd\u003E\n\u003Ctd\u003E568\u003C\u002Ftd\u003E\n\u003Ctd\u003E\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E内存条\u003C\u002Ftd\u003E\n\u003Ctd\u003E酷兽夜枭 16GB 3200Mhz 两根共 32GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E470\u003C\u002Ftd\u003E\n\u003Ctd\u003E京东自营即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E硬盘\u003C\u002Ftd\u003E\n\u003Ctd\u003E光威弈 Pro 512GB nvme ssd\u003C\u002Ftd\u003E\n\u003Ctd\u003E295\u003C\u002Ftd\u003E\n\u003Ctd\u003E按自己喜好选即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E散热器\u003C\u002Ftd\u003E\n\u003Ctd\u003E利民 Axp90 x47 + LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003Ctd\u003E139\u003C\u002Ftd\u003E\n\u003Ctd\u003E不用太强力的散热器,随便压一压\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E机箱\u003C\u002Ftd\u003E\n\u003Ctd\u003E傻瓜超人 K39 + 定制显卡延长线\u003C\u002Ftd\u003E\n\u003Ctd\u003E200\u003C\u002Ftd\u003E\n\u003Ctd\u003E一定要买定制显卡延长线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E益衡 7030B 300W Flex 1U 电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E398\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买全模组定制版本\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E显卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E盈通 Rx550 4GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E299\u003C\u002Ftd\u003E\n\u003Ctd\u003E淘宝二手,记得询问客服是否是 Polaris 核心\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E网卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E博通 BCM94360CS2 + m2 ngff 正向转接卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E118\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买外置天线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Ch3\u003EM1 Ultra Mac Studio 同等性能(5159 元,64GB + 1TB SSD)\u003C\u002Fh3\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E部件\u003C\u002Fth\u003E\n\u003Cth\u003E产品\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E备注\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECPU\u003C\u002Ftd\u003E\n\u003Ctd\u003EIntel i7 12700f 散片\u003C\u002Ftd\u003E\n\u003Ctd\u003E2110\u003C\u002Ftd\u003E\n\u003Ctd\u003E淘宝找个人多的店买就行\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E主板\u003C\u002Ftd\u003E\n\u003Ctd\u003E铭瑄 h610itx 挑战者\u003C\u002Ftd\u003E\n\u003Ctd\u003E568\u003C\u002Ftd\u003E\n\u003Ctd\u003E官方旗舰店\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E内存条\u003C\u002Ftd\u003E\n\u003Ctd\u003E酷兽夜枭 32GB 3200Mhz 两根共 64GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E798\u003C\u002Ftd\u003E\n\u003Ctd\u003E京东自营即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E硬盘\u003C\u002Ftd\u003E\n\u003Ctd\u003E光威弈 Pro 1TB nvme ssd\u003C\u002Ftd\u003E\n\u003Ctd\u003E499\u003C\u002Ftd\u003E\n\u003Ctd\u003E如果不需要 1TB,可以换成 512GB,酌情购买\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E散热器\u003C\u002Ftd\u003E\n\u003Ctd\u003E利民 Axp90 x53 + LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003Ctd\u003E169\u003C\u002Ftd\u003E\n\u003Ctd\u003E买 53mm 版本,记得买 17xx 扣具\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E机箱\u003C\u002Ftd\u003E\n\u003Ctd\u003E傻瓜超人 K39 + 定制显卡延长线\u003C\u002Ftd\u003E\n\u003Ctd\u003E200\u003C\u002Ftd\u003E\n\u003Ctd\u003E一定要买定制显卡延长线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E益衡 7030B 300W Flex 1U 电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E398\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买全模组定制版本\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E显卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E盈通 Rx550 4GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E299\u003C\u002Ftd\u003E\n\u003Ctd\u003E淘宝二手,记得询问客服是否是 Polaris 核心\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E网卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E博通 BCM94360CS2 + m2 ngff 转接卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E118\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买 ngff 转接卡,带外置天线版本\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n",should404:false,id:34}},mutations:void 0}); \ No newline at end of file diff --git a/_nuxt/static/1667843362/categories/2/payload.js b/_nuxt/static/1667922389/categories/2/payload.js similarity index 100% rename from _nuxt/static/1667843362/categories/2/payload.js rename to _nuxt/static/1667922389/categories/2/payload.js diff --git a/_nuxt/static/1667843362/categories/2/state.js b/_nuxt/static/1667922389/categories/2/state.js similarity index 75% rename from _nuxt/static/1667843362/categories/2/state.js rename to _nuxt/static/1667922389/categories/2/state.js index 23346c5..508587c 100644 --- a/_nuxt/static/1667843362/categories/2/state.js +++ b/_nuxt/static/1667922389/categories/2/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fcategories\u002F2",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fcategories\u002F2",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/categories/3/payload.js b/_nuxt/static/1667922389/categories/3/payload.js similarity index 100% rename from _nuxt/static/1667843362/categories/3/payload.js rename to _nuxt/static/1667922389/categories/3/payload.js diff --git a/_nuxt/static/1667843362/categories/3/state.js b/_nuxt/static/1667922389/categories/3/state.js similarity index 75% rename from _nuxt/static/1667843362/categories/3/state.js rename to _nuxt/static/1667922389/categories/3/state.js index c235e48..5adcbd8 100644 --- a/_nuxt/static/1667843362/categories/3/state.js +++ b/_nuxt/static/1667922389/categories/3/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fcategories\u002F3",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fcategories\u002F3",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/categories/4/payload.js b/_nuxt/static/1667922389/categories/4/payload.js similarity index 100% rename from _nuxt/static/1667843362/categories/4/payload.js rename to _nuxt/static/1667922389/categories/4/payload.js diff --git a/_nuxt/static/1667843362/categories/4/state.js b/_nuxt/static/1667922389/categories/4/state.js similarity index 75% rename from _nuxt/static/1667843362/categories/4/state.js rename to _nuxt/static/1667922389/categories/4/state.js index e928cfd..b44c62b 100644 --- a/_nuxt/static/1667843362/categories/4/state.js +++ b/_nuxt/static/1667922389/categories/4/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fcategories\u002F4",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fcategories\u002F4",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/categories/5/payload.js b/_nuxt/static/1667922389/categories/5/payload.js similarity index 100% rename from _nuxt/static/1667843362/categories/5/payload.js rename to _nuxt/static/1667922389/categories/5/payload.js diff --git a/_nuxt/static/1667843362/categories/5/state.js b/_nuxt/static/1667922389/categories/5/state.js similarity index 75% rename from _nuxt/static/1667843362/categories/5/state.js rename to _nuxt/static/1667922389/categories/5/state.js index 2823820..979be34 100644 --- a/_nuxt/static/1667843362/categories/5/state.js +++ b/_nuxt/static/1667922389/categories/5/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fcategories\u002F5",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fcategories\u002F5",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/cv/payload.js b/_nuxt/static/1667922389/cv/payload.js similarity index 100% rename from _nuxt/static/1667843362/cv/payload.js rename to _nuxt/static/1667922389/cv/payload.js diff --git a/_nuxt/static/1667922389/manifest.js b/_nuxt/static/1667922389/manifest.js new file mode 100644 index 0000000..ca961c4 --- /dev/null +++ b/_nuxt/static/1667922389/manifest.js @@ -0,0 +1 @@ +__NUXT_JSONP__("manifest.js", {routes:["\u002F","\u002Fcv","\u002Fcategories\u002F2","\u002Fposts\u002F3","\u002Fcategories\u002F3","\u002Fcategories\u002F4","\u002Fcategories\u002F5","\u002Fposts\u002F4","\u002Fposts\u002F6","\u002Fposts\u002F8","\u002Fposts\u002F5","\u002Fposts\u002F9","\u002Fposts\u002F12","\u002Fposts\u002F11","\u002Fposts\u002F14","\u002Fposts\u002F13","\u002Fposts\u002F16","\u002Fposts\u002F15","\u002Fposts\u002F17","\u002Fposts\u002F21","\u002Fposts\u002F25","\u002Fposts\u002F26","\u002Fposts\u002F28","\u002Fposts\u002F30","\u002Fposts\u002F31","\u002Fposts\u002F34","\u002Fposts\u002F29","\u002Fposts\u002F20","\u002Fposts\u002F22","\u002Fposts\u002F27"]}) \ No newline at end of file diff --git a/_nuxt/static/1667843362/payload.js b/_nuxt/static/1667922389/payload.js similarity index 100% rename from _nuxt/static/1667843362/payload.js rename to _nuxt/static/1667922389/payload.js diff --git a/_nuxt/static/1667843362/posts/11/payload.js b/_nuxt/static/1667922389/posts/11/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/11/payload.js rename to _nuxt/static/1667922389/posts/11/payload.js diff --git a/_nuxt/static/1667843362/posts/11/state.js b/_nuxt/static/1667922389/posts/11/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/11/state.js rename to _nuxt/static/1667922389/posts/11/state.js index 41f5fd2..e69cc22 100644 --- a/_nuxt/static/1667843362/posts/11/state.js +++ b/_nuxt/static/1667922389/posts/11/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F11",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F11",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/12/payload.js b/_nuxt/static/1667922389/posts/12/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/12/payload.js rename to _nuxt/static/1667922389/posts/12/payload.js diff --git a/_nuxt/static/1667843362/posts/12/state.js b/_nuxt/static/1667922389/posts/12/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/12/state.js rename to _nuxt/static/1667922389/posts/12/state.js index 4c35e62..9b7da4e 100644 --- a/_nuxt/static/1667843362/posts/12/state.js +++ b/_nuxt/static/1667922389/posts/12/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F12",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F12",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/13/payload.js b/_nuxt/static/1667922389/posts/13/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/13/payload.js rename to _nuxt/static/1667922389/posts/13/payload.js diff --git a/_nuxt/static/1667843362/posts/13/state.js b/_nuxt/static/1667922389/posts/13/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/13/state.js rename to _nuxt/static/1667922389/posts/13/state.js index 92f5f79..f7476b1 100644 --- a/_nuxt/static/1667843362/posts/13/state.js +++ b/_nuxt/static/1667922389/posts/13/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F13",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F13",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/14/payload.js b/_nuxt/static/1667922389/posts/14/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/14/payload.js rename to _nuxt/static/1667922389/posts/14/payload.js diff --git a/_nuxt/static/1667843362/posts/14/state.js b/_nuxt/static/1667922389/posts/14/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/14/state.js rename to _nuxt/static/1667922389/posts/14/state.js index b307dc8..82af017 100644 --- a/_nuxt/static/1667843362/posts/14/state.js +++ b/_nuxt/static/1667922389/posts/14/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F14",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F14",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/15/payload.js b/_nuxt/static/1667922389/posts/15/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/15/payload.js rename to _nuxt/static/1667922389/posts/15/payload.js diff --git a/_nuxt/static/1667843362/posts/15/state.js b/_nuxt/static/1667922389/posts/15/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/15/state.js rename to _nuxt/static/1667922389/posts/15/state.js index 251ff06..9feb135 100644 --- a/_nuxt/static/1667843362/posts/15/state.js +++ b/_nuxt/static/1667922389/posts/15/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F15",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F15",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/16/payload.js b/_nuxt/static/1667922389/posts/16/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/16/payload.js rename to _nuxt/static/1667922389/posts/16/payload.js diff --git a/_nuxt/static/1667843362/posts/16/state.js b/_nuxt/static/1667922389/posts/16/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/16/state.js rename to _nuxt/static/1667922389/posts/16/state.js index 93a9ea3..aeb2d6b 100644 --- a/_nuxt/static/1667843362/posts/16/state.js +++ b/_nuxt/static/1667922389/posts/16/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F16",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F16",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/17/payload.js b/_nuxt/static/1667922389/posts/17/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/17/payload.js rename to _nuxt/static/1667922389/posts/17/payload.js diff --git a/_nuxt/static/1667843362/posts/17/state.js b/_nuxt/static/1667922389/posts/17/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/17/state.js rename to _nuxt/static/1667922389/posts/17/state.js index be82ca1..bc21981 100644 --- a/_nuxt/static/1667843362/posts/17/state.js +++ b/_nuxt/static/1667922389/posts/17/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F17",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F17",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/20/payload.js b/_nuxt/static/1667922389/posts/20/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/20/payload.js rename to _nuxt/static/1667922389/posts/20/payload.js diff --git a/_nuxt/static/1667843362/posts/20/state.js b/_nuxt/static/1667922389/posts/20/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/20/state.js rename to _nuxt/static/1667922389/posts/20/state.js index 3f83a98..18e13fa 100644 --- a/_nuxt/static/1667843362/posts/20/state.js +++ b/_nuxt/static/1667922389/posts/20/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F20",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F20",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/21/payload.js b/_nuxt/static/1667922389/posts/21/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/21/payload.js rename to _nuxt/static/1667922389/posts/21/payload.js diff --git a/_nuxt/static/1667843362/posts/21/state.js b/_nuxt/static/1667922389/posts/21/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/21/state.js rename to _nuxt/static/1667922389/posts/21/state.js index db2e61c..6ef9606 100644 --- a/_nuxt/static/1667843362/posts/21/state.js +++ b/_nuxt/static/1667922389/posts/21/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F21",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F21",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/22/payload.js b/_nuxt/static/1667922389/posts/22/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/22/payload.js rename to _nuxt/static/1667922389/posts/22/payload.js diff --git a/_nuxt/static/1667843362/posts/22/state.js b/_nuxt/static/1667922389/posts/22/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/22/state.js rename to _nuxt/static/1667922389/posts/22/state.js index 16c03b8..d123acd 100644 --- a/_nuxt/static/1667843362/posts/22/state.js +++ b/_nuxt/static/1667922389/posts/22/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F22",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F22",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/25/payload.js b/_nuxt/static/1667922389/posts/25/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/25/payload.js rename to _nuxt/static/1667922389/posts/25/payload.js diff --git a/_nuxt/static/1667843362/posts/25/state.js b/_nuxt/static/1667922389/posts/25/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/25/state.js rename to _nuxt/static/1667922389/posts/25/state.js index b6526c4..e7fdf09 100644 --- a/_nuxt/static/1667843362/posts/25/state.js +++ b/_nuxt/static/1667922389/posts/25/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F25",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F25",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/26/payload.js b/_nuxt/static/1667922389/posts/26/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/26/payload.js rename to _nuxt/static/1667922389/posts/26/payload.js diff --git a/_nuxt/static/1667843362/posts/26/state.js b/_nuxt/static/1667922389/posts/26/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/26/state.js rename to _nuxt/static/1667922389/posts/26/state.js index bcc5cab..a606ce5 100644 --- a/_nuxt/static/1667843362/posts/26/state.js +++ b/_nuxt/static/1667922389/posts/26/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F26",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F26",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/27/payload.js b/_nuxt/static/1667922389/posts/27/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/27/payload.js rename to _nuxt/static/1667922389/posts/27/payload.js diff --git a/_nuxt/static/1667843362/posts/27/state.js b/_nuxt/static/1667922389/posts/27/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/27/state.js rename to _nuxt/static/1667922389/posts/27/state.js index e3171af..4f0eaea 100644 --- a/_nuxt/static/1667843362/posts/27/state.js +++ b/_nuxt/static/1667922389/posts/27/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F27",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F27",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/28/payload.js b/_nuxt/static/1667922389/posts/28/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/28/payload.js rename to _nuxt/static/1667922389/posts/28/payload.js diff --git a/_nuxt/static/1667843362/posts/28/state.js b/_nuxt/static/1667922389/posts/28/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/28/state.js rename to _nuxt/static/1667922389/posts/28/state.js index 6d90704..87ae4c9 100644 --- a/_nuxt/static/1667843362/posts/28/state.js +++ b/_nuxt/static/1667922389/posts/28/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F28",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F28",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/29/payload.js b/_nuxt/static/1667922389/posts/29/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/29/payload.js rename to _nuxt/static/1667922389/posts/29/payload.js diff --git a/_nuxt/static/1667843362/posts/29/state.js b/_nuxt/static/1667922389/posts/29/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/29/state.js rename to _nuxt/static/1667922389/posts/29/state.js index c936501..1edf6b7 100644 --- a/_nuxt/static/1667843362/posts/29/state.js +++ b/_nuxt/static/1667922389/posts/29/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F29",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F29",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/3/payload.js b/_nuxt/static/1667922389/posts/3/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/3/payload.js rename to _nuxt/static/1667922389/posts/3/payload.js diff --git a/_nuxt/static/1667843362/posts/3/state.js b/_nuxt/static/1667922389/posts/3/state.js similarity index 74% rename from _nuxt/static/1667843362/posts/3/state.js rename to _nuxt/static/1667922389/posts/3/state.js index 8581f4e..529be6f 100644 --- a/_nuxt/static/1667843362/posts/3/state.js +++ b/_nuxt/static/1667922389/posts/3/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F3",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F3",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/30/payload.js b/_nuxt/static/1667922389/posts/30/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/30/payload.js rename to _nuxt/static/1667922389/posts/30/payload.js diff --git a/_nuxt/static/1667843362/posts/30/state.js b/_nuxt/static/1667922389/posts/30/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/30/state.js rename to _nuxt/static/1667922389/posts/30/state.js index e0031a4..5b47cec 100644 --- a/_nuxt/static/1667843362/posts/30/state.js +++ b/_nuxt/static/1667922389/posts/30/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F30",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F30",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/31/payload.js b/_nuxt/static/1667922389/posts/31/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/31/payload.js rename to _nuxt/static/1667922389/posts/31/payload.js diff --git a/_nuxt/static/1667843362/posts/31/state.js b/_nuxt/static/1667922389/posts/31/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/31/state.js rename to _nuxt/static/1667922389/posts/31/state.js index 1631a05..4bb4048 100644 --- a/_nuxt/static/1667843362/posts/31/state.js +++ b/_nuxt/static/1667922389/posts/31/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F31",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F31",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667922389/posts/34/payload.js b/_nuxt/static/1667922389/posts/34/payload.js new file mode 100644 index 0000000..71eb3e7 --- /dev/null +++ b/_nuxt/static/1667922389/posts/34/payload.js @@ -0,0 +1 @@ +__NUXT_JSONP__("/posts/34", {data:[{}],fetch:{"0":{zoom:null,pageCount:true,title:"如何使用 5000 块组装一台顶配 Mac Studio",author:"Yidadaa",date:"2022\u002F11\u002F07",content:"\u003Cblockquote\u003E\n\u003Cp\u003E写给程序员的小尺寸高性能黑苹果主机装配指南,全文约 10000 字。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E本文同步发表至:\u003Ca href=\"https:\u002F\u002Fflowus.cn\u002Fyifei\u002Fshare\u002Fcb8f8b2f-591f-4a34-a901-b714a4c81bcc\"\u003EFlowUs\u003C\u002Fa\u003E \u002F \u003Ca href=\"https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F580506404\"\u003E知乎\u003C\u002Fa\u003E \u002F \u003Ca href=\"https:\u002F\u002Fgithub.com\u002FYidadaa\u002FYidadaa.github.io\u002Fissues\u002F34\"\u003EGithub\u003C\u002Fa\u003E \u002F \u003Ca href=\"https:\u002F\u002Fblog.simplenaive.cn\u002F34\"\u003E博客\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E请使用良好的网络环境访问此文,否则图片可能无法加载。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374112-bac58562-f71f-4f32-b674-b012a275d56a.png\" alt=\"image\"\u003E\n左:Mac Studio,右:穷逼版\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374176-9667385a-dc3d-414e-86b6-f6c88cb4afc7.png\" alt=\"image\"\u003E\n与 24 寸显示器的对比\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374261-61910710-fe87-40c8-91a8-c59bcd20b5c0.png\" alt=\"image\"\u003E\n与 330ml 杯子的对比\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374347-dadcac5b-fde2-4b7f-a74c-c0e9da1b257b.png\" alt=\"image\"\u003E\n日常使用负载\u003C\u002Fp\u003E\n\u003Ch2\u003E长话短说\u003C\u002Fh2\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E性能对比\u003C\u002Fth\u003E\n\u003Cth\u003ECPU\u003C\u002Fth\u003E\n\u003Cth\u003E内存\u003C\u002Fth\u003E\n\u003Cth\u003E硬盘\u003C\u002Fth\u003E\n\u003Cth\u003E性能(R23)\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E体积\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EMac Studio\u003C\u002Ftd\u003E\n\u003Ctd\u003EM1 Ultra\u003C\u002Ftd\u003E\n\u003Ctd\u003E64GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E1TB 固态\u003C\u002Ftd\u003E\n\u003Ctd\u003E多核 23705\u003C\u002Ftd\u003E\n\u003Ctd\u003E29999\u003C\u002Ftd\u003E\n\u003Ctd\u003E197mm * 197mm * 95mm = 3.68L\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E穷逼版\u003C\u002Ftd\u003E\n\u003Ctd\u003Ei7 12700\u003C\u002Ftd\u003E\n\u003Ctd\u003E64GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E1TB 固态\u003C\u002Ftd\u003E\n\u003Ctd\u003E多核 21568\u003C\u002Ftd\u003E\n\u003Ctd\u003E4946\u003C\u002Ftd\u003E\n\u003Ctd\u003E196mm * 218mm * 117mm = 4.9L\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Cul\u003E\n\u003Cli\u003E本文只面向程序员群体,优先保证编译性能和开发舒适度,不面向视频剪辑等创意工作者,不考虑视频剪辑性能;\u003C\u002Fli\u003E\n\u003Cli\u003E重点考虑 CPU 单核和多核性能,优先内存和硬盘容量,考虑机箱体积,不太考虑内存频率,不追求硬盘速度,不太考虑显卡性能,不太考虑噪音,完全不考虑外观精致程度;\u003C\u002Fli\u003E\n\u003Cli\u003E优点:便宜,量大管饱,装机完成后,可当做白苹果使用,无痛升级后续系统,可支持 MacOS、Windows 和 Linux 三系统共存;\u003C\u002Fli\u003E\n\u003Cli\u003E缺点:折腾,组装机箱和安装系统需要有一定动手能力,不适用于日薪大于 1w 的用户,不适用于无计算机硬件常识用户;\u003C\u002Fli\u003E\n\u003Cli\u003E硬件到位之后,在网络良好情况下,半天即可完成装机;\u003C\u002Fli\u003E\n\u003Cli\u003E多核编译同一个项目,M1 Max 需 6min,本文主机需 4min,多核性能对比与 R23 跑分对比大致保持一致。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch2\u003E阅读须知\u003C\u002Fh2\u003E\n\u003Cul\u003E\n\u003Cli\u003E适用群体:\n\u003Cul\u003E\n\u003Cli\u003E写代码的\u003C\u002Fli\u003E\n\u003Cli\u003E买不起 Mac Studio 的\u003C\u002Fli\u003E\n\u003Cli\u003E有一定的折腾能力的\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E不适用群体:\n\u003Cul\u003E\n\u003Cli\u003E打游戏的\u003C\u002Fli\u003E\n\u003Cli\u003E富哥富姐请直接使用钞能力\u003C\u002Fli\u003E\n\u003Cli\u003E剪视频的\u003C\u002Fli\u003E\n\u003Cli\u003E懒得折腾的(可以购置硬件后,购买淘宝黑苹果装机服务)\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E本文包含以下内容:\n\u003Cul\u003E\n\u003Cli\u003E提供了购买硬件和装机过程中的一些需要考虑的事项\u003C\u002Fli\u003E\n\u003Cli\u003E提供了分别对标 M1 Mac mini、M1 max Mac studio 和 M1 ultra Mac studio 性能(不含 GPU 和磁盘性能)且同等体积的装机购置清单\u003C\u002Fli\u003E\n\u003Cli\u003E提供了安装黑苹果所需的硬件考虑事项以及系统安装须知\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch2\u003E为什么需要装这样一台机器\u003C\u002Fh2\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E可以跳过此章节。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cp\u003E总而言之,对于我来说,主要有两个原因:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E\n\u003Cp\u003E主要原因:公司电脑性能拉跨,满足不了开发需要;\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003E次要原因:目前在用的 MacOS 13 Ventura 系统拉跨,降级麻烦。\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E所以,我需要搞一台高性能主机来替换掉公司发的老旧 MBP。\u003C\u002Fp\u003E\n\u003Cp\u003E其实公司配的 2020 款 MBP 并不算老,就是性能有点差,10 代标压 i5-1038NG7 处理器加 16GB 板载内存,多核性能大概是 M1 的一半,省着点还是可以用的。\u003C\u002Fp\u003E\n\u003Cp\u003E但我在 MacOS 13 技术预览版刚放出来时候,就迫不及待地手贱升级了一波,然后成功被新系统的各种卡顿 Bug 和内存占用虚高问题治好了低血压。即便到如今已经发布了正式版,这些问题仍旧存在,以至于日常开发时随便开个 VS Code 再加几个网页,内存就基本见底了。\u003C\u002Fp\u003E\n\u003Cp\u003E后来为了能保证正常开发,我不得不花一周时间把 Vim 打造成主力 IDE,以便直接在开发机上写代码。\u003C\u002Fp\u003E\n\u003Cp\u003E此外,除了 MacOS 拉跨,公司的项目本身也对设备性能要求很高。\u003C\u002Fp\u003E\n\u003Cp\u003E身后的同事老哥前段时间斥巨资入手了一款中配 M1 Max 款 Mac Studio,原因就是公司发放的设备很难满足开发需求。我们日常的技术栈是 C++ 和 React,前者在我那台十代标压 i5 上,全量编译整个项目需要花费 20 分钟到 30 分钟的时间,编译个几次项目,半天就没了,倒是非常适合划水。虽然大多数时间并不会全量编译,但每次增量编译耗时也在分钟级别,非常可观,十分难受。\u003C\u002Fp\u003E\n\u003Cp\u003E后者 React 更是重量级,NodeJS + HMR + 浏览器堪称内存黑洞,再配合 C++ 建立的 Clang 符号表索引,16GB 板载内存直接被榨干,一天下来大多数时间都在等编译和页面响应。\u003C\u002Fp\u003E\n\u003Cp\u003E当然这也并不是完全没有好处,由于等待的成本过于高昂,我们写下每行代码前都要深思熟虑,每次编译前都要检查个半天,所以那段时间的代码质量总感觉也高了不少 😆。\u003C\u002Fp\u003E\n\u003Cp\u003E后来我们安排了一台 36 核 72 线程(含两颗 18C36T Intel Xeon Gold 6240 CPU)的开发机,第一次用它编译项目的时候,有种便秘两周之后突然窜稀的畅快感觉,以前 20 多分钟的编译,现在两分钟就完事了,比以前跑单测都快。\u003C\u002Fp\u003E\n\u003Cp\u003E然而即便用了开发机和 Vim 之后,我还是经常被系统卡顿和内存不足烦得要死,随便开几个网页,该卡还是卡,就这样将就了两个多月,在某次系统彻底无响应之后,我一咬牙一狠心,决定加亿点钱解决这个问题。\u003C\u002Fp\u003E\n\u003Cp\u003E于是我便问了下身后老哥的 M1 Max 的编译时间,居然只要 6 分钟,已经完全够用了,怪不得他不怎么用开发机,我马上点开了苹果官网:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200374857-d6067e58-1765-4da2-8929-a4b58b24b145.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E然后看到这个价格我火速关闭,那没事了,告辞。\u003C\u002Fp\u003E\n\u003Cp\u003E看来富哥的路是走不通了,只能使用穷逼的道路曲线救国了。\u003C\u002Fp\u003E\n\u003Ch2\u003E性能\u003C\u002Fh2\u003E\n\u003Cp\u003E苹果在发布会上把 M1 系列芯片吹得天花乱坠,拳打 Intel i9-11900,脚踢 RTX 3090,让我们来看看它的真面目:\u003C\u002Fp\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E芯片名称\u003C\u002Fth\u003E\n\u003Cth\u003E规格\u003C\u002Fth\u003E\n\u003Cth\u003ER23 单核跑分\u003C\u002Fth\u003E\n\u003Cth\u003ER23 多核跑分\u003C\u002Fth\u003E\n\u003Cth\u003E功耗\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIntel Core i5 1038NG7\u003C\u002Ftd\u003E\n\u003Ctd\u003E4 核 8 线程\u003C\u002Ftd\u003E\n\u003Ctd\u003E1116\u003C\u002Ftd\u003E\n\u003Ctd\u003E4979\u003C\u002Ftd\u003E\n\u003Ctd\u003E28W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EApple M1\u003C\u002Ftd\u003E\n\u003Ctd\u003E8 核\u003C\u002Ftd\u003E\n\u003Ctd\u003E1510\u003C\u002Ftd\u003E\n\u003Ctd\u003E7687\u003C\u002Ftd\u003E\n\u003Ctd\u003E6.8W ~ 39W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIntel Core i5 12400\u003C\u002Ftd\u003E\n\u003Ctd\u003E6 核 12 线程\u003C\u002Ftd\u003E\n\u003Ctd\u003E1697\u003C\u002Ftd\u003E\n\u003Ctd\u003E11905\u003C\u002Ftd\u003E\n\u003Ctd\u003E65W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EApple M1 Max\u003C\u002Ftd\u003E\n\u003Ctd\u003E10 核\u003C\u002Ftd\u003E\n\u003Ctd\u003E1533\u003C\u002Ftd\u003E\n\u003Ctd\u003E12337\u003C\u002Ftd\u003E\n\u003Ctd\u003E11W ~ 115W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIntel Core i7 12700\u003C\u002Ftd\u003E\n\u003Ctd\u003E12 核 20 线程\u003C\u002Ftd\u003E\n\u003Ctd\u003E1879\u003C\u002Ftd\u003E\n\u003Ctd\u003E21568\u003C\u002Ftd\u003E\n\u003Ctd\u003EPL1: 65W, PL2 180W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EApple M1 Ultra\u003C\u002Ftd\u003E\n\u003Ctd\u003E20 核\u003C\u002Ftd\u003E\n\u003Ctd\u003E1506\u003C\u002Ftd\u003E\n\u003Ctd\u003E23705\u003C\u002Ftd\u003E\n\u003Ctd\u003E13W ~ 215W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003EIntel Core i9 12900\u003C\u002Ftd\u003E\n\u003Ctd\u003E16 核 24 线程\u003C\u002Ftd\u003E\n\u003Ctd\u003E1988\u003C\u002Ftd\u003E\n\u003Ctd\u003E26455\u003C\u002Ftd\u003E\n\u003Ctd\u003EPL1: 65W, PL2 200W\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Cp\u003E确实很强,比 10 代 intel 强多了,但是面对 12 代 intel 还是不太行,毕竟功耗在这摆着,i9 12900 勉强守住了桌面 CPU 的底裤。\u003C\u002Fp\u003E\n\u003Cp\u003E苹果 M1 系列的能耗比确实惊人,但是我要放在公司使用,完全不用担心电费问题,功耗都是图一乐,于是经过一周的调研,确定了硬件配置单。\u003C\u002Fp\u003E\n\u003Ch2\u003E硬件配置单\u003C\u002Fh2\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E下列配置中的硬件,除显卡和网卡外,均为全新价格。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E如需查看更多配置单(M1 \u002F M1 Max 级别),请跳转到文章末尾。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E部件\u003C\u002Fth\u003E\n\u003Cth\u003E产品\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E备注\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECPU\u003C\u002Ftd\u003E\n\u003Ctd\u003EIntel i7 12700f 散片\u003C\u002Ftd\u003E\n\u003Ctd\u003E2110\u003C\u002Ftd\u003E\n\u003Ctd\u003Ef 后缀代表不集成核显,比带核显版本便宜 100 块左右\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E主板\u003C\u002Ftd\u003E\n\u003Ctd\u003E铭瑄 H610itx 挑战者\u003C\u002Ftd\u003E\n\u003Ctd\u003E569\u003C\u002Ftd\u003E\n\u003Ctd\u003E最便宜的能带得动 i7 12700 的 itx 主板\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E内存条\u003C\u002Ftd\u003E\n\u003Ctd\u003E酷兽 32GB 3000Mhz 两根共 64GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E798\u003C\u002Ftd\u003E\n\u003Ctd\u003E最便宜的 DDR4 内存,有 2666mhz \u002F 3000mhz \u002F 3200Mhz 三个版本,价格一样,哪个便宜有货买哪个\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E硬盘\u003C\u002Ftd\u003E\n\u003Ctd\u003E光威弈 Pro 512GB nvme ssd\u003C\u002Ftd\u003E\n\u003Ctd\u003E295\u003C\u002Ftd\u003E\n\u003Ctd\u003E随便选的,可以加一百多块上 1TB 固态,反正便宜\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E散热器\u003C\u002Ftd\u003E\n\u003Ctd\u003E利民 Axp90 x53 + LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003Ctd\u003E159\u003C\u002Ftd\u003E\n\u003Ctd\u003E选购时需要考虑机箱适配的散热器高度,需要加 18 元额外购买利民 LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E机箱\u003C\u002Ftd\u003E\n\u003Ctd\u003E傻瓜超人 K39 4.9L + 显卡延长线\u003C\u002Ftd\u003E\n\u003Ctd\u003E199\u003C\u002Ftd\u003E\n\u003Ctd\u003E196mm * 117mm * 218mm = 4.9L 体积,当前配置的体积最小最便宜的选择\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E益衡 7030B 300W Flex 1U 电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E398\u003C\u002Ftd\u003E\n\u003Ctd\u003E这个价格是加 100 块让店家换全模组电源线和静音风扇之后的版本,组装的时候理线会更方便\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E显卡\u003C\u002Ftd\u003E\n\u003Ctd\u003EAMD RX550 2GB\u002F4GB Polaris 核心\u003C\u002Ftd\u003E\n\u003Ctd\u003E300\u003C\u002Ftd\u003E\n\u003Ctd\u003E二手亮机卡,有 Lexa 和 Polaris 核心两个版本,建议买 4GB 显存 Polaris 核心版本,各版本价格无区别\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E网卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E博通 BCM94360CS2 + m2 ngff 正向转接卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E118\u003C\u002Ftd\u003E\n\u003Ctd\u003E也可以选择 Intel AX201 或者更低端的 Intel 9560,能省个几十块钱,但需要额外配置驱动,没必要\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Cp\u003E我们一项一项来看,为什么要选用这些配置。\u003C\u002Fp\u003E\n\u003Ch3\u003ECPU 和主板\u003C\u002Fh3\u003E\n\u003Cp\u003E首先,要再次明确一下目标,我们想要 M1 Ultra 级别的性能,那就只能在 i7 12700 及以上的 CPU 中选,大概有这么几个选项:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E\n\u003Cp\u003EIntel i7 12700f 和 12700\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003EIntel i7 12700kf 和 12700k\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003EIntel i9 12900f\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003EIntel i9 12900kf 和 12900k\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E**为什么不选 AMD 的 CPU?**因为性价比不高,12 代 CPU 同价位性能比 AMD 5000 系列同价位性能稍强,价格稍便宜,而且目前 AMD 装黑苹果需要额外配置,有点麻烦,所以完全没有理由选 AMD。\u003C\u002Fp\u003E\n\u003Cp\u003E**为什么不选 13 代 Intel CPU?**因为性价比不高,目前只发布了带 K 后缀的高性能版本,需要搭配更好的主板和电源,而且 13 代 CPU 也相对更贵。\u003C\u002Fp\u003E\n\u003Cp\u003E然后,i7 和 i9 分别都有 f \u002F k \u002F kf 几个版本:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E\n\u003Cp\u003E其中凡是带 f 的,都是不带集成显卡的版本,考虑到我们是装黑苹果,12 代集成显卡不能被驱动,所以必须得配个独立显卡,选择 f 版本就行,可以省下 100 块;\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E\n\u003Cp\u003E其中凡是带 k 的,都代表着性能增强版,大概比不带 k 的版本性能强 10% - 15%,价格也要贵 5% - 10%,酌情购买。\u003C\u002Fp\u003E\n\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E由于 i9 处理器需要主板有更强的供电能力,主板的价格就水涨船高,所以我们退而求其次,选择 i7 12700,性能只比 M1 Ultra 弱 10%。当然,也可以加点钱上 12700k,这样性能和 M1 Ultra 持平,不过这点差距在实际使用时基本没什么区别,12700k 对主板供电能力也有一定的要求,丐板估计带不动。\u003C\u002Fp\u003E\n\u003Cp\u003E主板就不提了,铭瑄 H610itx 在 600 块价位基本无敌,找不到其他能一起打的。唯一缺点就是 itx 主板接口有点少,只能插两根内存条,目前消费级内存条单条最大也就 32GB,所以最多只能上 64GB 内存。\u003C\u002Fp\u003E\n\u003Cp\u003E而且这种低端丐板没有额外的 M.2 接口,只能插一条 Nvme 固态,要扩容的话,要么换固态,要么加 SATA 硬盘。不过还好 512GB SSD 够用了,即便是 1TB 的 SSD 也只要 400 出头,可以自己决定要不要加钱。\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E购买散片板 U 套装即可\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch3\u003E内存条和硬盘\u003C\u002Fh3\u003E\n\u003Cp\u003E酷兽和光威是同一个厂,不同的产品线,酷兽主打性价比,399 块的 32GB 内存条要啥自行车,直接来两条插满。\u003C\u002Fp\u003E\n\u003Cp\u003E此外这个内存条有 2666mhz \u002F 3000mhz \u002F 3200mhz 三个频率版本,理论上铭瑄 h610itx 主板是支持 3200mhz 高频内存的,只需要在 BIOS 里设置一下就行,但是说实话内存频率感知不强,只有核显打游戏才对内存频率有要求,如果用来开发,随便买就行,高频内存属实浪费预算,哪个有货买哪个。\u003C\u002Fp\u003E\n\u003Cp\u003E最近固态硬盘也是白菜价,1tb 固态已经降到了 400 以下,当然也有更贵的性能更好的版本,但是对于开发来说感知不强,3000mb\u002Fs 和 7000mb\u002Fs 虽然看起来差距挺大,但是实际用的时候也就几百毫秒的差距,感知不出来,所以随便买个便宜的就行,重点还是看容量。\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E哪个便宜买哪个,频率和读写速度并不重要;\u003C\u002Fli\u003E\n\u003Cli\u003EMac Studio 的统一内存,官方读速是 800GB\u002Fs,与 3200mhz DDR4 内存条读速差不多;\u003C\u002Fli\u003E\n\u003Cli\u003E如果磁盘性能也想对标一下 Mac Studio,Mac Studio 硬盘速度为:读 5000MB\u002Fs,写 4000MB\u002Fs,大致与 PCIE 4.0 固态速度接近,所以可以酌情购买 PCIE 4.0 固态硬盘(铭瑄的这块主板支持,精粤的不支持),同体积大概是 PCIE 3.0 硬盘的 1.5 倍价格,512GB 版本大概 400 块可以搞定。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch3\u003E散热器、机箱和电源\u003C\u002Fh3\u003E\n\u003Cp\u003E这三兄弟要一起看,其实显卡也应该拿过来一起看,但是开发场景,显卡不是很重要,所以显卡另说吧。\u003C\u002Fp\u003E\n\u003Cp\u003E之所以要放一起看,主要是尺寸问题,我想要体积尽可能接近 Mac Studio,Mac Studio 的体积是 197mm * 197mm * 9.5mm = 3.68L,一开始我买的是下面这个机箱:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375317-17a17f5d-b8ae-424d-aab5-3cf3054e979e.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E整体尺寸是 187mm * 230mm * 97mm = 4.2L,非常小巧。\u003C\u002Fp\u003E\n\u003Cp\u003E我选择了其中的 A 背板版本,没有显卡位,把空间全部让给散热器和电源,散热器也是买的 Axp90 x53(90mm 宽,53mm 高)。\u003C\u002Fp\u003E\n\u003Cp\u003E因为我一开始并没有打算装黑苹果,想直接用 Linux 来开发,但是后来发现 Linux 没办法入公司内网,而且企业微信也是残废状态,所以才考虑装黑苹果,于是换成了下面的机箱:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375363-2379fe91-e33e-45f3-816a-1c58aad6c11a.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E体积大了一圈,来到了 196mm * 117mm * 218mm = 4.9L,不过好在便宜,加显卡延长线才 200 块,比上一个 300 块的机箱便宜很多,而且可以加装一个 19cm 以内的双槽短卡,非常适合本文的需求。\u003C\u002Fp\u003E\n\u003Cp\u003E从上面机箱的商品介绍就可以知道,这个机箱支持 56mm 以内的散热器,一般来讲,散热器的尺寸与散热能力正相关,基本上体积越大,散热能力越好,在看了一圈视频之后,出现次数的就是利民的axp90 系列的几个散热器,价格在 100 元到 150 元之间:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375397-361c99d5-7cf0-489b-837e-f4fb5244390f.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E可以看到主要有 36mm \u002F 47mm \u002F 53mm 三种高度,名字 AXP90 则代表 90mm * 90mm 的尺寸,为了最大化机箱利用率,我选用了 53mm 高度版本,实测整机满载温度可以控制在 80 摄氏度以内,日常使用则只有 27 摄氏度左右,好悬没给 CPU 给冻感冒。\u003C\u002Fp\u003E\n\u003Cp\u003E此外,如果你想选用一些更小尺寸的机箱,在选散热器的时候一定要注意,最好预留 3mm 以上的空间余量,比如如果限高 56mm 的机箱,如果配了一个 55mm 高度的散热器,风扇叶片和机箱离得太近,会出现十分明显的啸叫。\u003C\u002Fp\u003E\n\u003Cp\u003E然后是电源,前面了解到 i7 12700 的满载功耗是 180w,再加上 50w 亮机显卡的功耗,总计 230w 的实际载荷,按照 80% 的电源转化率,我们只需要选择 290w 以上的电源即可满足需求。在尺寸方面,由于我们想让机箱的体积尽可能的小,而小尺寸 itx 机箱往往搭配使用小 1u 尺寸的 Flex 电源,这类电源的价格要比常规尺寸的电源稍贵一点,如果想要省钱,可以选购支持 SFX 电源的更大尺寸的机箱。\u003C\u002Fp\u003E\n\u003Cp\u003E最终我选择了益衡的 7030B 300w 电源:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375452-b24da35f-415b-48cc-8953-af0ebe584500.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375491-e805645b-fc76-4d2c-957f-03bb3a463178.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E小 1u 电源还有个缺点是会比较吵,因为受体积限制,风扇必须提高转速才能保证散热,运行时会有比较明显的高频噪声,所以我选择加了 100 块,买了店家改装之后的版本(上图右侧)。\u003C\u002Fp\u003E\n\u003Cp\u003E改装版本换了更静音的风扇,并且配备了全模组电源线,大概长下面这个样子:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375578-2df51c37-0c56-486c-ba22-d36fd79793b7.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E电源线统一成了黑色的软线,比原装的五颜六色的硬线好看多了,而且装机的时候理线会非常简单,这一点在小机箱里尤其关键,所以这 100 块还是挺值的。\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E购买全模组小 1u 电源,考虑购买全模组 + 静音风扇的改装版本\u003C\u002Fli\u003E\n\u003Cli\u003E购买散热器时注意机箱散热器限高\u003C\u002Fli\u003E\n\u003Cli\u003E利民散热器默认附带的主板支架只能与上一代 Intel CPU 搭配使用,需要额外花费 18 元购买利民 LGA17xx 扣具\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch3\u003E显卡\u003C\u002Fh3\u003E\n\u003Cp\u003E显卡需要着重讲一下,因为 MacOS 非常挑显卡,目前市售的 RTX 系列显卡基本都是驱动不了的,只要一些比较老的 Nvidia 显卡才能驱动。而大多数 AMD 显卡都能驱动,比如最近出的 6600 和 6700,但是 6400 除外,详细的 GPU 列表可以\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FGPU-Buyers-Guide\u002Fmodern-gpus\u002Famd-gpu.html#native-amd-gpus\"\u003E查看这里\u003C\u002Fa\u003E。\u003C\u002Fp\u003E\n\u003Cp\u003E由于我们并不需要特别强劲的显卡性能,所以只需要选择能够正常驱动的低功耗亮机卡即可,我一开始就直接从比较老的 R7 和 R9 系列显卡开始看:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375686-da4a53e8-daf6-4927-87dd-b5586b5628cc.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E可以看到有非常多的选项,我在淘宝搜了一圈之后,发现最低端的 R7 240\u002F250 显卡只要 100 块,于是就直接下单买了一块,结果入手后折腾了许久才发现,这张 2013 年发售的老显卡,最高只能运行在 MacOS 10.x 系统上,最新的 12.x 系统是没法完美驱动的。我花了大量的时间搜索国内外关于这张 Oland 核心的显卡驱动帖子,没有发现任何人能成功在 11.x 及以上的系统上成功运行过它,虽然可以使用仿冒显卡 ID 的方法成功安装并进入系统,但是却无法使用 Metal GPU 加速,导致系统所有的高斯模糊效果以及动效都是缺失的,完全无法满足日常使用。\u003C\u002Fp\u003E\n\u003Cp\u003E于是我就又斥巨资花了 300 块买了一张 Rx550,这张显卡发售于 2017 年,网上有相当多的黑苹果视频表示这张卡可以完美工作在最新的 MacOS 13.0 系统上,而且完全不需要任何额外操作,插上就能用。当然,前提是买到了正确的版本的显卡,这张显卡有两种核心版本:Lexa 核心和 Buffin 核心(或称 Polaris 核心),其中 Buffin(Polaris) 核心是可以即插即用,但是 Lexa 核心则需要\u003Ca href=\"https:\u002F\u002Fwww.bilibili.com\u002Fread\u002Fcv15800495\"\u003E仿冒参数\u003C\u002Fa\u003E才能工作。\u003C\u002Fp\u003E\n\u003Cp\u003E很不幸我买的这张 Rx550 2GB 联想拆机卡是 Lexa 核心,不过由于有了之前折腾 R7 240 的经历,对我来说还算简单。\u003C\u002Fp\u003E\n\u003Cp\u003E此外这张显卡有 2GB 显存和 4GB 显存两个版本,价格基本一样,我个人建议购买 4GB 版本,因为我在实际使用时发现,MacOS 12.x 系统很容易就把 2GB 显存吃满了,所以能买大的还是买大点的。\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375752-81118659-e6fe-443e-bf82-bad353cf5940.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E盈通的这张 RX550 4GB 就很适合,虽然是二手,有翻车的风险,但是我们并不会用来打游戏,所以只要它能点亮就行,反正日常负载不会太高,暴毙的可能性比较小。\u003C\u002Fp\u003E\n\u003Cp\u003E如果实在担心翻车,可以加点钱买全新版本,这张卡有全新正品在售,价格在 500 块到 600 块之间:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375812-0c4657dc-fded-407c-836b-132ded517957.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E购买 RX550 4GB Polaris 核心版本\u003C\u002Fli\u003E\n\u003Cli\u003E省钱就买二手,图稳就加点钱买全新\u003C\u002Fli\u003E\n\u003Cli\u003E购买显卡时注意显卡尺寸是否兼容机箱\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch3\u003E无线网卡\u003C\u002Fh3\u003E\n\u003Cp\u003E无线网卡也值得花点篇幅来说一说,目前黑苹果无线网卡可选 Intel NGFF 接口的一系列网卡,以及搭配转接板使用的博通 BCM 系列苹果拆机显卡:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375892-727c87ae-e39e-48eb-8dfb-fe4495c209cf.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E如果从省事的角度来看,直接买博通的拆机网卡即可,插上就能用。如果想省点钱,就买 Intel 最低端的 3168 系列:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200375922-f435efa8-1651-462d-bfb6-8cddf0228072.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E其实在几年前,Intel 的无线网卡是没办法在苹果系统上使用的,后来有个大佬搓了一个驱动出来,然后 Intel 几乎所有型号的无线网卡一夜之间焕发第二春,具体的支持列表可以查看下面的链接:\u003Ca href=\"https:\u002F\u002Fopenintelwireless.github.io\u002Fitlwm\u002FCompat.html#dvm-iwn\"\u003ECompatibility | OpenIntelWireless\u003C\u002Fa\u003E。\u003C\u002Fp\u003E\n\u003Cp\u003E此外无线网卡的天线,有内置和外置两种区别:\u003C\u002Fp\u003E\n\u003Cimg width=\"202\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376150-0858b518-afca-46c7-80fa-5b5381c66631.png\"\u003E\n\u003Cimg width=\"202\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376191-3dcfe932-0251-4d42-a9d3-b4261c297aa7.png\"\u003E\n\u003Cp\u003E两者的区别:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E外置天线是机箱后面的小尾巴,看起来可能不太美观,但是信号比较稳定,增益较强;\u003C\u002Fli\u003E\n\u003Cli\u003E内置天线则需要粘在机箱上,容易受机箱内部杂波干扰,增益较小,信号不太稳定,但由于不需要伸出机箱,所以会比较美观一点。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E我选择了 Intel 9560 内置天线版本,花费 48 块,实装之后可以完美使用 WIFI 和蓝牙,隔空投送也可以正常使用,但是内置天线信号不稳定,蓝牙连接妙控板非常卡顿,基本无法日常使用。而且由于网卡规格较低,虽然标称 2.4G 300Mbps \u002F 5G 1733Mbps,但实测只能跑到 50Mbps,可能最大的原因还是这个内置天线的信号太差,干扰太多。\u003C\u002Fp\u003E\n\u003Cp\u003E不过问题不大,这个主机的 WIFI 纯粹是为了应付公司的入网认证,认证完了之后就直接关掉用有线连接了,如果你对无线网络有需求,建议购买高规格的外置天线版本。\u003C\u002Fp\u003E\n\u003Cp\u003E购买建议:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E购买博通 BCM 94360CS + 正向转接卡 + 外置天线版本,不用折腾,信号更好。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch2\u003E装机\u003C\u002Fh2\u003E\n\u003Cp\u003E十分推荐先观看此视频:[DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!](【DIY、攒机、ITX的魅力?熬过劝退,就会让你上瘾!】 https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1v54y1Z7Er\u002F?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9),视频中的配置与本文配置基本一致,可以参考。\u003C\u002Fp\u003E\n\u003Cimg width=\"548\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376409-19d4c0eb-2acc-42f7-a80b-036677c214cb.png\"\u003E\n\u003Cp\u003E先上所有硬件的全家福,图中不包含显卡和网卡,因为拍摄照片的时候显卡和网卡还没到。\u003C\u002Fp\u003E\n\u003Ch3\u003E安装 CPU\u003C\u002Fh3\u003E\n\u003Cimg width=\"274\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376495-af4db007-b6da-4a49-8a92-bb302c9d70ec.png\"\u003E\n\u003Cp\u003E主板说明书会提示如何安装 CPU,一定要小心不要弄坏了针脚,主板上的金属压杆会比较紧,确认 CPU 按照防呆口安装妥当后,稍加用力按下压杆,将 CPU 固定好即可。\u003C\u002Fp\u003E\n\u003Ch3\u003E上电测试\u003C\u002Fh3\u003E\n\u003Cp\u003E先别急着往机箱里塞,把内存条和硬盘插好之后,连接电源线和键鼠,用螺丝刀碰触主板上的开机引脚,显示器能够显示 BIOS 界面,则表示成功点亮。\u003C\u002Fp\u003E\n\u003Ch3\u003E安装散热器\u003C\u002Fh3\u003E\n\u003Cp\u003E按照散热器说明书进行安装即可,注意需要额外购买利民 LGA17xx 扣具。\u003C\u002Fp\u003E\n\u003Ch3\u003E安装机箱\u003C\u002Fh3\u003E\n\u003Cp\u003E按照机箱说明书安装即可,建议先安装电源,再安装主板,然后接电源线,安装显卡延长线,安装显卡。\u003C\u002Fp\u003E\n\u003Cimg width=\"609\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376596-5fc17184-3e23-40ff-921e-d07882522f97.png\"\u003E\n\u003Ch2\u003E成品展示\u003C\u002Fh2\u003E\n\u003Cimg width=\"543\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376705-5ab95e41-da17-42ec-bfbb-7ed44c9d5c17.png\"\u003E\n\u003Cimg width=\"538\" alt=\"image\" src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376804-8948e19c-6fdd-4c1d-be37-8a61d18684da.png\"\u003E\n\u003Cp\u003E最终的成品大概比 Mac Studio 大了一圈,当然精致程度是没法比的,如果想要精致,可以加钱买更贵的全铝合金一体成型机箱(5.6L 体积):\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200376871-9d4540ef-a61e-44cf-b8f7-576fc212d69e.png\" alt=\"image\"\u003E\u003C\u002Fp\u003E\n\u003Ch2\u003E系统\u003C\u002Fh2\u003E\n\u003Cp\u003E可以直接跳转到流程清单部分,如果你不需要安装 MacOS,可以无视下列内容。\u003C\u002Fp\u003E\n\u003Ch3\u003E为什么不用 Windows 和 Linux?\u003C\u002Fh3\u003E\n\u003Cp\u003E在最开始决定装机的时候,我就在考虑要使用什么操作系统作为主力开发环境,前文已经提到我需要频繁编译,所以编译性能是我首要考虑的问题,这样 Window 肯定就不能用了,Windows 下 C++ 编译性能一向令人诟病,即便有了 WSL,由于它运行在 Hyper-V 虚拟机中,导致 IO 性能跟不上,实际的性能大概是 Linux 原生的 80% 左右,直接损耗了 20% 的性能,要知道 Intel 的 CPU 一次代际升级的性能也才差不多 25%,Window 直接就让 CPU 性能倒退一代,这绝对是无法接受的。\u003C\u002Fp\u003E\n\u003Cp\u003E我随后的想法是使用 Linux 作为日常开发系统,并使用 KVM 安装 Windows 虚拟机来解决常用软件问题,我也确实尝试了这种做法,并依次使用了下列 Linux 发行版:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E使用 KDE 桌面的 Debian,由于 Stable 版本的 Linux 内核太老,导致无法在 12 代 Intel 平台上正常安装,尝试几次后均未果,作罢;\u003C\u002Fli\u003E\n\u003Cli\u003E使用 KDE 桌面的 Ubuntu,也就是 KUbuntu,这个发行版确实很不错,KDE 对高分屏的支持非常不错,而且窗口主题和动效都很细腻,我用了大概一周时间,在上面使用 KVM 分别安装了 Windows 10 LTSC 以及 Mac OS 12 的虚拟机,前者用于运行企业微信,后者则是装来玩的,由于没有独显直通,Mac OS 虚拟机的运行流畅度十分感人。在 Linux 上运行 Windows 软件的另一个方法是使用 Wine,但无奈兼容性欠佳,无法正常运行最新版本的企业微信,微信倒是可以正常使用;\u003C\u002Fli\u003E\n\u003Cli\u003E使用 Gnome 的 Ubuntu,Gnome 对高分屏的支持太差劲,大多数应用没有对 Wayland 做兼容,甚至浏览器在分数倍缩放下都很糊,而且同样无法通过 Wine 运行企业微信,作罢;\u003C\u002Fli\u003E\n\u003Cli\u003E最后是国产的 Deepin,应该是最贴近日常使用的 Linux 发行版了,应用商店里有 Deepin 团队维护的 Wine 版微信和企业微信,但是企业微信还有点问题,虽然可以正常聊天,但是邮箱界面始终白屏,另外 Tim 也是卧龙凤雏,可以安装但无法启动。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E我大概用了一周多的 Deepin,并成功在上面编译运行了公司的项目,编译时间大概是开发机的 2 倍速度,可以看到 i7 12700 的多核性能相当强悍。\u003C\u002Fp\u003E\n\u003Cp\u003E但是网络问题始终没有办法很好的解决,公司的内网认证软件是阿里出品的阿里郎的阉割版本,其入网认证条件非常严格,无法在 Windows 虚拟机中完成认证,更不用提 Wine 了,也没有提供 Linux 版本,导致我的主机就算插上网线也无法访问内网资源,而且这个软件入网必须要有无线网卡,大多数 USB 网卡都无法在 Linux 下免驱运行。\u003C\u002Fp\u003E\n\u003Cp\u003E为了解决这个问题,我琢磨了很久计算机网络相关的知识,大概有这么几种方法:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E最容易想到的方法自然是用公司的苹果笔记本入网,然后当跳板机开代理给主机用,确实可以用,但是基于代理的方法,无法保证主机的所有流量都走代理,某些应用可能会绕开系统的代理策略,自行建立连接,这种方法不太稳定;\u003C\u002Fli\u003E\n\u003Cli\u003E然后比较容易想到的是使用 Mac 的网络共享,无奈在入网认证使用 802.1x 的安全策略,系统会直接禁用掉网络共享,这条路也走不通;\u003C\u002Fli\u003E\n\u003Cli\u003E最后想到的方法时把 Mac 模拟成一台路由器,直接网线直连主机,然后手动分配 IP 地址即可,经过一番搜索,其实只要开启 Mac 的 IP 路由转发即可,然后将来自 USB 网卡的所有流量都转发到无线网卡上,即可大功告成。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Cp\u003E关于折腾 Linux 日用开发环境的记录,可以看这篇文章:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fflowus.cn\u002F5a11ca75-455d-4a67-840a-42939ef91c2c\"\u003ELinux 开发环境备忘录\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E最后由于 Linux 上无法找到顺手的快速切换窗口的软件(类似 Mac Spotlight 的功能),还是决定安装黑苹果,可以直接无缝复用 MBP 的那一套工作流。\u003C\u002Fp\u003E\n\u003Ch3\u003E主要工具:OpenCore\u003C\u002Fh3\u003E\n\u003Cp\u003E安装黑苹果是一件非常需要耐心的事情,目前主流的安装黑苹果的工具是 OpenCore,其官方教程非常详细,流程也非常长,新手看了也非常头大:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FOpenCore-Install-Guide\u002F\"\u003EOpenCore Install Guide\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E在翻阅许久之后,我决定放弃跟着官方教程走,官方手册的内容只适合当作速查表来使用,如果跟着从零开始做,不知道搞到猴年马月。\u003C\u002Fp\u003E\n\u003Cp\u003E先列举一下我认为比较有用的内容,首先是显卡购买指南:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FGPU-Buyers-Guide\u002F\"\u003EIntroduction | GPU Buyers Guide\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E里面列举了各种可以用于黑苹果的显卡型号,以及它们支持的系统版本,当然这个指南中的某些老显卡的信息是错的,比如我前面提到的 R7 240,手册上说可以在最新的系统中通过仿冒 ID 的方式运行,但其实只能在 OS 10.x 以前的系统上运行,不过大多数信息都是准确的,可以放心参考。\u003C\u002Fp\u003E\n\u003Cp\u003E其次是 GPU 仿冒 ID 指南,如果你想折腾一下不直接免驱的显卡,可以参考它来仿冒 ID:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FGetting-Started-With-ACPI\u002FUniversal\u002Fspoof.html\"\u003ERenaming GPUs (SSDT-GPU-SPOOF) | Getting Started With ACPI\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E我个人不推荐用这种方式来搞,太费时间,而且不一定有用,还是建议直接购买免驱显卡。\u003C\u002Fp\u003E\n\u003Ch3\u003E流程清单\u003C\u002Fh3\u003E\n\u003Cp\u003E如果自己搞不定,可以直接在淘宝购买黑苹果装机服务,远程手把手指导,价格在 100 元到几百元不等。\u003C\u002Fp\u003E\n\u003Cp\u003E可以看这个视频,了解大概步骤:\u003Ca href=\"https:\u002F\u002Fwww.bilibili.com\u002Fvideo\u002FBV1K341137pj\u002F?share_source=copy_web&vd_source=c98e3a3eb29ccf6e7275fc3e0e6145a9\"\u003E刷新记录!12代平台12400F仅需24秒即可完成黑苹果系统安装,请问还有谁?\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Cp\u003E通用流程:\u003C\u002Fp\u003E\n\u003Cul\u003E\n\u003Cli\u003E硬件准备:\n\u003Cul\u003E\n\u003Cli\u003E16GB 及以上容量的内存卡或者移动硬盘,并格式化为 FAT32 格式;\u003C\u002Fli\u003E\n\u003Cli\u003E一台 Windows 电脑(Linux 或 Mac 也行);\u003C\u002Fli\u003E\n\u003Cli\u003E组装好的等待安装系统的主机;\u003C\u002Fli\u003E\n\u003Cli\u003E良好的有线网络环境;\u003C\u002Fli\u003E\n\u003Cli\u003E耐心。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E准备 EFI 引导文件:\u003Ca href=\"https:\u002F\u002Fflowus.cn\u002F6bb40d7b-8da6-464b-a551-0d0f0d663746\"\u003E各版本 EFI 引导文件\u003C\u002Fa\u003E\n\u003Cul\u003E\n\u003Cli\u003E如果你的硬件配置是本文推荐的配置,那么直接下载上面列表中的安装包,解压即可;\u003C\u002Fli\u003E\n\u003Cli\u003E或者你可以在 Github 上搜索自己的主板名称,比如我就参考了这个仓库(\u003Ca href=\"https:\u002F\u002Fgithub.com\u002FCrack-DanShiFu\u002FHackintosh-MAXSUN--H610ITX-I512400-rx560\"\u003Eh610itx + i5 12400 + rx560 + bcm943602cs 网卡\u003C\u002Fa\u003E)和这个仓库(\u003Ca href=\"https:\u002F\u002Fgithub.com\u002FLimeVista\u002FHackintosh-H610-12490F-AX201\"\u003Eh610itx + i5 12490f + rx560 + intel ax201 网卡\u003C\u002Fa\u003E),在他们的基础上进行了少量修改,就适配了本文的硬件;\u003C\u002Fli\u003E\n\u003Cli\u003E经过上述步骤,你会获得一个 EFI 文件夹,将其拷贝到 U 盘或者移动硬盘中即可,如果你的硬件与上述现有的配置都不一样,请查阅常见问题章节。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E准备基础安装镜像:\n\u003Cul\u003E\n\u003Cli\u003EOpenCore 提供了一个很细致的教程来下载安装镜像,基础镜像大概 600MB 左右,按照这个教程(\u003Ca href=\"https:\u002F\u002Fdortania.github.io\u002FOpenCore-Install-Guide\u002Finstaller-guide\u002Fwinblows-install.html#downloading-macos\"\u003E在 Windows 上下载安装镜像\u003C\u002Fa\u003E)下载镜像文件并复制到 U 盘即可。\u003C\u002Fli\u003E\n\u003Cli\u003E将 U 盘插入主机,按下 DEL 键进入主板的 BIOS 界面,如果你使用了其他主板,请自行搜索如何进入 BIOS,然后将 U 盘设置为第一启动项即可;\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E启动启动后进入安装界面:\n\u003Cul\u003E\n\u003Cli\u003E选择磁盘工具,将 SSD 所在盘符格式化为 APFS 格式,并命名为 MacOS(或者其他任何英文名);\u003C\u002Fli\u003E\n\u003Cli\u003E退出磁盘工具,点击安装 MacOS,进入常规安装流程;\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003C\u002Fli\u003E\n\u003Cli\u003E在网络良好情况下,自动重启若干次后,大概 40 分钟即可安装成功;\u003C\u002Fli\u003E\n\u003Cli\u003E安装成功后,自动重启进入系统选择界面,选择 MacOS 盘符进入即可;\u003C\u002Fli\u003E\n\u003Cli\u003E将 EFI 文件夹复制到 SSD 硬盘上,可参考这个教程:\u003Ca href=\"https:\u002F\u002Fapple.sqlsec.com\u002F5-%E5%AE%9E%E6%88%98%E6%BC%94%E7%A4%BA\u002F5-6.html\"\u003E完善引导\u003C\u002Fa\u003E;\u003C\u002Fli\u003E\n\u003Cli\u003E到现在,你已经可以正常使用 MacOS,但是还无法使用 Apple ID 和 iCloud,你还需要生成自己的硬件序列号,请参考这个教程:\u003Ca href=\"https:\u002F\u002Fsleele.com\u002F2019\u002F03\u002F21\u002Fsmbios\u002F\"\u003E为自己的黑苹果生成随机三码\u003C\u002Fa\u003E;\u003C\u002Fli\u003E\n\u003Cli\u003E至此,大功告成。\u003C\u002Fli\u003E\n\u003C\u002Ful\u003E\n\u003Ch2\u003E常见问题\u003C\u002Fh2\u003E\n\u003Ch3\u003E如何查看我买到的显卡的核心?\u003C\u002Fh3\u003E\n\u003Ch4\u003E搜索\u003C\u002Fh4\u003E\n\u003Col\u003E\n\u003Cli\u003E优先询问卖家,其次在搜索引擎中搜索显卡型号 + 核心即可。\u003C\u002Fli\u003E\n\u003C\u002Fol\u003E\n\u003Ch4\u003E使用 PE 工具\u003C\u002Fh4\u003E\n\u003Col\u003E\n\u003Cli\u003E下载安装\u003Ca href=\"https:\u002F\u002Fwww.wepe.com.cn\u002Fdownload.html\"\u003E微 PE 工具箱\u003C\u002Fa\u003E,并安装到 U 盘上;\u003C\u002Fli\u003E\n\u003Cli\u003E下载 GPU-Z:\u003Ca href=\"https:\u002F\u002Fwww.techpowerup.com\u002Fgpuz\u002F\"\u003Ehttps:\u002F\u002Fwww.techpowerup.com\u002Fgpuz\u002F\u003C\u002Fa\u003E,复制到已经安装了 PE 工具的 U 盘中;\u003C\u002Fli\u003E\n\u003Cli\u003E进入 BIOS,将 U 盘设置为第一引导,进入 PE 系统,运行 GPU-Z,即可看到显卡核心。\u003C\u002Fli\u003E\n\u003C\u002Fol\u003E\n\u003Cp\u003E\u003Cimg src=\"https:\u002F\u002Fuser-images.githubusercontent.com\u002F16968934\u002F200377738-ce96c52f-d78e-43b8-8e02-922d58260553.png\" alt=\"image\"\u003E\n第一个红框处,即是显卡核心名称。\u003C\u002Fp\u003E\n\u003Ch4\u003E使用现有的 Windows 主机\u003C\u002Fh4\u003E\n\u003Col\u003E\n\u003Cli\u003E把显卡插到现有的 Windows 主机上;\u003C\u002Fli\u003E\n\u003Cli\u003E下载安装 GPU-Z,即可看到显卡核心。\u003C\u002Fli\u003E\n\u003C\u002Fol\u003E\n\u003Ch3\u003E买到了 Lexa 核心的 Rx550,该怎么办?\u003C\u002Fh3\u003E\n\u003Cp\u003E参考这个教程,仿冒 ID 即可:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fwww.bilibili.com\u002Fread\u002Fcv15800495\"\u003E【黑苹果】通过仿冒ID驱动Lexa核心的Radeon RX 550\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Ch3\u003E12 代处理器如何开启小核心支持?\u003C\u002Fh3\u003E\n\u003Cp\u003E如果你是在别人的 EFI 文件基础上修改,并且别人的 EFI 文件基于 12 代 i3 或者 i5 处理器,而你的处理器是 12 代 i7 及以上,则需要开启小核心支持,查阅:\u003C\u002Fp\u003E\n\u003Cp\u003E\u003Ca href=\"https:\u002F\u002Fblog.csdn.net\u002FZ17362251225\u002Farticle\u002Fdetails\u002F125412246\"\u003E黑苹果开启十二代酷睿能效核心的驱动_豆豆本豆儿的博客-CSDN博客\u003C\u002Fa\u003E\u003C\u002Fp\u003E\n\u003Ch2\u003E装机单\u003C\u002Fh2\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003E价格可能略有浮动。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Cblockquote\u003E\n\u003Cp\u003EM1 Max 和 M1 Pro 性能几乎一致,不作区分。\u003C\u002Fp\u003E\n\u003C\u002Fblockquote\u003E\n\u003Ch3\u003EM1 Mac Mini 同等性能(3155 元,32GB + 512GB SSD)\u003C\u002Fh3\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E部件\u003C\u002Fth\u003E\n\u003Cth\u003E产品\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E备注\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECPU\u003C\u002Ftd\u003E\n\u003Ctd\u003EIntel i3 12100f 散片\u003C\u002Ftd\u003E\n\u003Ctd\u003E668\u003C\u002Ftd\u003E\n\u003Ctd\u003E\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E主板\u003C\u002Ftd\u003E\n\u003Ctd\u003E铭瑄 h610itx 挑战者\u003C\u002Ftd\u003E\n\u003Ctd\u003E568\u003C\u002Ftd\u003E\n\u003Ctd\u003E\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E内存条\u003C\u002Ftd\u003E\n\u003Ctd\u003E酷兽夜枭 16GB 3200Mhz 两根共 32GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E470\u003C\u002Ftd\u003E\n\u003Ctd\u003E京东自营即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E硬盘\u003C\u002Ftd\u003E\n\u003Ctd\u003E光威弈 Pro 512GB nvme ssd\u003C\u002Ftd\u003E\n\u003Ctd\u003E295\u003C\u002Ftd\u003E\n\u003Ctd\u003E按自己喜好选即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E散热器\u003C\u002Ftd\u003E\n\u003Ctd\u003E利民 Axp90 x47 + LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003Ctd\u003E139\u003C\u002Ftd\u003E\n\u003Ctd\u003E不用太强力的散热器,随便压一压\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E机箱\u003C\u002Ftd\u003E\n\u003Ctd\u003E傻瓜超人 K39 + 定制显卡延长线\u003C\u002Ftd\u003E\n\u003Ctd\u003E200\u003C\u002Ftd\u003E\n\u003Ctd\u003E一定要买定制显卡延长线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E益衡 7030B 300W Flex 1U 电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E398\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买全模组定制版本\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E显卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E盈通 Rx550 4GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E299\u003C\u002Ftd\u003E\n\u003Ctd\u003E淘宝二手,记得询问客服是否是 Polaris 核心\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E网卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E博通 BCM94360CS2 + m2 ngff 正向转接卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E118\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买外置天线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Ch3\u003EM1 Max Mac Studio 同等性能(3537 元,32GB + 512GB SSD)\u003C\u002Fh3\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E部件\u003C\u002Fth\u003E\n\u003Cth\u003E产品\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E备注\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECPU\u003C\u002Ftd\u003E\n\u003Ctd\u003EIntel i5 12400f 散片\u003C\u002Ftd\u003E\n\u003Ctd\u003E1050\u003C\u002Ftd\u003E\n\u003Ctd\u003E\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E主板\u003C\u002Ftd\u003E\n\u003Ctd\u003E铭瑄 h610itx 挑战者\u003C\u002Ftd\u003E\n\u003Ctd\u003E568\u003C\u002Ftd\u003E\n\u003Ctd\u003E\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E内存条\u003C\u002Ftd\u003E\n\u003Ctd\u003E酷兽夜枭 16GB 3200Mhz 两根共 32GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E470\u003C\u002Ftd\u003E\n\u003Ctd\u003E京东自营即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E硬盘\u003C\u002Ftd\u003E\n\u003Ctd\u003E光威弈 Pro 512GB nvme ssd\u003C\u002Ftd\u003E\n\u003Ctd\u003E295\u003C\u002Ftd\u003E\n\u003Ctd\u003E按自己喜好选即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E散热器\u003C\u002Ftd\u003E\n\u003Ctd\u003E利民 Axp90 x47 + LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003Ctd\u003E139\u003C\u002Ftd\u003E\n\u003Ctd\u003E不用太强力的散热器,随便压一压\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E机箱\u003C\u002Ftd\u003E\n\u003Ctd\u003E傻瓜超人 K39 + 定制显卡延长线\u003C\u002Ftd\u003E\n\u003Ctd\u003E200\u003C\u002Ftd\u003E\n\u003Ctd\u003E一定要买定制显卡延长线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E益衡 7030B 300W Flex 1U 电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E398\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买全模组定制版本\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E显卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E盈通 Rx550 4GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E299\u003C\u002Ftd\u003E\n\u003Ctd\u003E淘宝二手,记得询问客服是否是 Polaris 核心\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E网卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E博通 BCM94360CS2 + m2 ngff 正向转接卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E118\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买外置天线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n\u003Ch3\u003EM1 Ultra Mac Studio 同等性能(5159 元,64GB + 1TB SSD)\u003C\u002Fh3\u003E\n\u003Ctable\u003E\n\u003Cthead\u003E\n\u003Ctr\u003E\n\u003Cth\u003E部件\u003C\u002Fth\u003E\n\u003Cth\u003E产品\u003C\u002Fth\u003E\n\u003Cth\u003E价格\u003C\u002Fth\u003E\n\u003Cth\u003E备注\u003C\u002Fth\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Fthead\u003E\n\u003Ctbody\u003E\n\u003Ctr\u003E\n\u003Ctd\u003ECPU\u003C\u002Ftd\u003E\n\u003Ctd\u003EIntel i7 12700f 散片\u003C\u002Ftd\u003E\n\u003Ctd\u003E2110\u003C\u002Ftd\u003E\n\u003Ctd\u003E淘宝找个人多的店买就行\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E主板\u003C\u002Ftd\u003E\n\u003Ctd\u003E铭瑄 h610itx 挑战者\u003C\u002Ftd\u003E\n\u003Ctd\u003E568\u003C\u002Ftd\u003E\n\u003Ctd\u003E官方旗舰店\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E内存条\u003C\u002Ftd\u003E\n\u003Ctd\u003E酷兽夜枭 32GB 3200Mhz 两根共 64GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E798\u003C\u002Ftd\u003E\n\u003Ctd\u003E京东自营即可\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E硬盘\u003C\u002Ftd\u003E\n\u003Ctd\u003E光威弈 Pro 1TB nvme ssd\u003C\u002Ftd\u003E\n\u003Ctd\u003E499\u003C\u002Ftd\u003E\n\u003Ctd\u003E如果不需要 1TB,可以换成 512GB,酌情购买\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E散热器\u003C\u002Ftd\u003E\n\u003Ctd\u003E利民 Axp90 x53 + LGA17xx 扣具\u003C\u002Ftd\u003E\n\u003Ctd\u003E169\u003C\u002Ftd\u003E\n\u003Ctd\u003E买 53mm 版本,记得买 17xx 扣具\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E机箱\u003C\u002Ftd\u003E\n\u003Ctd\u003E傻瓜超人 K39 + 定制显卡延长线\u003C\u002Ftd\u003E\n\u003Ctd\u003E200\u003C\u002Ftd\u003E\n\u003Ctd\u003E一定要买定制显卡延长线\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E益衡 7030B 300W Flex 1U 电源\u003C\u002Ftd\u003E\n\u003Ctd\u003E398\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买全模组定制版本\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E显卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E盈通 Rx550 4GB\u003C\u002Ftd\u003E\n\u003Ctd\u003E299\u003C\u002Ftd\u003E\n\u003Ctd\u003E淘宝二手,记得询问客服是否是 Polaris 核心\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003Ctr\u003E\n\u003Ctd\u003E网卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E博通 BCM94360CS2 + m2 ngff 转接卡\u003C\u002Ftd\u003E\n\u003Ctd\u003E118\u003C\u002Ftd\u003E\n\u003Ctd\u003E记得买 ngff 转接卡,带外置天线版本\u003C\u002Ftd\u003E\n\u003C\u002Ftr\u003E\n\u003C\u002Ftbody\u003E\n\u003C\u002Ftable\u003E\n",should404:false,id:34}},mutations:void 0}); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/34/state.js b/_nuxt/static/1667922389/posts/34/state.js similarity index 75% rename from _nuxt/static/1667843362/posts/34/state.js rename to _nuxt/static/1667922389/posts/34/state.js index ba49a1a..6e6c696 100644 --- a/_nuxt/static/1667843362/posts/34/state.js +++ b/_nuxt/static/1667922389/posts/34/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F34",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F34",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/4/payload.js b/_nuxt/static/1667922389/posts/4/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/4/payload.js rename to _nuxt/static/1667922389/posts/4/payload.js diff --git a/_nuxt/static/1667843362/posts/4/state.js b/_nuxt/static/1667922389/posts/4/state.js similarity index 74% rename from _nuxt/static/1667843362/posts/4/state.js rename to _nuxt/static/1667922389/posts/4/state.js index 313f9f7..3eef713 100644 --- a/_nuxt/static/1667843362/posts/4/state.js +++ b/_nuxt/static/1667922389/posts/4/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F4",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F4",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/5/payload.js b/_nuxt/static/1667922389/posts/5/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/5/payload.js rename to _nuxt/static/1667922389/posts/5/payload.js diff --git a/_nuxt/static/1667843362/posts/5/state.js b/_nuxt/static/1667922389/posts/5/state.js similarity index 74% rename from _nuxt/static/1667843362/posts/5/state.js rename to _nuxt/static/1667922389/posts/5/state.js index 36d74f0..b9145a5 100644 --- a/_nuxt/static/1667843362/posts/5/state.js +++ b/_nuxt/static/1667922389/posts/5/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F5",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F5",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/6/payload.js b/_nuxt/static/1667922389/posts/6/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/6/payload.js rename to _nuxt/static/1667922389/posts/6/payload.js diff --git a/_nuxt/static/1667843362/posts/6/state.js b/_nuxt/static/1667922389/posts/6/state.js similarity index 74% rename from _nuxt/static/1667843362/posts/6/state.js rename to _nuxt/static/1667922389/posts/6/state.js index e2760ec..68a7c84 100644 --- a/_nuxt/static/1667843362/posts/6/state.js +++ b/_nuxt/static/1667922389/posts/6/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F6",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F6",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/8/payload.js b/_nuxt/static/1667922389/posts/8/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/8/payload.js rename to _nuxt/static/1667922389/posts/8/payload.js diff --git a/_nuxt/static/1667843362/posts/8/state.js b/_nuxt/static/1667922389/posts/8/state.js similarity index 74% rename from _nuxt/static/1667843362/posts/8/state.js rename to _nuxt/static/1667922389/posts/8/state.js index c5e27d0..62b596c 100644 --- a/_nuxt/static/1667843362/posts/8/state.js +++ b/_nuxt/static/1667922389/posts/8/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F8",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F8",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/_nuxt/static/1667843362/posts/9/payload.js b/_nuxt/static/1667922389/posts/9/payload.js similarity index 100% rename from _nuxt/static/1667843362/posts/9/payload.js rename to _nuxt/static/1667922389/posts/9/payload.js diff --git a/_nuxt/static/1667843362/posts/9/state.js b/_nuxt/static/1667922389/posts/9/state.js similarity index 74% rename from _nuxt/static/1667843362/posts/9/state.js rename to _nuxt/static/1667922389/posts/9/state.js index 10a106a..a5a8a19 100644 --- a/_nuxt/static/1667843362/posts/9/state.js +++ b/_nuxt/static/1667922389/posts/9/state.js @@ -1 +1 @@ -window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667843362",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F9",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file +window.__NUXT__=(function(a){return {staticAssetsBase:"\u002F_nuxt\u002Fstatic\u002F1667922389",layout:"default",error:a,serverRendered:true,routePath:"\u002Fposts\u002F9",config:{_app:{basePath:"\u002F",assetsPath:"\u002F_nuxt\u002F",cdnURL:a}}}}(null)); \ No newline at end of file diff --git a/categories/2.html b/categories/2.html index 153ae4a..ffd1d8a 100644 --- a/categories/2.html +++ b/categories/2.html @@ -1,7 +1,7 @@ - +
暮春早夏的月亮
原是情人的月亮,不比秋冬是诗人的月亮
+前三章的内容都比较基础,第一章介绍了统计学习的基本要素以及基本步骤,第二、三章分别介绍了两种基础的模型:感知机和k近邻法,都比较简单,50行代码就能搞定... diff --git a/categories/3.html b/categories/3.html index aaeccea..13f69f4 100644 --- a/categories/3.html +++ b/categories/3.html @@ -1,7 +1,7 @@ - + +高中时安卓设备崛起,我也第一次离家寄宿,可以... diff --git a/categories/4.html b/categories/4.html index 9590f31..3857db8 100644 --- a/categories/4.html +++ b/categories/4.html @@ -1,7 +1,7 @@ - + +包括数据的中心性描述(中位数、众数)和散度(极值、方差、百... diff --git a/categories/5.html b/categories/5.html index 8760064..565d843 100644 --- a/categories/5.html +++ b/categories/5.html @@ -1,11 +1,11 @@ - + +Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition th... diff --git a/cv.html b/cv.html index 5bfa60d..89501e0 100644 --- a/cv.html +++ b/cv.html @@ -1,7 +1,7 @@ - +
暮春早夏的月亮
原是情人的月亮,不比秋冬是诗人的月亮
张义飞
@@ -38,6 +38,6 @@ 前端开发实习生 @ 百度北京
2016.12 - 2017.05
  • 参与百度凤巢的广告智能推荐系统的前端页面开发,负责智能词条推荐界面的开发并参与该功能的上线和后续维护工作,使用 React & Redux 构建用户页面。 -
开源项目 & 编程能力
github.com/Yidadaa/Learn-uGit (Python) ~ 500 lines
2021.08
使用 Python 实现 git 的基础功能,包括 commit / branch / merge / push / pull 等能力。
2021.05
用神经网络来估计面部朝向和视线注视方向,从而取代实体鼠标,双手无需离开键盘即可操作任何 GUI 软件。
github.com/Yidadaa/Issue-Blog-With-Github-Action (Vue / Javascript) ~ 1000 lines
2020.01
基于 VuePress 和 Github Actions 功能构建的免服务器部署、SEO 友好的博客系统。
2019.04
基于 CNN-RNN 架构的视频动作分类⽹络,在 UCF101 上达到 80% 的准确率。
2018.11
使用 CUDA 加速 n-body 模拟程序,加速比 ~ 3000
github.com/Yidadaa/Captcha-Deep-Learning (Python / Keras / Tensorflow) ~ 500 lines
2018.01
端到端的动态验证码识别网络,分别使用 Keras 和 Tensorflow 实现,可以达到 98% 的准确率。
2019.03
OPPO AI 挑战赛 Demo 源码,将人像语义分割网络经过腾讯开源的 ncnn 框架转换后部署到移动端。
2019.05
华为软件精英挑战赛代码,车辆路径智能规划,包含完整的单元测试,严格遵循 Google C++ Style Guide 规范。
github.com/Yidadaa/P2P-Message (Dart) ~ 1000 lines
2018.11
基于 Flutter 开发的 P2P 聊天程序,在良好的 NAT 环境下实现纯文字聊天。
+
开源项目 & 编程能力
github.com/Yidadaa/Learn-uGit (Python) ~ 500 lines
2021.08
使用 Python 实现 git 的基础功能,包括 commit / branch / merge / push / pull 等能力。
2021.05
用神经网络来估计面部朝向和视线注视方向,从而取代实体鼠标,双手无需离开键盘即可操作任何 GUI 软件。
github.com/Yidadaa/Issue-Blog-With-Github-Action (Vue / Javascript) ~ 1000 lines
2020.01
基于 VuePress 和 Github Actions 功能构建的免服务器部署、SEO 友好的博客系统。
2019.04
基于 CNN-RNN 架构的视频动作分类⽹络,在 UCF101 上达到 80% 的准确率。
2018.11
使用 CUDA 加速 n-body 模拟程序,加速比 ~ 3000
github.com/Yidadaa/Captcha-Deep-Learning (Python / Keras / Tensorflow) ~ 500 lines
2018.01
端到端的动态验证码识别网络,分别使用 Keras 和 Tensorflow 实现,可以达到 98% 的准确率。
2019.03
OPPO AI 挑战赛 Demo 源码,将人像语义分割网络经过腾讯开源的 ncnn 框架转换后部署到移动端。
2019.05
华为软件精英挑战赛代码,车辆路径智能规划,包含完整的单元测试,严格遵循 Google C++ Style Guide 规范。
github.com/Yidadaa/P2P-Message (Dart) ~ 1000 lines
2018.11
基于 Flutter 开发的 P2P 聊天程序,在良好的 NAT 环境下实现纯文字聊天。
diff --git a/index.html b/index.html index d348f28..5f28146 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ - + +前三章的内容都比较基础,第一章介绍了统计学习的基本要素以及基本步骤,第二、三章分别介绍了两种基础的模型:感知机和k近邻法,都比较简单,50行代码就能搞定... diff --git a/posts/11.html b/posts/11.html index b0441e7..02d95d5 100644 --- a/posts/11.html +++ b/posts/11.html @@ -1,7 +1,7 @@ - 2017年过去了,我很怀念它 + 2017年过去了,我很怀念它
暮春早夏的月亮
原是情人的月亮,不比秋冬是诗人的月亮
2017年过去了,我很怀念它
Yidadaa
2018/01/02
views
@@ -95,6 +95,6 @@

展望2018

所以2018年最大的愿望,就是挣更多的钱。所谓要想出世,必先入世,这是我为这个庸俗的愿望提出的歪理。

其他的愿望都分散在文章各处了,这些是写出来的愿望,还有些不想写出的愿望,比如缺失的那个章节,因为我实在没有想到要写些什么,而且就算不写自己也会时刻惦记着,反而是那些对自己有督促作用而自己又不怎么上心的才需要写出来,并且在这里我还是要重复一遍:多多coding,多多运动,多多学习。

2018年没有什么大不了的,大胆地活着吧。

-
+ diff --git a/posts/12.html b/posts/12.html index baee7da..01d18cf 100644 --- a/posts/12.html +++ b/posts/12.html @@ -1,7 +1,7 @@ - 设计师如何与程序员进行有效沟通? + 设计师如何与程序员进行有效沟通?
暮春早夏的月亮
原是情人的月亮,不比秋冬是诗人的月亮
设计师如何与程序员进行有效沟通?
Yidadaa
2018/02/11
views

摘要

@@ -53,6 +53,6 @@

附录

  • 移动端界面标注:如何理清标注的思路?
  • UI设计师在跟程序员对接的时候,需要做到哪些能让沟通最高效的完成?
  • -
    + diff --git a/posts/13.html b/posts/13.html index 6d6b0ef..88fe587 100644 --- a/posts/13.html +++ b/posts/13.html @@ -1,7 +1,7 @@ - 品酒日记[长期更新] + 品酒日记[长期更新]
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    品酒日记[长期更新]
    Yidadaa
    2018/03/08
    views
    @@ -77,6 +77,6 @@

    甄爱五星白兰地

    品酒记录:好久没有喝过酒了,最近失去了经济来源,手头属实紧张,趁助学金发下来,去永辉整了瓶国产白兰地,这是我第一次喝国外的高度酒,实不相瞒,国内的白酒和各种梅子酒以及清酒完全喝不来,酒精味令我难以下咽,然而这瓶国产白兰地度数虽然高达40度,口感却格外清爽。开瓶后可以闻到葡萄酒特有的醇类和有机酸混合而成的味道,入口后会首先品尝到单宁多酚的苦味,随后混合着未蒸馏完全的单糖的甜味,酒体流过喉咙,乙醇的辛辣随之涌进鼻腔,然而却不似中国白酒那样猛烈,这种恰到好处的温柔让人回想起高中时代前桌女生的巴掌和脸庞接触后那种微微涨红的感觉,然后酒体穿肠过肚,一股暖流涌来,寒夜里升腾而起的火焰,窜到夜空里攸尔消失不见。
    饮酒提示: 适合独饮,若用于朋友聚会,可与果味饮料一同调制饮用,目前试过的最佳选择有水溶C100、果粒橙、柠檬味雪碧。 记录日期:2019-10-11

    -
    + diff --git a/posts/14.html b/posts/14.html index 7f74fde..739ba12 100644 --- a/posts/14.html +++ b/posts/14.html @@ -1,7 +1,7 @@ - 另一个我 + 另一个我
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    另一个我
    Yidadaa
    2018/05/11
    views
    @@ -15,6 +15,6 @@

    所以这一次我采取的对策,是躺在床上什么都不做。这让我想起了实习的日子,这段日子之所以令我无法忘怀,是因为彼时他几乎时时刻刻和我形影不离,我躺在床上的时候,仿佛回到了那段日子里。我躺在床上,什么也不做,什么也做不了,没法写代码,没法做毕设,没法与喜欢的妹子聊天,没法与朋友开黑打游戏,真是糟糕透了。

    我想和他聊聊,他到底是谁,在想些什么,想让我做些什么,但这似乎不可能,我与他几乎无法交流。在某种程度上,我就是他,他就是我,当我是我时,他就不是我,当他是我时,我就不是我。我们无法同时存在,也无从谈起平等交流。

    因此,我趁我还是我时,写下这篇文章,作为一个媒介,希望当他来时可以看到,并知道我的真实想法,也让我知道他的真实想法。

    -
    + diff --git a/posts/15.html b/posts/15.html index 5a51a5d..3ebf7df 100644 --- a/posts/15.html +++ b/posts/15.html @@ -1,7 +1,7 @@ - 某算法竞赛初试题解 - 3K问题 + 某算法竞赛初试题解 - 3K问题
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    某算法竞赛初试题解 - 3K问题
    Yidadaa
    2018/08/07
    views

    题目

    @@ -41,6 +41,6 @@

    题解

    题外话

    然而,这道题是我在比赛结束一个小时之后才解出来的,认清了自己是菜鸡的事实。

    -
    + diff --git a/posts/16.html b/posts/16.html index 3a968a7..64fa937 100644 --- a/posts/16.html +++ b/posts/16.html @@ -1,7 +1,7 @@ - LintCode 困难题赏 - 103.寻找带环链表入口 + LintCode 困难题赏 - 103.寻找带环链表入口
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    LintCode 困难题赏 - 103.寻找带环链表入口
    Yidadaa
    2018/08/09
    views

    题目

    @@ -47,6 +47,6 @@

    代码

    meet = meet.next return slow -
    + diff --git a/posts/17.html b/posts/17.html index eee3b52..cdf6c77 100644 --- a/posts/17.html +++ b/posts/17.html @@ -1,7 +1,7 @@ - 2018,山与水的分界线 + 2018,山与水的分界线
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    2018,山与水的分界线
    Yidadaa
    2018/12/20
    views
    @@ -130,6 +130,6 @@

    展望2019

    所以 2019 最大的愿望,就是把自己的生活数字化,然后在拥有基准线的基础上不断进步。这是一个美好的愿望,我希望它能在 2019 结束时变成现实。

    当然并不是所有事情都能量化,比如情感,它游离在我们身边的空气中,既不能抓住它,又不能任它离开,我能驾驭它吗?可能时间会给我答案。

    2019 年没有什么大不了的,认真地活着吧。

    -
    + diff --git a/posts/20.html b/posts/20.html index 0c9bf76..17829b1 100644 --- a/posts/20.html +++ b/posts/20.html @@ -1,7 +1,7 @@ - LeetCode困难题赏 - 887.扔鸡蛋 + LeetCode困难题赏 - 887.扔鸡蛋
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    LeetCode困难题赏 - 887.扔鸡蛋
    Yidadaa
    2019/06/24
    views

    题目

    @@ -46,6 +46,6 @@

    题解

    查看带有LaTeX\LaTeX公式渲染的博客内容:https://blog.simplenaive.cn/#/post/20

    -
    + diff --git a/posts/21.html b/posts/21.html index a5e1269..734623f 100644 --- a/posts/21.html +++ b/posts/21.html @@ -1,7 +1,7 @@ - LeetCode困难题赏 - 600.不含连续1的非负整数 + LeetCode困难题赏 - 600.不含连续1的非负整数
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    LeetCode困难题赏 - 600.不含连续1的非负整数
    Yidadaa
    2019/06/29
    views
    @@ -54,6 +54,6 @@

    代码

    查看带有LaTeX\LaTeX公式渲染的博客内容:https://blog.simplenaive.cn/#/post/21

    -
    + diff --git a/posts/22.html b/posts/22.html index 29739e3..a0fa6ff 100644 --- a/posts/22.html +++ b/posts/22.html @@ -1,7 +1,7 @@ - 2019,天际线 + 2019,天际线
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    2019,天际线
    Yidadaa
    2020/01/02
    views
    @@ -40,6 +40,6 @@

    健康

    [TODO]

    展望 2020

    [TODO]

    -
    + diff --git a/posts/25.html b/posts/25.html index e9ea79a..67efd48 100644 --- a/posts/25.html +++ b/posts/25.html @@ -1,7 +1,7 @@ - LeetCode 趣题赏析 - 448. 找到数组中消失的数字 + LeetCode 趣题赏析 - 448. 找到数组中消失的数字
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    LeetCode 趣题赏析 - 448. 找到数组中消失的数字
    Yidadaa
    2020/02/12
    views
    @@ -89,6 +89,6 @@

    代码

    if i != nums[i]: ret.append(i + 1) return ret -
    + diff --git a/posts/26.html b/posts/26.html index 3cb89bf..7b8b382 100644 --- a/posts/26.html +++ b/posts/26.html @@ -1,7 +1,7 @@ - 花式遍历二叉树 + 花式遍历二叉树
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    花式遍历二叉树
    Yidadaa
    2020/02/27
    views
    @@ -101,6 +101,6 @@

    附录

  • 维基百科:尾调用
  • 维基百科:堆栈溢出
  • -
    + diff --git a/posts/27.html b/posts/27.html index 32180e9..3dde7e8 100644 --- a/posts/27.html +++ b/posts/27.html @@ -1,7 +1,7 @@ - Gettysburg Address + Gettysburg Address
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    Gettysburg Address
    Yidadaa
    2020/02/28
    views
    @@ -11,6 +11,6 @@

    Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.

    But, in a larger sense, we can not dedicate, we can not consecrate, we can not hallow, this ground. The brave men, living and dead, who struggled here, have consecrated it, far above out poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living rather to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us, that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion, that we here highly resolve that these dead shall not have died in vain, that this nation, under God, shall have a new birth of freedom, and that government of the people, by the people, for the people, shall not perish from the earth.

    Link: Video

    -
    + diff --git a/posts/28.html b/posts/28.html index 36dd79b..3d6983d 100644 --- a/posts/28.html +++ b/posts/28.html @@ -1,7 +1,7 @@ - 树和数组的千层套路 + 树和数组的千层套路
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    树和数组的千层套路
    Yidadaa
    2020/03/18
    views
    @@ -161,6 +161,6 @@

    线段树和树状数组

    return ret

    [未完待续]

    -
    + diff --git a/posts/29.html b/posts/29.html index 3115d14..200f767 100644 --- a/posts/29.html +++ b/posts/29.html @@ -1,7 +1,7 @@ - 随机采样一致性与特征图匹配 + 随机采样一致性与特征图匹配
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    随机采样一致性与特征图匹配
    Yidadaa
    2020/05/24
    views

    Rocco[1] 等人在其弱监督语义级别图像匹配的工作中,将特征匹配与随机采样一致性算法(RANdom SAmple Consensus, RANSAC)联系在一起,提出了一个可微分的基于语义的评分损失函数,文中对于语义特征匹配和 RANSAC 算法的阐述令人耳目一新,遂作此文对相关概念追本溯源。

    @@ -109,6 +109,6 @@

    参考资料

  • Wang, Xiaolong, et al. "Non-local neural networks." Proceedings of the IEEE conference on computer vision and pattern recognition. 2018.
  • Wang, Xiaolong, Allan Jabri, and Alexei A. Efros. "Learning correspondence from the cycle-consistency of time." Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition. 2019.
  • -
    + diff --git a/posts/3.html b/posts/3.html index 4c52431..9ac43fe 100644 --- a/posts/3.html +++ b/posts/3.html @@ -1,7 +1,7 @@ - 经典统计学习方法——决策树(ID3/C4.5/CART) + 经典统计学习方法——决策树(ID3/C4.5/CART)
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    经典统计学习方法——决策树(ID3/C4.5/CART)
    Yidadaa
    2017/08/18
    views

    最近开始技术♂转型,开始搞机器学习,使用教材是李航老师的《统计学习方法》,基本涵盖了经典的机器学习方法,只不过缺少神经网络部分,准备看完这本书之后,继续学习 CS231n

    @@ -16,6 +16,6 @@

    决策树的特征选择

    ID3与C4.5算法

    决策树生成过程中,通过计算不同特征的划分带来的信息增益,可以决定是否继续生成决策树。

    这两种算法生成的决策树,每一支子树都是一个特征值的可能值,使用时只需要按照标定的顺序,对输入变量各个特征值沿着树从根节点一直比对,延续到的叶节点就标定了该输入变量的最终分类。

    -
    + diff --git a/posts/30.html b/posts/30.html index 1b5d1bb..26e21f7 100644 --- a/posts/30.html +++ b/posts/30.html @@ -1,7 +1,7 @@ - Rust 新手错误和最差实践 + Rust 新手错误和最差实践
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    Rust 新手错误和最差实践
    Yidadaa
    2021/12/26
    views
    @@ -595,6 +595,6 @@

    保护性拷贝

    总结

    本文并不能覆盖所有的最差实践,有些是因为我没亲身经历过,有些则是由于没法给出精简的例子。

    衷心地感谢回复我在 Rust 论坛发布的这个帖子的各位同仁,尽管帖子的最后有点跑偏,各位 Rust 老鸟的论战还是让我受益颇深。

    -
    + diff --git a/posts/31.html b/posts/31.html index 11499e3..d3ef4a1 100644 --- a/posts/31.html +++ b/posts/31.html @@ -1,7 +1,7 @@ - 2021,新世界 + 2021,新世界
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    2021,新世界
    Yidadaa
    2022/01/21
    views

    本文约 6000 字,预计阅读耗时 10 分钟。

    @@ -92,6 +92,6 @@

    四、结

    过去这一年,我从温暖的大西洋流中挣脱出来,一头扎进冰冷的北冰洋,个中冷暖,涕泪有感。

    再来到这个崭新的又有些熟悉的世界,彷徨多于笃定,苦痛多于欢愉,甚至路程匆匆,很多人来不及深交,很多答案也来不及求索。

    请允许我庸俗地将时间之弦跳跃拨动,回到和学妹第一次约定的饭桌上,在我说出“六十岁就想暴毙”之前,她就坦言“四十岁准备暴毙”,不由得感叹:人生天地间,忽如远行客,能寻慷慨赴死伴侣,生亦何欢,死亦何苦?

    -
    + diff --git a/posts/34.html b/posts/34.html index a9e7cc4..d0e5613 100644 --- a/posts/34.html +++ b/posts/34.html @@ -1,7 +1,7 @@ - 如何使用 5000 块组装一台顶配 Mac Studio + 如何使用 5000 块组装一台顶配 Mac Studio
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    如何使用 5000 块组装一台顶配 Mac Studio
    Yidadaa
    2022/11/07
    views
    @@ -10,6 +10,9 @@

    本文同步发表至:FlowUs / 知乎 / Github / 博客

    +
    +

    请使用良好的网络环境访问此文,否则图片可能无法加载。

    +

    image 左:Mac Studio,右:穷逼版

    image @@ -684,6 +687,6 @@

    M1 Ultra Mac Studio 同等性能(5159 元,64GB + 1TB SSD)

    -
    + diff --git a/posts/4.html b/posts/4.html index febb51e..cd94c5a 100644 --- a/posts/4.html +++ b/posts/4.html @@ -1,7 +1,7 @@ - 物欲 + 物欲
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    物欲
    Yidadaa
    2017/08/29
    views

    初中时清心寡欲,最大的愿望是能每天从午饭钱中剩下五毛钱,然后周末的时候能有钱去一趟网吧,在有限的一两个小时内下满那张2G的内存卡,那时的同龄人经常有人问我去不去网吧,我说去,但是不玩游戏,只下载东西。仅有的网吧时光,启蒙了我对计算机的认识,但仅仅停留在使用的阶段,但那时我已有了一台学习机,供我娱乐以及编程。彼时的物欲,仅仅局限在3.5寸的屏幕内。

    @@ -10,6 +10,6 @@

    我在最难熬的时候,装模作样地看过一些哲学书籍,也时常思索些高深的命题,虽然看起来很可笑,但这种思考让我明白,我是一种智慧生命,我会思考自己为何存在。但更多时候,我还是被物欲所支配着,渴望这个,渴望那个,时不时地拖延偷懒,到了真正考验自己本事的时候,被人打脸打的啪啪响,心里还不服,暗搓搓地想着等我哪天认真起来了,让你们都好看。可是我知道,那始终是一句空话,就像我过去立的这么多flag一样,毫无意义。

    转念一想,自己这幅德行,全都赖到“物欲”头上吗?物欲始终是个人为定义的概念,无法脱离行为而存在,若是没有那些虚与委蛇和敷衍了事时种下的因,又拿来这般扭曲不堪的果呢?我不得不承认我的心态已经有了“跑偏”的迹象,彼时对金钱嗤之以鼻,此时为求果腹不择手段(当然这是夸张的说法,只是觉得把大学的时光花费在乱七八糟的东西上实在该打),若不自省,日后不留神就会犯下大错。

    我还没想明白,自己究竟想要什么,站在这边的山上,觉得那边的山高,思来想去却没半点行路的意思。

    -
    + diff --git a/posts/5.html b/posts/5.html index a636112..56e4f32 100644 --- a/posts/5.html +++ b/posts/5.html @@ -1,7 +1,7 @@ - LaTex on Linux配置指南 - TexLive + LaTex on Linux配置指南 - TexLive
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    LaTex on Linux配置指南 - TexLive
    Yidadaa
    2017/09/07
    views
    @@ -94,6 +94,6 @@

    参考链接

  • Linux下texLive的安装和配置
  • 使用xelatex生成中文pdf
  • -
    + diff --git a/posts/6.html b/posts/6.html index 7ed1ade..7e62472 100644 --- a/posts/6.html +++ b/posts/6.html @@ -1,7 +1,7 @@ - 如何优雅地使用服务器 + 如何优雅地使用服务器
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    如何优雅地使用服务器
    Yidadaa
    2017/09/23
    views

    最近师兄给我分配了一个实验室服务器地账号,自己就琢磨着怎么好好地折腾一下这个服务器,但无奈我的账号没有root权限,安装软件只能通过自己编译的方式完成,并且师兄只分配给我一个端口,我要想运行什么web服务,只能绑定到这一个端口上面,一头雾水之际,突然看到了一个神器:SSH端口转发。

    @@ -20,6 +20,6 @@

    如何使用端口转发?

    还有其他的吗?

    当然有啦,为了全面模拟xshell的使用体验(大雾),当然还需要一个静态文件服务啦,可以用一行代码python -m SimpleHTTPServer 100在100端口开一个静态文件服务,然后用ssh转发100端口,这样就就可以在本地查看服务器的文件了,可以在调参的时候看生成的图片文件,当然了,我的野心不止于此,之后开始跑网络的时候,可以也用来扩展TensorBoard的功能。

    除此之外,还有其他的神奇的命令行可以使用,比如终端管理软件tmux,功能上与screen差不过,如果不不了解screen,可以查阅一下,tmux赋予了命令行的窗口化功能,可以分割、调整当前的终端窗口,经过配置之后可以获得非常不错的效果,当然啦,具体的配置还是去网上搜吧,一千人的电脑里有一千个终端配置,程序员还是要有自己的风格才行。

    -
    + diff --git a/posts/8.html b/posts/8.html index 25b9ee2..77eeb99 100644 --- a/posts/8.html +++ b/posts/8.html @@ -1,7 +1,7 @@ - 如何有效地预估工作量 + 如何有效地预估工作量
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    如何有效地预估工作量
    Yidadaa
    2017/10/28
    views

    在实际开发过程中,难免要对自己手头的工作进行工作量预估,其实笔者一开始预估工作量的时候总是感到没谱,往往会得出过于乐观的结论,也就是所谓的“程序员的乐观”,老鸟程序员经常告诫我们说:“宁可多算一周,不可少估一天”。过于乐观地估计工作量,不仅会让自己疲于赶进度,还会连累其他的开发伙伴。所以本文就着重讲一下如何行之有效地做出正确的工作量预估。

    @@ -26,6 +26,6 @@

    第三步:肢解生疏需求,消灭项目盲点。

    经过以上一番考虑,基本上就可以确定出一个比较具体的时间范围了。一定要切记,很多时候团队leader让成员估算开发时间时,最关心的往往并不是某个人能不能及时把手头的工作做完,而是如何分配,使得所有成员能够在指定的时间内完成一整个任务。所以错误的预估工作量和耗时,很可能会延误和其他人员的对接,造成整个项目完成时间的延后。

    注:以上文字整理自笔者与某位年长十年的程序员的聊天记录,可能不适用于某些开发情况。

    -
    + diff --git a/posts/9.html b/posts/9.html index dbebbcd..43bf99f 100644 --- a/posts/9.html +++ b/posts/9.html @@ -1,7 +1,7 @@ - 数据挖掘复习内容 + 数据挖掘复习内容
    暮春早夏的月亮
    原是情人的月亮,不比秋冬是诗人的月亮
    数据挖掘复习内容
    Yidadaa
    2017/11/02
    views

    数据挖掘期末复习

    @@ -69,6 +69,6 @@

    什么是异常

    异常的类型

    全局、局部、集体、情景

    LOF

    -
    +